Changeset 37287
- Timestamp:
- 2023-02-07T16:25:32+13:00 (8 months ago)
- 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 291 291 // Audio Scripts for Enriched Playback 292 292 293 var wavesurfer; 294 293 295 function loadAudio(audio, sectionData) { 294 296 let editMode = false; … … 304 306 let dualMode = false; 305 307 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; 306 316 307 317 let editsMade = false; … … 315 325 // let accentColour = "#F8C537"; 316 326 let regionTransparency = "50"; 327 let colourbrewerSet = colorbrewer.Set2[8]; 328 let regionColourSet = []; 317 329 318 330 let waveformContainer = document.getElementById("waveform"); 319 331 320 letwavesurfer = WaveSurfer.create({ // wavesurfer options332 wavesurfer = WaveSurfer.create({ // wavesurfer options 321 333 container: waveformContainer, 322 334 backend: "MediaElement", … … 355 367 'font-size': '12px' 356 368 }, 357 formatTimeCallback: (num) => { return minutize(num); }369 formatTimeCallback: (num) => { return formatCursor(num); } 358 370 }), 359 371 ], … … 367 379 368 380 function handleRegionClick(region, e) { 381 contextMenu.classList.remove('visible'); 369 382 e.stopPropagation(); 370 383 if (!editMode) { // play region audio on click 371 384 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 } 375 393 prevUndoState = ""; 376 394 … … 378 396 currentRegions = []; 379 397 if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) { 380 removeCurrentRegion(); // deselect current region on click398 // removeCurrentRegion(); // deselect current region on click 381 399 } else { 382 400 currentRegion = region; 383 currentRegion.speaker = currentRegion.attributes.label ;401 currentRegion.speaker = currentRegion.attributes.label.innerText; 384 402 region.play(); // start and stop to move play cursor to beginning of region 385 403 wavesurfer.playPause(); … … 393 411 if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed 394 412 currentRegion = currentRegions[0]; 395 // currentRegions = [];396 413 } 397 414 } else { … … 399 416 if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist 400 417 currentRegion = region; 401 currentRegion.speaker = currentRegion.attributes.label ;418 currentRegion.speaker = currentRegion.attributes.label.innerText; 402 419 region.play(); 403 420 wavesurfer.playPause(); … … 405 422 if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one 406 423 } else if (e.shiftKey) { // shift was held during click 424 clearChapterSearch(); 407 425 if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) { 408 426 if (currentRegions && currentRegions.length > 0) { … … 421 439 } 422 440 } 423 if ( speakerCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }441 if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) } 424 442 reloadRegionsAndChapters(); 425 443 } … … 428 446 function getIndexInCurrentRegions(region) { 429 447 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) { 432 450 return currentRegions.indexOf(reg); 433 451 } … … 438 456 function getIndexOfRegion(region) { 439 457 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) { 441 459 return currSpeakerSet.tempSpeakerObjects.indexOf(reg); 442 460 } … … 446 464 447 465 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 = ""; 452 489 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 } 470 495 }); 471 496 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({ 475 500 behavior: "smooth", 476 501 block: "nearest" … … 480 505 wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) }); 481 506 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 event507 wavesurfer.on('region-updated', handleRegionSnap); 483 508 484 509 let loader = document.createElement("span"); // loading audio element … … 490 515 if (inputFile.endsWith("csv")) { // diarization if csv 491 516 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 } 493 549 } else if (inputFile.endsWith("json")) { // transcription if json 494 550 itemType = "word"; … … 497 553 console.log("Filetype of " + inputFile + " not supported.") 498 554 } 555 499 556 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 502 560 }); 503 561 … … 512 570 const audioContainer = document.getElementById("audioContainer"); 513 571 const dualModeCheckbox = document.getElementById("dual-mode-checkbox"); 514 const wave form= document.getElementsByTagName("wave")[0];572 const wave = document.getElementsByTagName("wave")[0]; 515 573 const primaryCaret = document.getElementById("primary-caret"); 516 574 const secondaryCaret = document.getElementById("secondary-caret"); 517 575 const chapters = document.getElementById("chapters"); 576 const chaptersContainer = document.getElementById("chapters-container"); 518 577 const editPanel = document.getElementById("edit-panel"); 519 578 const chapterButton = document.getElementById("chapterButton"); 579 const chapterSearchInput = document.getElementById("chapter-search-input"); 520 580 const zoomOutButton = document.getElementById("zoomOutButton"); 521 581 const zoomSlider = document.getElementById("zoom-slider"); … … 524 584 const playPauseButton = document.getElementById("playPauseButton"); 525 585 const forwardButton = document.getElementById("forwardButton"); 526 const editButton = document.getElementById("edit Button");586 const editButton = document.getElementById("editorModeButton"); 527 587 const downloadButton = document.getElementById("downloadButton"); 528 588 const muteButton = document.getElementById("muteButton"); 529 589 const volumeSlider = document.getElementById("volume-slider"); 530 590 const fullscreenButton = document.getElementById("fullscreenButton"); 531 const speakerCheckbox = document.getElementById("change-all-checkbox");591 const changeAllCheckbox = document.getElementById("change-all-checkbox"); 532 592 const changeAllLabel = document.getElementById("change-all-label"); 533 593 const speakerInput = document.getElementById("speaker-input"); … … 541 601 const saveButton = document.getElementById("save-button"); 542 602 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"); 543 611 544 612 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); 547 619 primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id)); 548 620 secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id)); 549 621 chapters.style.height = "0px"; 622 chaptersContainer.style.height = "0px"; 550 623 editPanel.style.height = "0px"; 551 624 chapterButton.addEventListener("click", () => { toggleChapters() }); 625 chapterSearchInput.addEventListener("input", chapterSearchInputChange) 552 626 zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) }); 553 627 zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) }); … … 561 635 fullscreenButton.addEventListener("click", toggleFullscreen); 562 636 zoomSlider.style["accent-color"] = accentColour; 563 speakerCheckbox.addEventListener("change", speakerCheckboxChanged);637 changeAllCheckbox.addEventListener("change", () => { selectAllCheckboxChanged() }); 564 638 speakerInput.addEventListener("input", speakerChange); 565 639 speakerInput.addEventListener("blur", speakerInputUnfocused); … … 570 644 redoButton.addEventListener("click", redo); 571 645 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 572 649 document.querySelectorAll('input[type=number]').forEach(e => { 573 650 e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed 574 651 e.onblur = () => { prevUndoState = "" }; 575 652 }); 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; 579 895 if (e.target.tagName !== "INPUT") { 580 896 if (e.code === "Backspace" || e.code === "Delete") removeRegion(); … … 583 899 else if (e.code === "ArrowRight") wavesurfer.skipForward(); 584 900 } 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; 589 912 currSpeakerSet = primarySet; 913 if (!dualMode) removeCurrentRegion(); 590 914 reloadRegionsAndChapters(); 591 915 if (dualMode) { 916 dualModeMenuButton.classList.add('visible'); 592 917 if (!secondaryLoaded) { 593 918 loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet); … … 596 921 document.getElementById("caret-container").style.display = "flex"; 597 922 } else { 923 dualModeMenuButton.classList.remove('visible'); 924 caretClicked('primary-caret'); 598 925 document.getElementById("caret-container").style.display = "none"; 599 926 } 600 927 currSpeakerSet = primarySet; 928 drawVersionNames(); 929 if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dualModeChange", getCurrentRegionIndex()); 601 930 } 602 931 … … 605 934 606 935 function caretClicked(id) { 607 wavesurfer.clearRegions(); 608 // flashChapters(); 936 clearChapterSearch(); 609 937 if (id === "primary-caret") { 610 938 currSpeakerSet = primarySet; … … 614 942 swapCarets(false); 615 943 } 616 $(".region-top").remove();617 $(".region-bottom").remove();618 populateChapters(primarySet);619 populateChapters(secondarySet);620 944 } 621 945 622 946 function swapCarets(toPrimary) { 623 947 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 } 625 953 if (toPrimary) { 626 954 primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg"; … … 629 957 primaryCaret.src = interface_bootstrap_images + "caret-right.svg"; 630 958 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); 631 984 } 632 985 } … … 661 1014 wavesurfer.zoom(Number(this.value) / 4); 662 1015 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(); 671 1018 } 672 1019 let handles = document.getElementsByClassName("wavesurfer-handle"); … … 685 1032 let toggleChapters = function() { // show & hide chapter section 686 1033 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..."; 688 1037 } else { 689 1038 chapters.style.height = "0px"; 1039 chaptersContainer.style.height = "0px"; 1040 chapterSearchInput.placeholder = ""; 690 1041 } 691 1042 } … … 702 1053 703 1054 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, set705 1055 $.ajax({ 706 1056 type: "GET", … … 745 1095 // this colour scheme is designed for qualitative data 746 1096 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; 755 1107 data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time 1108 if (isSelectedSet || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population 756 1109 757 1110 for (let i = 0; i < data.tempSpeakerObjects.length; i++) { … … 759 1112 chapter.classList.add("chapter"); 760 1113 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; 762 1117 let speakerTime = document.createElement("span"); 763 1118 speakerTime.classList.add("speakerTime"); 764 1119 speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s"; 765 chapter. innerHTML = speakerName;1120 chapter.appendChild(speakerName); 766 1121 chapter.appendChild(speakerTime); 767 1122 chapter.addEventListener("click", chapterClicked); 768 chapter.addEventListener("mouse over", 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)) }); 769 1124 chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) }); 770 1125 … … 772 1127 let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end }; 773 1128 774 if (( dataIsSelected|| !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {1129 if ((isSelectedSet || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) { 775 1130 chapter.classList.add("selected-chapter"); 776 1131 selected = true; 777 1132 } 778 1133 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 } 780 1143 781 1144 let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region … … 788 1151 label: speakerName, 789 1152 }, 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, 791 1155 ...(selected) && {color: "rgba(255,50,50,0.5)"}, 792 1156 }); 793 1157 data.tempSpeakerObjects[i].region = associatedReg; 794 1158 } 1159 1160 let handles = document.getElementsByTagName('handle'); 1161 for (const handle of handles) handle.addEventListener('mousedown', () => mouseDown = true); 795 1162 796 1163 let regions = document.getElementsByTagName("region"); … … 801 1168 if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important"); 802 1169 else for (const reg of regions) reg.style.setProperty("z-index", "1", "important"); 1170 1171 chapterSearchInput.dispatchEvent(new Event("input")); 803 1172 } 804 1173 … … 838 1207 839 1208 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 } 843 1214 } 844 1215 … … 852 1223 let reg = currSpeakerSet.tempSpeakerObjects[idx].region; 853 1224 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); 856 1226 if (!isInCurrentRegions(reg)) { 857 1227 removeRegionBounds(); 858 drawRegionBounds(reg, wave form.scrollLeft, "black");1228 drawRegionBounds(reg, wave.scrollLeft, "black"); 859 1229 } 860 1230 } … … 865 1235 hoverSpeaker.innerHTML = ""; 866 1236 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 } 875 1240 } 876 1241 … … 888 1253 colour = "rgba(255, 50, 50, 0.5)"; 889 1254 } 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; 893 1256 } 894 1257 } 895 1258 896 1259 function regionEnter(region) { 1260 // console.log("regionEnter"); 897 1261 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)" }); 899 1263 } else { 900 1264 region.update({ color: "rgba(255, 255, 255, 0.35)" }); … … 908 1272 } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { 909 1273 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 }); 911 1275 } 912 1276 } else { … … 916 1280 917 1281 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-seconds919 920 1282 let date = new Date(null); 921 1283 date.setSeconds(num); 922 1284 return date.toTimeString().split(" ")[0].substring(3); 1285 } 1286 1287 function formatCursor(num) { 1288 cursorPos = num; 1289 return minutize(num); 923 1290 } 924 1291 … … 934 1301 935 1302 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 } 938 1326 } 939 1327 940 1328 function toggleEditPanel() { // show & hide edit panel 941 currentRegion.speaker = ''; 942 currentRegion.start = ''; 943 currentRegion.end = ''; 944 currentRegions = []; 945 removeRegionBounds(); 1329 removeCurrentRegion(); 946 1330 hoverSpeaker.innerHTML = ""; 947 1331 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 } 949 1335 editPanel.style.height = "30vh"; 950 1336 editPanel.style.padding = "1rem"; … … 965 1351 966 1352 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 988 1376 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)}); 994 1383 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 } 1001 1401 } 1002 1402 1003 1403 function updateRegionEditPanel() { // updates edit panel content/inputs 1404 // console.log('updating regionEditPanel') 1004 1405 if (currentRegion && currentRegion.speaker == "") { 1005 1406 removeButton.classList.add("disabled"); 1006 1407 speakerInput.classList.add("disabled"); 1007 speakerCheckbox.classList.add("disabled");1008 speakerCheckbox.disabled = true;1408 changeAllCheckbox.classList.add("disabled"); 1409 changeAllCheckbox.disabled = true; 1009 1410 disableStartEndInputs(); 1010 1411 speakerInput.readOnly = true; … … 1013 1414 removeButton.classList.remove("disabled"); 1014 1415 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; 1017 1418 enableStartEndInputs(); 1018 1419 speakerInput.readOnly = false; … … 1025 1426 saveButton.classList.add("disabled"); 1026 1427 } 1027 if ( speakerCheckbox.checked) {1428 if (changeAllCheckbox.checked) { 1028 1429 // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")"; 1029 1430 disableStartEndInputs(); … … 1034 1435 setInputInSeconds(endTimeInput, currentRegion.end); 1035 1436 } 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"); 1036 1441 } 1037 1442 1038 1443 function createNewRegion() { // adds a new region to the waveform 1444 clearChapterSearch(); 1039 1445 const speaker = "NEW_SPEAKER"; // default name 1040 let offset = 0;1041 1446 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 time1043 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 1045 1450 currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end}); 1451 1046 1452 editsMade = true; 1047 1453 currentRegions = []; 1048 1454 currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end}); 1049 1455 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]; 1057 1464 } 1058 1465 } … … 1067 1474 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { 1068 1475 if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { 1069 // if (!currentRegion.region) currentRegion.remove(); // remove from wavesurfer.regions.list1476 currSpeakerSet.tempSpeakerObjects[i].region.remove(); 1070 1477 currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects 1071 // else currentRegion.region.remove(); // remove if region was just added1072 1478 editsMade = true; 1073 1479 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) { 1075 1481 removeCurrentRegion(); 1076 reloadRegionsAndChapters();1077 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex);1078 return; // jump out of f or loop1482 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex); 1483 updateRegionEditPanel(); 1484 return; // jump out of function 1079 1485 } 1080 1486 } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) { 1487 currSpeakerSet.tempSpeakerObjects[i].region.remove(); 1081 1488 currSpeakerSet.tempSpeakerObjects.splice(i, 1); 1082 1489 if (i >= 0) i--; … … 1084 1491 } 1085 1492 removeCurrentRegion(); 1086 reloadRegionsAndChapters();1087 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed1493 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed 1494 updateRegionEditPanel(); 1088 1495 } else { console.log("no region selected") } 1089 1496 } … … 1091 1498 1092 1499 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; 1094 1501 return false; 1095 1502 } … … 1115 1522 if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i } 1116 1523 } 1117 // if (dualMode) {1118 // for (let i = 0; i < secondarySet.tempSpeakerObjects.length; i++) {1119 // if (isCurrentRegion(secondarySet.tempSpeakerObjects[i].region)) { return i }1120 // }1121 // }1122 1524 return -1; 1123 1525 } … … 1154 1556 function speakerChange() { // speaker input name onInput handler 1155 1557 const newSpeaker = speakerInput.value; 1558 clearChapterSearch(); 1156 1559 if (newSpeaker && newSpeaker != "") { 1157 speakerInput.style. border= "2px solid transparent";1560 speakerInput.style.outline = "2px solid transparent"; 1158 1561 if (getCurrentRegionIndex() != -1) { // if a region is selected 1562 const chaps = chapters.childNodes; 1159 1563 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 } 1163 1574 } 1164 speakerInput.value = "";1165 1575 currentRegion.speaker = newSpeaker; 1576 chapterLeave(getCurrentRegionIndex()); // update region bound text 1166 1577 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()); 1169 1579 } 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"; } 1171 1581 } 1172 1582 … … 1174 1584 prevUndoState = ""; 1175 1585 if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) { 1176 speakerInput.style. border= "2px solid firebrick";1586 speakerInput.style.outline = "2px solid firebrick"; 1177 1587 window.alert("Speaker input cannot be left empty. Please enter a speaker name."); 1178 1588 setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected 1179 } else speakerInput.style. border= "2px transparent";1180 } 1181 1182 function s peakerCheckboxChanged() { // "Change all" toggled1183 if ( speakerCheckbox.checked) {1589 } else speakerInput.style.outline = "2px transparent"; 1590 } 1591 1592 function selectAllCheckboxChanged(skipUndoState) { // "Change all" toggled 1593 if (changeAllCheckbox.checked) { 1184 1594 if (!isZooming) { 1185 1595 tempZoomSave = zoomSlider.value; … … 1193 1603 currentRegions = []; 1194 1604 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 } 1198 1610 } else { 1199 1611 if (!isZooming) { … … 1201 1613 } 1202 1614 currentRegions = []; // this will lose track of previously selected region*s* 1203 // changeAllLabel.innerHTML = "Change all";1204 1205 }1615 } 1616 reloadRegionsAndChapters(); 1617 if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "selectAllChange", getCurrentRegionIndex(), getCurrentRegionsIndexes()); 1206 1618 } 1207 1619 … … 1218 1630 function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination 1219 1631 isZooming = true; 1220 speakerCheckbox.disabled = true;1632 changeAllCheckbox.disabled = true; 1221 1633 let isOut = false; 1222 1634 if (dest == 0) isOut = true; … … 1230 1642 clearInterval(zoomInterval); 1231 1643 isZooming = false; 1232 speakerCheckbox.disabled = false;1644 changeAllCheckbox.disabled = false; 1233 1645 zoomSlider.dispatchEvent(new Event("input")); 1234 1646 } … … 1241 1653 clearInterval(zoomInterval); 1242 1654 isZooming = false; 1243 speakerCheckbox.disabled = false;1655 changeAllCheckbox.disabled = false; 1244 1656 zoomSlider.dispatchEvent(new Event("input")); 1245 1657 } … … 1278 1690 $(".region-top").remove(); 1279 1691 $(".region-bottom").remove(); 1692 $(".wavesurfer-region").remove(); 1280 1693 populateChapters(primarySet); 1281 1694 if (dualMode) { … … 1284 1697 } 1285 1698 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(); 1295 1702 } 1296 1703 if (currentRegions.length < 1) { … … 1302 1709 const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions 1303 1710 uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); 1304 // console.log(uniqueSelectedSpeakers); // CLG1305 1711 speakerInput.value = uniqueSelectedSpeakers.join(", "); 1306 1712 } … … 1311 1717 let newEnd = getTimeInSecondsFromInput(endTimeInput); 1312 1718 let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio 1313 1314 1719 if (getCurrentRegionIndex() != -1) { // if there is a selected region 1315 1720 if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward … … 1324 1729 currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart; 1325 1730 currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd; 1731 currSpeakerSet.tempSpeakerObjects[currRegIdx].region.update({start: newStart, end: newEnd}); 1326 1732 currentRegion.start = newStart; 1327 1733 currentRegion.end = newEnd; 1328 1734 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()); 1332 1737 } else { 1333 1738 console.log("no region selected"); … … 1349 1754 input.children[0].value = date.getHours() % 12; 1350 1755 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); 1353 1757 document.querySelectorAll('input[type=number]').forEach(e => { 1354 1758 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) 1360 1762 }); 1361 1763 } 1362 1764 1363 function addUndoState(state, secState, isSec, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack1765 function addUndoState(state, secState, isSec, dualMode, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack 1364 1766 let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references 1365 1767 let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references 1366 1768 undoButton.classList.remove("disabled"); 1367 1769 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}); 1369 1771 if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously 1370 1772 undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits) … … 1373 1775 prevUndoState = type; 1374 1776 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); 1376 1783 } 1377 1784 1378 1785 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(); 1380 1788 if (undoLevel - 1 < 0) console.log("ran out of undos"); 1381 1789 else { 1790 removeCurrentRegion(); 1382 1791 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 } 1386 1824 } 1387 1825 editsMade = true; 1388 1826 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 1416 1828 reloadRegionsAndChapters(); 1417 undoLevel--; // decrement undoLevel1829 localStorage.setItem('undoLevel', undoLevel); 1418 1830 if (undoLevel - 1 < 0) undoButton.classList.add("disabled"); 1419 1831 else undoButton.classList.remove("disabled"); … … 1424 1836 1425 1837 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"); 1428 1841 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 1436 1854 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 } 1447 1872 } 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 1456 1876 1457 1877 reloadRegionsAndChapters(); 1458 1878 undoLevel++; // increment undoLevel 1879 localStorage.setItem('undoLevel', undoLevel); 1459 1880 if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled"); 1460 1881 else redoButton.classList.remove("disabled"); … … 1466 1887 1467 1888 function resetUndoStates() { // clear undo history 1889 // console.log('resetUndoStates') 1468 1890 undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}]; 1469 1891 undoLevel = 0; 1892 localStorage.removeItem('undoLevel'); 1893 localStorage.removeItem('undoStates'); 1470 1894 undoButton.classList.add("disabled"); 1471 1895 redoButton.classList.add("disabled"); … … 1474 1898 function waveformScrolled() { // waveform scroll handler 1475 1899 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) { 1479 1908 let currIndexes = getCurrentRegionsIndexes(); 1909 if (getCurrentRegionIndex != 0) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick"); 1480 1910 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 1488 1917 const hoverSpeakerCanvas = document.createElement("canvas"); 1489 let colour = "black";1490 1918 hoverSpeakerCanvas.id = "hover-speaker-canvas"; 1491 1919 hoverSpeakerCanvas.classList.add("region-bounds"); … … 1495 1923 ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency 1496 1924 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) { 1498 1927 colour = "FireBrick"; 1499 1928 ctx.lineWidth = 3; … … 1525 1954 } 1526 1955 1527 function flashInput(valid) { // flashes background of input to show validity of input1528 if (valid) speakerInput.style.backgroundColor = "rgb(50,255,50)";1529 else speakerInput.style.backgroundColor = "rgb(255,50,50)";1530 setTimeout(() => { speakerInput.style.backgroundColor = "rgb(255,255,255)" }, 750);1531 }1532 1533 1956 function flashChapters() { 1534 1957 chapters.style.backgroundColor = "rgb(66, 84, 88)"; -
main/trunk/greenstone3/web/interfaces/default/style/core.css
r37068 r37287 1408 1408 #waveform { 1409 1409 background-color: white; 1410 position: relative; 1410 1411 } 1411 1412 … … 1444 1445 1445 1446 #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%; 1452 1450 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; 1456 1453 cursor: wait; 1457 user-select: none;1458 1454 } 1459 1455 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 } 1469 1459 1470 1460 .chapter { … … 1472 1462 border-style: solid; 1473 1463 border-width: 1px 0 0 0; 1474 border-top-right-radius: 5px;1464 /* border-top-right-radius: 5px; */ 1475 1465 border-top-left-radius: 5px; 1476 1466 padding: 0.5rem; 1477 1467 transition: 0.1s ease; 1478 1468 display: flex; 1469 flex-direction: row; 1470 flex-wrap: nowrap; 1471 justify-content: space-between; 1472 cursor: pointer; 1479 1473 } 1480 1474 1481 1475 .chapter:hover { 1482 1476 background-color: rgb(101, 116, 116); 1483 cursor: pointer; 1477 } 1478 1479 #chapter-alert { 1480 display: inline-block; 1481 margin: 1rem; 1484 1482 } 1485 1483 … … 1490 1488 .selected-chapter:hover { 1491 1489 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; */ 1492 1499 } 1493 1500 … … 1497 1504 } 1498 1505 1499 #downloadButton, #edit Button, #fullscreenButton {1506 #downloadButton, #editorModeButton, #fullscreenButton { 1500 1507 transform: scale(0.75); 1501 1508 padding-right: 0.75rem; … … 1540 1547 } 1541 1548 1542 .speakerTime {1543 float: right;1544 }1545 1546 1549 #zoom-slider { 1547 1550 width: 10rem; … … 1640 1643 .metadataTable td { 1641 1644 padding: 2px; 1642 <<<<<<< .mine1643 1645 } 1644 1646 1645 1647 /* edit functionality */ 1646 1648 #edit-panel { 1649 max-width: 70%; 1647 1650 width: 50%; 1648 1651 height: 0px; 1652 max-height: 30vh; 1649 1653 position: relative; 1650 1654 right: 0; 1651 max-height: 30vh;1652 font-size: 15px;1653 background-color: rgb(40, 54, 58);1654 1655 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; 1657 1658 box-sizing: border-box; /* ensures padding doesn't modify width */ 1658 1659 font-family: 'Courier New', monospace; 1659 1660 font-size: 0.85rem; 1660 1661 border-left: 1px solid rgb(24, 36, 39); 1662 user-select: none; 1661 1663 1662 1664 display: flex; … … 1664 1666 flex-wrap: nowrap; 1665 1667 justify-content: space-between; 1668 flex-grow: 1; 1666 1669 } 1667 1670 … … 1751 1754 #audio-dropdowns { 1752 1755 width: 100%; 1756 background-color: rgb(40, 54, 58); 1753 1757 display: flex; 1754 1758 flex-direction: row; 1755 1759 flex-wrap: nowrap; 1756 justify-content: space-between;1760 justify-content: flex-start; 1757 1761 } 1758 1762 … … 1801 1805 filter: grayscale(100%); 1802 1806 -webkit-filter: grayscale(100%); 1803 cursor: not-allowed !important; 1807 /* cursor: not-allowed !important; */ 1808 pointer-events: none; 1804 1809 } 1805 1810 … … 1821 1826 .region-top { 1822 1827 height: 30% !important; 1823 top: 1 0% !important;1828 top: 15% !important; 1824 1829 } 1825 1830 1826 1831 .region-bottom { 1827 1832 height: 30% !important; 1828 top: 60% !important;1833 top: 55% !important; 1829 1834 } 1830 1835 … … 1851 1856 } 1852 1857 1853 #selected-header {1858 .selected-header { 1854 1859 justify-content: space-between; 1855 1860 padding-top: 0.5rem; … … 1857 1862 } 1858 1863 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 1859 2007 #favouritesFullViewLink { 1860 2008 color: black; -
main/trunk/greenstone3/web/interfaces/default/style/map-editors.css
r33081 r37287 54 54 } 55 55 56 input[type=range]{56 .ControlPanel input[type=range]{ 57 57 height: 10px; 58 58 } -
main/trunk/greenstone3/web/interfaces/default/transform/pages/document.xsl
r37091 r37287 497 497 <xsl:choose> 498 498 <xsl:when test="@docType='simple'"> 499 <xsl:call-template name="wrapDocumentNodes"/>499 <xsl:call-template name="wrapDocumentNodes"/> 500 500 </xsl:when> 501 501 <xsl:otherwise>
Note:
See TracChangeset
for help on using the changeset viewer.