From ef5e3d9581c81d0858f87974454c1be8d5c2ba04 Mon Sep 17 00:00:00 2001 From: Josh Stegmaier <104993387+joshuastegmaier@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:22:37 -0400 Subject: [PATCH] Moved all javascript scripts on the asset detail page to their own files. (#2479) --- .eslintrc.yaml | 1 + concordia/static/js/src/asset-reservation.js | 8 + concordia/static/js/src/banner.js | 18 + concordia/static/js/src/contribute.js | 23 + concordia/static/js/src/guide.js | 86 +++ concordia/static/js/src/ocr.js | 7 + concordia/static/js/src/quick-tips.js | 38 ++ concordia/static/js/src/viewer-split.js | 146 +++++ concordia/static/js/src/viewer.js | 200 +++++++ .../transcriptions/asset_detail.html | 509 +----------------- 10 files changed, 545 insertions(+), 491 deletions(-) create mode 100644 concordia/static/js/src/banner.js create mode 100644 concordia/static/js/src/guide.js create mode 100644 concordia/static/js/src/ocr.js create mode 100644 concordia/static/js/src/quick-tips.js create mode 100644 concordia/static/js/src/viewer-split.js create mode 100644 concordia/static/js/src/viewer.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 989beb8c0..56959c746 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -15,6 +15,7 @@ rules: 'unicorn/prefer-query-selector': off # See https://github.com/sindresorhus/eslint-plugin-unicorn/issues/276 'unicorn/prefer-node-append': off 'unicorn/prefer-ternary': off + 'unicorn/no-lonely-if': off env: browser: true es2024: true diff --git a/concordia/static/js/src/asset-reservation.js b/concordia/static/js/src/asset-reservation.js index bb1ef4cfe..a10d837b3 100644 --- a/concordia/static/js/src/asset-reservation.js +++ b/concordia/static/js/src/asset-reservation.js @@ -1,6 +1,8 @@ /* global jQuery displayMessage displayHtmlMessage buildErrorMessage Sentry */ /* exported attemptToReserveAsset */ +const assetData = document.currentScript.dataset; + function attemptToReserveAsset(reservationURL, findANewPageURL, actionType) { var $transcriptionEditor = jQuery('#transcription-editor'); @@ -95,3 +97,9 @@ function attemptToReserveAsset(reservationURL, findANewPageURL, actionType) { } }); } + +jQuery(function () { + if (assetData.reserveForEditing) { + attemptToReserveAsset(assetData.reserveAssetUrl, '', 'transcribe'); + } +}); diff --git a/concordia/static/js/src/banner.js b/concordia/static/js/src/banner.js new file mode 100644 index 000000000..a46e3ea2a --- /dev/null +++ b/concordia/static/js/src/banner.js @@ -0,0 +1,18 @@ +/* global $ */ + +if (typeof Storage !== 'undefined') { + if (!(window.screen.width < 1024 || window.screen.height < 768)) { + for (var key in localStorage) { + if (key.startsWith('banner-')) { + if ($('#' + key).hasClass('alert')) { + $('#' + key).attr('hidden', true); + } + } + } + } +} + +$('#no-interface-banner').click(function (event) { + localStorage.setItem(event.target.parentElement.id, true); + $('#' + event.target.parentElement.id).attr('hidden', true); +}); diff --git a/concordia/static/js/src/contribute.js b/concordia/static/js/src/contribute.js index 30f9c8942..f477ef02e 100644 --- a/concordia/static/js/src/contribute.js +++ b/concordia/static/js/src/contribute.js @@ -766,4 +766,27 @@ function setupPage() { } } +let transcriptionForm = document.getElementById('transcription-editor'); +let ocrForm = document.getElementById('ocr-transcription-form'); + +let formChanged = false; +transcriptionForm.addEventListener('change', function () { + formChanged = true; +}); +transcriptionForm.addEventListener('submit', function () { + formChanged = false; +}); +if (ocrForm) { + ocrForm.addEventListener('submit', function () { + formChanged = false; + }); +} +window.addEventListener('beforeunload', function (event) { + if (formChanged) { + // Some browsers ignore this value and always display a built-in message instead + return (event.returnValue = + "The transcription you've started has not been saved."); + } +}); + setupPage(); diff --git a/concordia/static/js/src/guide.js b/concordia/static/js/src/guide.js new file mode 100644 index 000000000..2bfe28570 --- /dev/null +++ b/concordia/static/js/src/guide.js @@ -0,0 +1,86 @@ +/* global $ trackUIInteraction */ +/* exported openOffcanvas closeOffcanvas showPane hidePane */ + +function openOffcanvas() { + var guide = document.getElementById('guide-sidebar'); + guide.classList.remove('offscreen'); + guide.style.borderWidth = '0 0 thick thick'; + guide.style.borderStyle = 'solid'; + guide.style.borderColor = '#0076ad'; + document.getElementById('open-guide').style.display = 'none'; + document.addEventListener('keydown', function (event) { + if (event.key == 'Escape') { + closeOffcanvas(); + } + }); +} + +function closeOffcanvas() { + var guide = document.getElementById('guide-sidebar'); + guide.classList.add('offscreen'); + + guide.style.border = 'none'; + document.getElementById('open-guide').style.display = 'block'; +} + +function showPane(elementId) { + document.getElementById(elementId).classList.add('show', 'active'); + document.getElementById('guide-nav').classList.remove('show', 'active'); +} + +function hidePane(elementId) { + document.getElementById(elementId).classList.remove('show', 'active'); + document.getElementById('guide-nav').classList.add('show', 'active'); +} + +function trackHowToInteraction(element, label) { + trackUIInteraction(element, 'How To Guide', 'click', label); +} + +$('#open-guide').on('click', function () { + trackHowToInteraction($(this), 'Open'); +}); +$('#close-guide').on('click', function () { + trackHowToInteraction($(this), 'Close'); +}); +$('#previous-guide').on('click', function () { + trackHowToInteraction($(this), 'Back'); +}); +$('#next-guide').on('click', function () { + trackHowToInteraction($(this), 'Next'); +}); +$('#guide-bars').on('click', function () { + trackHowToInteraction($(this), 'Hamburger Menu'); +}); +$('#guide-sidebar .nav-link').on('click', function () { + let label = $(this).text().trim(); + trackHowToInteraction($(this), label); +}); + +$('#guide-carousel') + .carousel({ + interval: false, + wrap: false, + }) + .on('slide.bs.carousel', function (event) { + if (event.to == 0) { + $('#guide-bars').addClass('d-none'); + } else { + $('#guide-bars').removeClass('d-none'); + } + }); + +$('#previous-card').hide(); + +$('#card-carousel').on('slid.bs.carousel', function () { + if ($('#card-carousel .carousel-item:first').hasClass('active')) { + $('#previous-card').hide(); + $('#next-card').show(); + } else if ($('#card-carousel .carousel-item:last').hasClass('active')) { + $('#previous-card').show(); + $('#next-card').hide(); + } else { + $('#previous-card').show(); + $('#next-card').show(); + } +}); diff --git a/concordia/static/js/src/ocr.js b/concordia/static/js/src/ocr.js new file mode 100644 index 000000000..8c5cf0d07 --- /dev/null +++ b/concordia/static/js/src/ocr.js @@ -0,0 +1,7 @@ +/* global $ */ +/* exported selectLanguage */ + +function selectLanguage() { + $('#ocr-transcription-modal').modal('hide'); + $('#language-selection-modal').modal('show'); +} diff --git a/concordia/static/js/src/quick-tips.js b/concordia/static/js/src/quick-tips.js new file mode 100644 index 000000000..139eb3acf --- /dev/null +++ b/concordia/static/js/src/quick-tips.js @@ -0,0 +1,38 @@ +/* global $ trackUIInteraction setTutorialHeight */ + +function trackQuickTipsInteraction(element, label) { + trackUIInteraction(element, 'Quick Tips', 'click', label); +} + +var mainContentHeight = $('#contribute-main-content').height(); +if (mainContentHeight < 710) { + $('.sidebar').height(mainContentHeight - 130); +} + +$('#tutorial-popup').on('shown.bs.modal', function () { + setTutorialHeight(); +}); + +$('#quick-tips').on('click', function () { + trackQuickTipsInteraction($(this), 'Open'); +}); +$('#previous-card').on('click', function () { + trackQuickTipsInteraction($(this), 'Back'); +}); +$('#next-card').on('click', function () { + trackQuickTipsInteraction($(this), 'Next'); +}); +$('.carousel-indicators li').on('click', function () { + let index = [...this.parentElement.children].indexOf(this); + trackQuickTipsInteraction($(this), `Carousel ${index}`); +}); +$('#tutorial-popup').on('hidden.bs.modal', function () { + // We're tracking whenever the popup closes, so we don't separately track the close button being clicked + trackUIInteraction($(this), 'Quick Tips', 'click', 'Close'); +}); +$('#tutorial-popup').on('shown-on-load', function () { + // We set a timeout to make sure the analytics code is loaded before trying to track + setTimeout(function () { + trackUIInteraction($(this), 'Quick Tips', 'load', 'Open'); + }, 1000); +}); diff --git a/concordia/static/js/src/viewer-split.js b/concordia/static/js/src/viewer-split.js new file mode 100644 index 000000000..2769b7b49 --- /dev/null +++ b/concordia/static/js/src/viewer-split.js @@ -0,0 +1,146 @@ +/* global Split seadragonViewer */ + +let pageSplit; +let contributeContainer = document.getElementById('contribute-container'); +let ocrSection = document.getElementById('ocr-section'); +let editorColumn = document.getElementById('editor-column'); +let viewerColumn = document.getElementById('viewer-column'); +let layoutColumns = ['#viewer-column', '#editor-column']; +let verticalKey = 'transcription-split-sizes-vertical'; +let horizontalKey = 'transcription-split-sizes-horizontal'; + +let sizesVertical = localStorage.getItem(verticalKey); + +if (sizesVertical) { + sizesVertical = JSON.parse(sizesVertical); +} else { + sizesVertical = [50, 50]; +} + +let sizesHorizontal = localStorage.getItem(horizontalKey); + +if (sizesHorizontal) { + sizesHorizontal = JSON.parse(sizesHorizontal); +} else { + sizesHorizontal = [50, 50]; +} + +let splitDirection = localStorage.getItem('transcription-split-direction'); + +if (splitDirection) { + splitDirection = JSON.parse(splitDirection); +} else { + splitDirection = 'h'; +} + +function saveSizes(sizes) { + let sizeKey; + if (splitDirection == 'h') { + sizeKey = horizontalKey; + sizesHorizontal = sizes; + } else { + sizeKey = verticalKey; + sizesVertical = sizes; + } + localStorage.setItem(sizeKey, JSON.stringify(sizes)); +} + +function saveDirection(direction) { + localStorage.setItem( + 'transcription-split-direction', + JSON.stringify(direction), + ); +} + +function verticalSplit() { + splitDirection = 'v'; + saveDirection(splitDirection); + contributeContainer.classList.remove('flex-row'); + contributeContainer.classList.add('flex-column'); + viewerColumn.classList.remove('h-100'); + if (ocrSection != undefined) { + editorColumn.prepend(ocrSection); + } + + return Split(layoutColumns, { + sizes: sizesVertical, + minSize: 100, + gutterSize: 8, + direction: 'vertical', + elementStyle: function (dimension, size, gutterSize) { + return { + 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', + }; + }, + gutterStyle: function (dimension, gutterSize) { + return { + 'flex-basis': gutterSize + 'px', + }; + }, + onDragEnd: saveSizes, + }); +} +function horizontalSplit() { + splitDirection = 'h'; + saveDirection(splitDirection); + contributeContainer.classList.remove('flex-column'); + contributeContainer.classList.add('flex-row'); + viewerColumn.classList.add('h-100'); + if (ocrSection != undefined) { + viewerColumn.append(ocrSection); + } + return Split(layoutColumns, { + sizes: sizesHorizontal, + minSize: 100, + gutterSize: 8, + elementStyle: function (dimension, size, gutterSize) { + return { + 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', + }; + }, + gutterStyle: function (dimension, gutterSize) { + return { + 'flex-basis': gutterSize + 'px', + }; + }, + onDragEnd: saveSizes, + }); +} + +document + .getElementById('viewer-layout-horizontal') + .addEventListener('click', function () { + if (splitDirection != 'h') { + if (pageSplit != undefined) { + pageSplit.destroy(); + } + pageSplit = horizontalSplit(); + setTimeout(function () { + // Some quirk in the viewer makes this + // sometimes not work depending on + // the rotation, unless it's delayed. + // Less than 10ms didn't reliable work. + seadragonViewer.viewport.zoomTo(1); + }, 10); + } + }); + +document + .getElementById('viewer-layout-vertical') + .addEventListener('click', function () { + if (splitDirection != 'v') { + if (pageSplit != undefined) { + pageSplit.destroy(); + } + pageSplit = verticalSplit(); + setTimeout(function () { + seadragonViewer.viewport.zoomTo(1); + }, 10); + } + }); + +if (splitDirection == 'v') { + pageSplit = verticalSplit(); +} else { + pageSplit = horizontalSplit(); +} diff --git a/concordia/static/js/src/viewer.js b/concordia/static/js/src/viewer.js new file mode 100644 index 000000000..9ad073409 --- /dev/null +++ b/concordia/static/js/src/viewer.js @@ -0,0 +1,200 @@ +/* global OpenSeadragon screenfull debounce */ +/* exported seadragonView stepUp stepDown resetImageFilterForms */ + +const viewerData = document.currentScript.dataset; + +var seadragonViewer = OpenSeadragon({ + id: 'asset-image', + prefixUrl: viewerData.prefixUrl, + tileSources: { + type: 'image', + url: viewerData.tileSourceUrl, + }, + gestureSettingsTouch: { + pinchRotate: true, + }, + showNavigator: true, + showRotationControl: true, + showFlipControl: true, + toolbar: 'viewer-controls', + zoomInButton: 'viewer-zoom-in', + zoomOutButton: 'viewer-zoom-out', + homeButton: 'viewer-home', + rotateLeftButton: 'viewer-rotate-left', + rotateRightButton: 'viewer-rotate-right', + flipButton: 'viewer-flip', + crossOriginPolicy: 'Anonymous', +}); + +// We need to define our own fullscreen function rather than using OpenSeadragon's +// because the built-in fullscreen function overwrites the DOM with the viewer, +// breaking our extra controls, such as the image filters. +if (screenfull.isEnabled) { + let fullscreenButton = document.querySelector('#viewer-fullscreen'); + fullscreenButton.addEventListener('click', function (event) { + event.preventDefault(); + let targetElement = document.querySelector( + fullscreenButton.dataset.target, + ); + if (screenfull.isFullscreen) { + screenfull.exit(); + } else { + screenfull.request(targetElement); + } + }); +} + +// The buttons configured as controls for the viewer don't properly get focus +// when clicked. This mostly isn't a problem, but causes odd-looking behavior +// when one of the extra buttons in the control bar is clicked (and therefore +// focused) first--clicking the control button leaves the focus on the extra +// button. +// TODO: Attempting to add focus to the clicked button here doesn't consistently +// work for unknown reasons, so it just removes focus from the extra buttons +// for now +let viewerControlButtons = document.querySelectorAll('.viewer-control-button'); +for (const node of viewerControlButtons) { + node.addEventListener('click', function () { + let focusedButton = document.querySelector( + '.extra-control-button:focus', + ); + if (focusedButton) { + focusedButton.blur(); + } + }); +} + +/* + * Image filter handling + */ + +let availableFilters = [ + { + formId: 'gamma-form', + inputId: 'gamma', + getFilter: function () { + let value = document.getElementById(this.inputId).value; + if ( + !Number.isNaN(value) && + value != 1 && + value >= 0 && + value <= 5 + ) { + return OpenSeadragon.Filters.GAMMA(value); + } + }, + }, + { + formId: 'invert-form', + inputId: 'invert', + getFilter: function () { + let value = document.getElementById(this.inputId).checked; + if (value) { + return OpenSeadragon.Filters.INVERT(); + } + }, + }, + { + formId: 'threshold-form', + inputId: 'threshold', + getFilter: function () { + let value = document.getElementById(this.inputId).value; + if (!Number.isNaN(value) && value > 0 && value <= 255) { + return OpenSeadragon.Filters.THRESHOLDING(value); + } + }, + }, +]; + +function updateFilters() { + let filters = []; + for (const filterData of availableFilters) { + let filter = filterData.getFilter(); + if (filter) { + filters.push(filter); + } + } + + seadragonViewer.setFilterOptions({ + filters: { + processors: filters, + }, + }); +} + +for (const filterData of availableFilters) { + let form = document.getElementById(filterData.formId); + if (form) { + form.addEventListener('change', updateFilters); + form.addEventListener('reset', function () { + // We use setTimeout to push the updateFilters + // call to the next event cycle in order to + // call it after the form is reset, instead + // of before, which is when this listener + // triggers + setTimeout(updateFilters); + }); + } + + let input = document.getElementById(filterData.inputId); + if (input) { + // We use debounce here so that updateFilters is only called once, + // after the user stops typing or scrolling with their mousewheel + input.addEventListener( + 'keyup', + debounce(() => updateFilters()), + ); + input.addEventListener( + 'wheel', + debounce(() => updateFilters()), + ); + } +} + +/* + * Image filter form handling + */ +function stepUp(id) { + let input = document.getElementById(id); + input.stepUp(); + input.dispatchEvent(new Event('input', {bubbles: true})); + input.dispatchEvent(new Event('change', {bubbles: true})); + return false; +} + +function stepDown(id) { + let input = document.getElementById(id); + input.stepDown(); + input.dispatchEvent(new Event('input', {bubbles: true})); + input.dispatchEvent(new Event('change', {bubbles: true})); + return false; +} + +function resetImageFilterForms() { + for (const filterData of availableFilters) { + let form = document.getElementById(filterData.formId); + form.reset(); + } +} + +let gammaNumber = document.getElementById('gamma'); +let gammaRange = document.getElementById('gamma-range'); + +gammaNumber.addEventListener('input', function () { + gammaRange.value = gammaNumber.value; +}); + +gammaRange.addEventListener('input', function () { + gammaNumber.value = gammaRange.value; +}); + +let thresholdNumber = document.getElementById('threshold'); +let thresholdRange = document.getElementById('threshold-range'); + +thresholdNumber.addEventListener('input', function () { + thresholdRange.value = thresholdNumber.value; +}); + +thresholdRange.addEventListener('input', function () { + thresholdNumber.value = thresholdRange.value; +}); diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index ff8d17ac8..d33623d10 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -803,495 +803,22 @@
{{ card.display_heading }}
{% endblock main_content %} {% block body_scripts %} - - - - - - - - - - - - - - - - - - - + + + + + + + + + + {% endblock body_scripts %}