Raw File
tf.js
/*eslint-env jquery*/

/* mode: results or passage
 *
 */

const lastJobKey = 'tfLastJob'

const switchMode = m => {
  const mode = $('#mode')
  const pageNav = $('#navigation')
  const pages = $('#pages')
  const nresults = $('#nresults')
  const passagesNav = $('#passagesnav')
  const passages = $('#passages')
  const sectionsTable = $('#sectionsTable')
  const tuplesTable = $('#tuplesTable')
  const queryTable = $('#queryTable')
  const passageTable = $('#passageTable')
  const resultsc = $('#moderesults')
  const passagec = $('#modepassage')

  mode.val(m)
  if (m == 'passage') {
    pageNav.hide()
    pages.hide()
    nresults.hide()
    passages.show()
    passagesNav.show()
    sectionsTable.hide()
    tuplesTable.hide()
    queryTable.hide()
    passageTable.show()
    resultsc.show()
    passagec.hide()
  } else if (m == 'results') {
    pageNav.show()
    pages.show()
    nresults.show()
    passagesNav.hide()
    passages.hide()
    sectionsTable.show()
    tuplesTable.show()
    queryTable.show()
    passageTable.hide()
    resultsc.hide()
    passagec.show()
  }
}

const modes = () => {
  const mode = $('#mode')
  const m = mode.val()

  $('#moderesults').click(e => {
    e.preventDefault()
    storeForm()
    switchMode('results')
  })
  $('#modepassage').click(e => {
    e.preventDefault()
    storeForm()
    switchMode('passage')
  })
  ensureLoaded('passage', 'passages', m)
  if (mode.val() == 'results') {
    ensureLoaded('sections', null, m)
    ensureLoaded('tuples', null, m)
    ensureLoaded('query', 'pages', m)
  }
}

// switch to passage mode after clicking on a result

const switchPassage = () => {
  $('.pq').click(e => {
    e.preventDefault()
    const { currentTarget } = e
    const seq = $(currentTarget)
      .closest('details')
      .attr('seq')
    $('#mode').val('passages')
    $('#sec0').val($(currentTarget).attr('sec0'))
    $('#sec1').val($(currentTarget).attr('sec1'))
    $('#sec2').val($(currentTarget).attr('sec2'))
    $('#pos').val(seq)
    storeForm()
    getTable('passage', 'passages', 'passage')
  })
}

/* tables: getting tabular data from the server
 *
 */

const ensureLoaded = (kind, subkind, m) => {
  const table = $(`#${kind}Table`)
  if (!table.html()) {
    getTable(kind, subkind, m)
  } else {
    switchMode(m)
  }
}

const getTable = (kind, subkind, m, button) => {
  const url = `/${kind}`
  const destTable = $(`#${kind}Table`)
  const destMsg = $(`#${kind}Messages`)
  const destSub = $(`#${subkind}`)
  const nresults = $('.nresults')
  const go = document.querySelector('form')
  const formData = new FormData(go)
  if (button) {
    button.addClass('fa-spin')
  }
  if (kind == 'query') {
    nresults.html('...')
  }
  $.ajax({
    type: 'POST',
    url,
    data: formData,
    processData: false,
    contentType: false,
    success: data => {
      const { table = '', messages = '', nResults = 0 } = data
      destTable.html(table)
      if (kind == 'query') {
        nresults.html(nResults)
      }
      destMsg.html(messages)
      if (subkind != null) {
        const subs = data[subkind]
        if (subs) {
          destSub.html(subs)
          subLinks(kind, subkind)
          if (subkind == 'passages') {
            filterS0()
          }
        }
      }
      const { features } = data
      if (features != null) {
        $('#features').val(features)
      }
      if (button) {
        button.removeClass('fa-spin')
      }
      switchPassage()
      details(kind)
      sections()
      tuples()
      nodes()
      switchMode(m)
      storeForm()
      gotoFocus(kind)
    },
  })
}

const gotoFocus = kind => {
  if (kind == 'passage' || kind == 'query') {
    const rTarget = $(`#${kind}Table details.focus`)
    if (rTarget != null && rTarget[0] != null) {
      rTarget[0].scrollIntoView(false)
    }
  }
}

