app/javascript/controllers/caption_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['contentStructure', 'ocrSettings', 'ocrAvailable', 'sttSettings', 'sttAvailable', 'runStt',
'manuallyCorrectedOcr', 'manuallyCorrectedStt', 'runOcr', 'ocrLanguages', 'ocrDropdown', 'runOcrDocumentNotes',
'runOcrImageNotes', 'runSttNotes', 'selectedLanguages', 'languageWarning', 'dropdownContent', 'ocrLanguageWrapper',
'usingFileManifest']
static values = { languages: Array }
connect () {
this.contentStructureChanged()
this.element.querySelector('form').reset()
}
ocrAvailable () {
return this.ocrContentTypes().indexOf(this.contentStructureTarget.value) >= 0 && this.ocrEnabled()
}
sttAvailable () {
return this.sttContentTypes().indexOf(this.contentStructureTarget.value) >= 0 && this.sttEnabled()
}
// list of content structures that are allowed to run OCR
ocrContentTypes () {
return ['simple_image', 'simple_book', 'document']
}
// list of content structures that are allowed to run speech to text
sttContentTypes () {
return ['media']
}
ocrEnabled () {
return this.data.get('ocr-enabled') === 'true'
}
sttEnabled () {
return this.data.get('stt-enabled') === 'true'
}
selectedContentStructure () {
return this.contentStructureTarget.options[this.contentStructureTarget.selectedIndex]
}
isDocument () {
return this.selectedContentStructure().value === 'document'
}
labelImagesManuallyCorrected () {
return 'Do the OCR files comply with accessibility standards? More info: <a target=_blank href="https://blog.adobe.com/en/publish/2016/03/08/correcting-ocr-errors">Correcting OCR</a>.'
}
labelDocumentsManuallyCorrected () {
return 'Do the PDF documents comply with accessibility standards? More info: <a target=_blank href="https://uit.stanford.edu/accessibility/guides/pdf">PDF Accessibility</a>.'
}
labelRunOcr () {
return `Do you want to auto-generate OCR files for the ${this.ocrFileTypeLabel()}?`
}
ocrFileTypeLabel () {
return this.isDocument() ? 'PDF documents' : 'images'
}
contentStructureChanged () {
// Hide the OCR and speech to text settings by default; we will show them if the content structure allows them to
this.ocrSettingsTarget.hidden = true
this.sttSettingsTarget.hidden = true
// if the user selects media as the content structure, file manifest is required, so don't bother showing the control
// we will automatically make it true on the server side
if (this.contentStructureTarget.value === 'media') {
this.usingFileManifestTarget.hidden = true
} else {
this.usingFileManifestTarget.hidden = false
}
if (this.ocrAvailable()) {
this.showOcrControls()
}
if (this.sttAvailable()) {
this.showSttControls()
}
}
// Show the OCR settings and controls
showOcrControls () {
this.ocrSettingsTarget.hidden = false
// Set the OCR settings label
this.runOcrTarget.querySelector('legend').innerHTML = this.labelRunOcr()
// Set specific OCR labels/options based on the content structure
if (this.isDocument()) { // documents have different labels and show different questions
this.manuallyCorrectedOcrTarget.querySelector('legend').innerHTML = this.labelDocumentsManuallyCorrected()
this.manuallyCorrectedOcrTarget.hidden = false
this.ocrAvailableTarget.hidden = true
this.manuallyCorrectedOcrChanged()
} else { // images and books have the same labels and show the same questions (different from documents)
this.manuallyCorrectedOcrTarget.querySelector('legend').innerHTML = this.labelImagesManuallyCorrected()
this.manuallyCorrectedOcrTarget.hidden = true
this.ocrAvailableTarget.hidden = false
this.ocrAvailableChanged()
}
// update notes/warnings and language selector based on the run OCR option
this.runOcrChanged()
}
// Show the Speech to text settings and controls
showSttControls () {
this.sttSettingsTarget.hidden = false
}
// if the user indicates they have speech to text available, show/hide the manually corrected and run stt options (for media)
sttAvailableChanged () {
const sttAvailable = this.sttAvailableTarget.querySelector('input[type="radio"]:checked').value === 'true'
this.manuallyCorrectedSttTarget.hidden = !sttAvailable
this.runSttTarget.hidden = sttAvailable
this.runSttChanged()
}
// if the user indicates they want to run SDR speech to text, show any relevant notes/warnings
runSttChanged () {
const runstt = this.runSttTarget.querySelector('input[type="radio"]:checked').value === 'true'
this.runSttNotesTarget.hidden = !runstt
}
// if the user indicates they have ocr available, show/hide the manually corrected and run OCR option (for images/books)
ocrAvailableChanged () {
const ocrAvailable = this.ocrAvailableTarget.querySelector('input[type="radio"]:checked').value === 'true'
this.manuallyCorrectedOcrTarget.hidden = !ocrAvailable
this.runOcrTarget.hidden = ocrAvailable
if (ocrAvailable) {
this.ocrLanguageWrapperTarget.classList.add('d-none')
} else {
this.runOcrChanged()
}
}
// if the user indicates they are providing OCR and have reviewed it, show/hide the run OCR option (for documents) and set the OCR available option
manuallyCorrectedOcrChanged () {
if (!this.isDocument()) return
const manuallyCorrectedOcr = this.manuallyCorrectedOcrTarget.querySelector('input[type="radio"]:checked').value === 'true'
this.ocrAvailableTarget.querySelector(`input[type=radio][value="${manuallyCorrectedOcr}"]`).checked = true
this.runOcrTarget.hidden = manuallyCorrectedOcr
}
// if the user indicates they want to run SDR OCR, show any relevant notes/warnings and language selector
runOcrChanged () {
// hide any notes to start, we will show as needed if OCR is selected
this.runOcrDocumentNotesTarget.hidden = true
this.runOcrImageNotesTarget.hidden = true
const runocr = this.runOcrTarget.querySelector('input[type="radio"]:checked').value === 'true'
if (runocr) {
this.runOcrDocumentNotesTarget.hidden = !this.isDocument()
this.runOcrImageNotesTarget.hidden = this.isDocument()
this.ocrLanguageWrapperTarget.classList.remove('d-none')
} else {
this.ocrLanguageWrapperTarget.classList.add('d-none')
}
Object.values(this.ocrDropdownTarget.children).forEach(child => {
child.disabled = !runocr
})
}
languageDropdown (event) {
const ishidden = Array.from(this.dropdownContentTarget.classList).includes('d-none')
this.dropdownContentTarget.classList.toggle('d-none')
this.ocrDropdownTarget.querySelector('#caret').innerHTML = `<i class="fa-solid fa-caret-${ishidden ? 'up' : 'down'}">`
event.preventDefault()
}
clickOutside (event) {
const isshown = !Array.from(this.dropdownContentTarget.classList).includes('d-none')
const incontainer = this.ocrLanguageWrapperTarget.contains(event.target)
const inselectedlangs = event.target.classList.contains('pill-close')
if (!incontainer && !inselectedlangs && isshown) {
this.languageDropdown(event)
}
}
languageUpdate (event) {
const target = event.target ? event.target : event
if (target.checked) {
this.languagesValue = this.languagesValue.concat([target.dataset])
} else {
this.languagesValue = this.languagesValue.filter(lang => lang.ocrValue !== target.value)
}
}
languagesValueChanged () {
if (this.languagesValue.length === 0) {
this.selectedLanguagesTarget.classList.add('d-none')
} else {
this.selectedLanguagesTarget.classList.remove('d-none')
this.selectedLanguagesTarget.innerHTML = `<div>Selected language(s)</div>
<ul class="list-unstyled border rounded mb-3 p-1 bg-white">${this.renderLanguagePills()}</ul>`
}
if (this.languagesValue.length > 8) {
this.languageWarningTarget.classList.remove('d-none')
} else {
this.languageWarningTarget.classList.add('d-none')
}
}
search (event) {
const searchterm = event.target.value.replace(/[^\w\s]/gi, '').toLowerCase()
this.dropdownContentTarget.classList.remove('d-none')
this.ocrLanguagesTargets.forEach(target => {
const compareterm = target.dataset.ocrLabel.replace(/[^\w\s]/gi, '').toLowerCase()
if (compareterm.includes(searchterm)) {
target.parentElement.classList.remove('d-none')
} else {
target.parentElement.classList.add('d-none')
}
})
}
deselect (event) {
event.preventDefault()
const target = this.ocrLanguagesTargets.find((language) => language.dataset.ocrValue === event.target.id)
if (target) target.checked = false
this.languageUpdate(target)
}
renderLanguagePills () {
return this.languagesValue.map((language) => {
return `
<li class="d-inline-flex gap-2 align-items-center my-2">
<span class="bg-light rounded-pill border language-pill">
<span class="language-label">
${language.ocrLabel}
</span>
<button data-action="${this.identifier}#deselect" id="${language.ocrValue}" type="button" class="btn-close py-0 pill-close" aria-label="Remove ${language.ocrLabel}"></button>
</span>
</li>
`
}).join('')
}
}