const activateTables = (kind, subkind) => {
  const button = $(`#${kind}Go`)
  const passButton = button.find('span')
  button.click(e => {
    e.preventDefault()
    storeForm()
    let m = $('#mode').val()
    if (kind == 'passage' && m != 'passage') {
      m = 'passage'
    } else if (kind != 'passage' && m != 'results') {
      m = 'results'
    }
    getTable(kind, subkind, m, passButton)
  })
  detailc(kind)
  const xpa = adjustOpened(kind)
  detailSet(kind, xpa)
}

// navigation links through passages and results

const subLinks = (kind, subkind) => {
  if (subkind == 'pages') {
    $('.pnav').click(e => {
      e.preventDefault()
      const { currentTarget } = e
      $('#pos').val($(currentTarget).html())
      storeForm()
      getTable(kind, subkind, 'results')
    })
  } else if (subkind == 'passages') {
    const opKey = `${kind}Op`
    $('.s0nav').click(e => {
      e.preventDefault()
      const { currentTarget } = e
      $('#sec0').val($(currentTarget).html())
      $('#sec1').val('1')
      $('#sec2').val('')
      $(`#${opKey}`).val('')
      storeForm()
      getTable(kind, subkind, 'passages')
    })
    $('.s1nav').click(e => {
      e.preventDefault()
      const { currentTarget } = e
      $('#sec1').val($(currentTarget).html())
      $('#sec2').val('')
      $(`#${opKey}`).val('')
      storeForm()
      getTable(kind, subkind, 'passages')
    })
  }
}

// controlling the "open all" checkbox

const detailc = kind => {
  $(`#${kind}Expac`).click(e => {
    e.preventDefault()
    const expa = $(`#${kind}Expa`)
    const xpa = expa.val()
    const newXpa =
      xpa == '1' ? '-1' : xpa == '-1' ? '1' : xpa == '0' ? '-1' : '-1'
    detailSet(kind, newXpa)
    const dPretty = $(`#${kind}Table details.pretty`)
    if (newXpa == '-1') {
      dPretty.each((i, elem) => {
        const el = $(elem)
        if (el.prop('open')) {
          el.prop('open', false)
        }
      })
    } else if (newXpa == '1') {
      dPretty.each((i, elem) => {
        const el = $(elem)
        if (!el.prop('open')) {
          el.prop('open', true)
        }
      })
    }
  })
}

const detailSet = (kind, xpa) => {
  const expac = $(`#${kind}Expac`)
  const expa = $(`#${kind}Expa`)
  const curVal = xpa == null ? expa.val() : xpa
  if (curVal == '-1') {
    expa.val('-1')
    expac.prop('checked', false)
    expac.prop('indeterminate', false)
  } else if (curVal == '1') {
    expa.val('1')
    expac.prop('checked', true)
    expac.prop('indeterminate', false)
  } else if (curVal == '0') {
    expa.val('0')
    expac.prop('checked', false)
    expac.prop('indeterminate', true)
  } else {
    expa.val('-1')
    expac.prop('checked', false)
    expac.prop('indeterminate', false)
  }
  const op = $(`#${kind}Op`)
  if (curVal == '-1') {
    op.val('')
  } else if (curVal == '1') {
    const dPretty = $(`#${kind}Table details.pretty`)
    const allNumbers = dPretty.map((i, elem) => $(elem).attr('seq')).get()
    op.val(allNumbers.join(','))
  }
  storeForm()
}

const adjustOpened = kind => {
  const openedElem = $(`#${kind}Op`)
  const dPretty = $(`#${kind}Table details.pretty`)
  const openedDetails = dPretty.filter((i, elem) => elem.open)
  const closedDetails = dPretty.filter((i, elem) => !elem.open)
  const openedNumbers = openedDetails
    .map((i, elem) => $(elem).attr('seq'))
    .get()
  const closedNumbers = closedDetails
    .map((i, elem) => $(elem).attr('seq'))
    .get()

  const currentOpenedStr = openedElem.val()
  const currentOpened =
    currentOpenedStr == '' ? [] : currentOpenedStr.split(',')
  const reduceOpened = currentOpened.filter(
    n => closedNumbers.indexOf(n) < 0 && openedNumbers.indexOf(n) < 0
  )
  const newOpened = reduceOpened.concat(openedNumbers)
  openedElem.val(newOpened.join(','))
  const nOpen = openedDetails.length
  const nClosed = closedDetails.length
  const xpa = nOpen == 0 ? '-1' : nClosed == 0 ? '1' : '0'
  return xpa
}

// opening and closing details in a table

const details = kind => {
  const details = $(`#${kind}Table details.pretty`)
  details.on('toggle', e => {
    const { currentTarget } = e
    const xpa = adjustOpened(kind)
    detailSet(kind, xpa)
    if (
      $(currentTarget).prop('open') &&
      !$(currentTarget)
        .find('div.pretty')
        .html()
    ) {
      getOpen(kind, $(currentTarget))
    }
  })
}

const getOpen = (kind, elem) => {
  const seq = elem.attr('seq')
  const url = `/${kind}/${seq}`
  const dest = elem.find('div.pretty')
  const go = document.querySelector('form')
  const formData = new FormData(go)
  $.ajax({
    type: 'POST',
    url,
    data: formData,
    processData: false,
    contentType: false,
    success: data => {
      const { table } = data
      dest.html(table)
    },
  })
}

/* auto submit data request after interacting with a control
 *
 */

const reactive = () => {
  const form = $('form')
  $('.r').change(() => {
    form.submit()
  })
  $('textarea').change(() => {
    storeForm()
  })
  $('input').change(() => {
    storeForm()
  })
}

const cradios = () => {
  $('.cradio').change(() => {
    $('#cond').prop('checked', true)
  })
}

/* controlling the filtering of the section list
 *
 */


const filterS0 = () => {
  const filterControl = $('#s0filter')
  const filterTotal = $('#s0total')
  const s0items = $('.s0nav')
  const total = s0items.length

  const applyFilter = filterValueRaw => {
    const filterValue = filterValueRaw.toLowerCase()
    let s = 0
    s0items.each((i, elem) => {
      const el = $(elem)
      const s0 = el.html().toLowerCase()
      if (filterValue == '' || s0.indexOf(filterValue) >= 0) {
        el.show()
        s += 1
      }
      else {
        el.hide()
      }
    })
    const totalRep = total == s
    ? `${total}`
    : `${s} of ${total}`
    filterTotal.html(totalRep)
  }

  filterControl.on('input', e => {
    e.preventDefault()
    const filterValue = e.target.value.toLowerCase()
    applyFilter(filterValue)
    storeForm()
  })

  applyFilter(filterControl.val())
}

/* controlling the side bar
 *
 */

const sidebar = () => {
  const side = $('#side')
  const sideStr = side.val()
  const parts = new Set(sideStr ? sideStr.split(',') : [])
  const headers = $('#sidebar div').filter((i, elem) => $(elem).attr('status') != 'about')
  const bodies = $('#sidebarcont div').filter((i, elem) => $(elem).attr('status') != 'about')
  headers.each((i, elem) => {
    const el = $(elem)
    const part = el.attr('status')
    if (parts.has(part)) {
      el.addClass('active')
    }
    else {
      el.removeClass('active')
    }
  })
  bodies.each((i, elem) => {
    const el = $(elem)
    const part = el.attr('status')
    if (parts.has(part)) {
      el.addClass('active')
    }
    else {
      el.removeClass('active')
    }
  })
  $('#sidebar a').click(e => {
    e.preventDefault()
    const { currentTarget } = e
    const header = $(currentTarget).closest('div')
    const part = header.attr('status')
    const side = $('#side')
    const sideStr = side.val()
    const parts = new Set(sideStr ? sideStr.split(',') : [])
    const body = $(`#sidebarcont div[status="${part}"]`)
    const isActive = header.hasClass('active')
    if (isActive) {
      header.removeClass('active')
      body.removeClass('active')
      parts.delete(part)
      side.val('')
    } else {
      header.addClass('active')
      body.addClass('active')
      parts.add(part)
    }
    side.val(Array.from(parts).join(','))
    storeForm()
  })
}

const help = () => {
  const help = $('#help')
  const expandedStr = help.val()
  const helpOpened = expandedStr == '' ? [] : expandedStr.split(',')
  for (const helpId of helpOpened) {
    const helpDetails = $(`#${helpId}`)
    helpDetails.prop('open', true)
  }
  $('details.help').on('toggle', e => {
    const { currentTarget } = e
    const dHelp = $('details.help')
    const op = $('#help')
    const thisHelp = $(currentTarget)
    const thisId = thisHelp.attr('id')
    const thisOpen = thisHelp.prop('open')
    const expandedDetails = dHelp
      .filter((i, elem) => $(elem).prop('open') && $(elem).attr('id') != thisId)
      .map((i, elem) => $(elem).attr('id'))
      .get()
    if (thisOpen) {
      expandedDetails.push(thisId)
    }
    op.val(expandedDetails.join(','))
  })
}

/* controlling the textarea pads
 * Clicking on certain elements in the table rows will
 * populate the pads
 */

const sections = () => {
  const secs = $('#sections')
  $('.rwh').click(e => {
    e.preventDefault()
    e.stopPropagation()
    const { currentTarget } = e
    const sec = $(currentTarget).attr('sec')
    const orig = secs.val()
    secs.val(`${orig}\n${sec}`)
  })
}
const tuples = () => {
  const tups = $('#tuples')
  $('.sq').click(e => {
    e.preventDefault()
    e.stopPropagation()
    const { currentTarget } = e
    const tup = $(currentTarget).attr('tup')
    const orig = tups.val()
    tups.val(`${orig}\n${tup}`)
  })
}
const nodes = () => {
  const tups = $('#tuples')
  $('.nd').click(e => {
    e.preventDefault()
    e.stopPropagation()
    const { currentTarget } = e
    const nd = $(currentTarget).html()
    const orig = tups.val()
    tups.val(`${orig}\n${nd}`)
  })
}

/* job control
 *
 */

const verifyApp = (jobContent, app) => {
  const { appName } = jobContent
  if (appName == app) {
    return true
  }
  if (confirm(`Change app "${appName}" to "${app}" ?`)) {
    jobContent['appName'] = app
    return true
  }
  return false
}

const suggestName = jobName => {
  const jobs = getJobs()
  let newName = jobName
  const resolved = s => s != '' && s != jobName && !jobs.has(s)
  let cancelled = false
  while (!resolved(newName) && !cancelled) {
    while (!resolved(newName)) {
      newName += 'N'
    }
    const answer = prompt(
      'New job name:',
      newName
    )
    if (answer == null) {
      cancelled = true
    }
    else {
      newName = answer
    }
  }
  return cancelled
    ? null
    : newName
}

const jobOptions = () => {
  const jChange = $('#jchange')
  const jobh = $('#jobh')
  const currentJob = jobh.val()
  let html = ''
  for (const job of getJobs()) {
    const selected = job == currentJob ? ' selected' : ''
    html += `<option value="${job}"${selected}>${job}</option>`
    jChange.html(html)
  }
}

const jobControls = () => {
  const jChange = $('#jchange')
  const jClear = $('#jclear')
  const jDelete = $('#jdelete')
  const jRename = $('#jrename')
  const jNew = $('#jnew')
  const jOpen = $('#jopen')
  const jFileDiv = $('#jfilediv')
  const jFile = $('#jfile')
  const aName = $('#appName')

  const form = $('form')
  const jobh = $('#jobh')
  const side = $('#side')
  const jobPart = 'jobs'

  jFileDiv.hide()

  jChange.change(e => {
    const oldJob = jobh.val()
    const newJob = e.target.value
    if (oldJob == newJob) {
      return
    }
    storeForm()
    setLastJob($('#appName').val(), newJob)
    jobh.val(e.target.value)
    readForm()
    side.val(jobPart)
    form.submit()
  })

  jClear.click(() => {
    clearForm()
    storeForm()
    setLastJob($('#appName').val(), $('#jobh').val())
  })

  jDelete.click(() => {
    setLastJob($('#appName').val(), '')
    deleteForm()
    jobh.val('')
    clearForm()
  })

  jRename.click(e => {
    const jobName = jobh.val()
    const newName = suggestName(jobName)
    if (newName == null) {
      e.preventDefault()
      return
    }
    deleteForm()
    jobh.val(newName)
    storeForm()
    setLastJob($('#appName').val(), $('#jobh').val())
  })

  jOpen.click(() => {
    jFileDiv.show()
  })
  jFile.change(() => {
    const jobFile = jFile.prop('files')[0]
    const reader = new FileReader()
    reader.onload = e => {
      const jobContent = JSON.parse(e.target.result)
      if (!verifyApp(jobContent, aName.val())) {
        e.preventDefault()
        return
      }
      const newName = suggestName(jobContent.jobName)
      if (newName == null) {
        e.preventDefault()
        return
      }
      storeForm()
      jobContent['jobName'] = newName
      readForm(jobContent)
      side.val(jobPart)
      setLastJob($('#appName').val(), $('#jobh').val())
      form.submit()
    }
    reader.readAsText(jobFile)
  })

  jNew.click(e => {
    const jobName = jobh.val()
    const newName = suggestName(jobName)
    if (newName == null) {
      e.preventDefault()
      return
    }
    storeForm()
    clearForm()
    jobh.val(newName)
    setLastJob($('#appName').val(), $('#jobh').val())
    storeForm()
  })
}

// managing form data on local storage

/*
const inspectJob = (appName, jobName) => {
  const formKey = `tf/${appName}/${jobName}`
  const formStr = localStorage.getItem(formKey)
  const formObj = JSON.parse(formStr)
  console.warn('inspect', formObj)
}
*/

const readForm = jobContent => {
  let formObj
  if (jobContent == null) {
    const go = document.querySelector('form')
    const formData = new FormData(go)
    const appName = formData.get('appName')
    const jobName = formData.get('jobName')
    const formKey = `tf/${appName}/${jobName}`
    const formStr = localStorage.getItem(formKey)
    formObj = JSON.parse(formStr)
  }
  else {
    formObj = jobContent
  }
  for (const [key, value] of Object.entries(formObj)) {
    $(`[name="${key}"]`).val(value)
  }
}

const clearForm = () => {
  const aName = $('#appName')
  const jobh = $('#jobh')
  const side = $('#side')

  const appName = aName.val()
  const jobName = jobh.val()
  const jobPart = 'jobs'

  $(`[name]`).val('')

  aName.val(appName)
  jobh.val(jobName)
  side.val(jobPart)
}

const deleteForm = () => {
  const go = document.querySelector('form')
  const formData = new FormData(go)
  const appName = formData.get('appName')
  const jobName = formData.get('jobName')
  const formKey = `tf/${appName}/${jobName}`
  localStorage.removeItem(formKey)
}

const storeForm = () => {
  const go = document.querySelector('form')
  const formData = new FormData(go)
  const formObj = {}
  for (const [key, value] of formData) {
    formObj[key] = value
  }
  const formStr = JSON.stringify(formObj)
  const appName = formData.get('appName')
  const jobName = formData.get('jobName')
  const formKey = `tf/${appName}/${jobName}`
  localStorage.setItem(formKey, formStr)
}

const getJobs = () => {
  const go = document.querySelector('form')
  const formData = new FormData(go)
  const appName = formData.get('appName')
  const tfPrefix = `tf/${appName}/`
  const tfLength = tfPrefix.length
  return new Set(
    Object.keys(localStorage)
    .filter(key => key.startsWith(tfPrefix))
    .map(key => key.substring(tfLength))
  )
}

const setLastJob = (appName, jobName) => {
  const lastJob = localStorage.getItem(lastJobKey)
  const lastJobData = lastJob
    ? JSON.parse(lastJob)
    : {}
  lastJobData[appName] = jobName
  localStorage.setItem(lastJobKey, JSON.stringify(lastJobData))
}

const getLastJob = appName => {
  const lastJob = localStorage.getItem(lastJobKey)
  const lastJobData = lastJob
    ? JSON.parse(lastJob)
    : {}
  const { [appName]: lastJobName } = lastJobData
  return (lastJobName == null)
    ? 'default'
    : lastJobName
}

const initForm = () => {
  const loadJob = $('#jobl')
  if (loadJob.val() == '1') {
    loadJob.val('')
    const appName = $('#appName').val()
    const lastJobName = getLastJob(appName)
    const jobContent = localStorage.getItem(`tf/${appName}/${lastJobName}`)
    if (jobContent) {
      readForm(JSON.parse(jobContent))
      $('form').submit()
    }
  }
  else {
    storeForm()
  }
}

/* main
 *
 */

$(() => {
  initForm()
  sidebar()
  modes()
  activateTables('sections', null)
  activateTables('tuples', null)
  activateTables('query', 'pages')
  cradios()
  help()
  reactive()
  jobOptions()
  jobControls()
})
back to top