michielbdejong/solid-panes

View on GitHub
src/outline/manager.js

Summary

Maintainability
F
1 mo
Test Coverage
/* -*- coding: utf-8-dos -*-
   Outline Mode Manager
*/
var panes = require('pane-registry')
const $rdf = require('rdflib')

var YAHOO = require('./dragDrop.js')
var outlineIcons = require('./outlineIcons.js')
var UserInput = require('./userInput.js')
var UI = require('solid-ui')
var queryByExample = require('./queryByExample.js')

/* global alert XPathResult sourceWidget */
// XPathResult?

// const iconHeight = '24px'

module.exports = function (context) {
  const dom = context.dom

  this.document = context.dom
  this.outlineIcons = outlineIcons
  this.labeller = this.labeller || {}
  this.labeller.LanguagePreference = '' // for now
  var outline = this // Kenny: do we need this?
  var thisOutline = this
  var selection = []
  this.selection = selection
  this.ancestor = UI.utils.ancestor // make available as outline.ancestor in callbacks
  this.sparql = UI.rdf.UpdateManager
  this.kb = UI.store
  var kb = UI.store
  var sf = UI.store.fetcher
  dom.outline = this
  this.qs = new queryByExample.QuerySource() // Track queries in queryByExample

  // var selection = []  // Array of statements which have been selected
  // this.focusTd // the <td> that is being observed
  this.UserInput = new UserInput(this)
  this.clipboardAddress = 'tabulator:clipboard' // Weird
  this.UserInput.clipboardInit(this.clipboardAddress)
  var outlineElement = this.outlineElement

  this.init = function () {
    var table = getOutlineContainer()
    table.outline = this
  }

  /** benchmark a function **/
  benchmark.lastkbsize = 0

  function benchmark (f) {
    var args = []
    for (var i = arguments.length - 1; i > 0; i--) args[i - 1] = arguments[i]
    // UI.log.debug('BENCHMARK: args=' + args.join());
    var begin = new Date().getTime()
    var returnValue = f.apply(f, args)
    var end = new Date().getTime()
    UI.log.info(
      'BENCHMARK: kb delta: ' +
        (kb.statements.length - benchmark.lastkbsize) +
        ', time elapsed for ' +
        f +
        ' was ' +
        (end - begin) +
        'ms'
    )
    benchmark.lastkbsize = kb.statements.length
    return returnValue
  } // benchmark

  // / ////////////////////// Representing data

  //  Represent an object in summary form as a table cell

  function appendRemoveIcon (node, subject, removeNode) {
    var image = UI.utils.AJARImage(
      outlineIcons.src.icon_remove_node,
      'remove',
      undefined,
      dom
    )
    image.addEventListener('click', removeNodeIconMouseDownListener)
    // image.setAttribute('align', 'right')  Causes icon to be moved down
    image.node = removeNode
    image.setAttribute('about', subject.toNT())
    image.style.marginLeft = '5px'
    image.style.marginRight = '10px'
    // image.style.border='solid #777 1px';
    node.appendChild(image)
    return image
  }

  this.appendAccessIcons = function (kb, node, obj) {
    if (obj.termType !== 'NamedNode') return
    var uris = kb.uris(obj)
    uris.sort()
    var last = null
    for (var i = 0; i < uris.length; i++) {
      if (uris[i] === last) continue
      last = uris[i]
      thisOutline.appendAccessIcon(node, last)
    }
  }

  this.appendAccessIcon = function (node, uri) {
    if (!uri) return ''
    var docuri = UI.rdf.uri.docpart(uri)
    if (docuri.slice(0, 5) !== 'http:') return ''
    var state = sf.getState(docuri)
    var icon, alt, listener
    switch (state) {
      case 'unrequested':
        icon = outlineIcons.src.icon_unrequested
        alt = 'fetch'
        listener = unrequestedIconMouseDownListener
        break
      case 'requested':
        icon = outlineIcons.src.icon_requested
        alt = 'fetching'
        listener = failedIconMouseDownListener // new: can retry yello blob
        break
      case 'fetched':
        icon = outlineIcons.src.icon_fetched
        listener = fetchedIconMouseDownListener
        alt = 'loaded'
        break
      case 'failed':
        icon = outlineIcons.src.icon_failed
        alt = 'failed'
        listener = failedIconMouseDownListener
        break
      case 'unpermitted':
        icon = outlineIcons.src.icon_failed
        listener = failedIconMouseDownListener
        alt = 'no perm'
        break
      case 'unfetchable':
        icon = outlineIcons.src.icon_failed
        listener = failedIconMouseDownListener
        alt = 'cannot fetch'
        break
      default:
        UI.log.error('?? state = ' + state)
        break
    } // switch
    var img = UI.utils.AJARImage(
      icon,
      alt,
      outlineIcons.tooltips[icon].replace(/[Tt]his resource/, docuri),
      dom
    )
    img.setAttribute('uri', uri)
    img.addEventListener('click', listener) // @@ seemed to be missing 2017-08
    addButtonCallbacks(img, docuri)
    node.appendChild(img)
    return img
  } // appendAccessIcon

  // Six different Creative Commons Licenses:
  // 1. http://creativecommons.org/licenses/by-nc-nd/3.0/
  // 2. http://creativecommons.org/licenses/by-nc-sa/3.0/
  // 3. http://creativecommons.org/licenses/by-nc/3.0/
  // 4. http://creativecommons.org/licenses/by-nd/3.0/
  // 5. http://creativecommons.org/licenses/by-sa/3.0/
  // 6. http://creativecommons.org/licenses/by/3.0/

  /** make the td for an object (grammatical object)
   *  @param obj - an RDF term
   *  @param view - a VIEW function (rather than a bool asImage)
   **/

  this.outlineObjectTD = function outlineObjectTD (
    obj,
    view,
    deleteNode,
    statement
  ) {
    var td = dom.createElement('td')
    td.setAttribute(
      'style',
      'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
    )
    td.setAttribute('notSelectable', 'false')
    var theClass = 'obj'

    // check the IPR on the data.  Ok if there is any checked license which is one the document has.
    if (statement && statement.why) {
      if (UI.licenceOptions && UI.licenceOptions.checkLicence()) {
        theClass += ' licOkay' // flag as light green etc .licOkay {background-color: #dfd}
      }
    }

    // set about and put 'expand' icon
    if (
      obj.termType === 'NamedNode' ||
      obj.termType === 'BlankNode' ||
      (obj.termType === 'Literal' &&
        obj.value.slice &&
        (obj.value.slice(0, 6) === 'ftp://' ||
          obj.value.slice(0, 8) === 'https://' ||
          obj.value.slice(0, 7) === 'http://'))
    ) {
      td.setAttribute('about', obj.toNT())
      td.appendChild(
        UI.utils.AJARImage(
          UI.icons.originalIconBase + 'tbl-expand-trans.png',
          'expand',
          undefined,
          dom
        )
      ).addEventListener('click', expandMouseDownListener)
    }
    td.setAttribute('class', theClass) // this is how you find an object
    // @@ TAKE CSS OUT OF STYLE SHEET
    if (kb.whether(obj, UI.ns.rdf('type'), UI.ns.link('Request'))) {
      td.className = 'undetermined'
    } // @@? why-timbl

    if (!view) {
      // view should be a function pointer
      view = viewAsBoringDefault
    }
    td.appendChild(view(obj))
    if (deleteNode) {
      appendRemoveIcon(td, obj, deleteNode)
    }

    try {
      // new YAHOO.util.DDExternalProxy(td)
    } catch (e) {
      UI.log.error('YAHOO Drag and drop not supported:\n' + e)
    }

    // set DOM methods
    td.tabulatorSelect = function () {
      setSelected(this, true)
    }
    td.tabulatorDeselect = function () {
      setSelected(this, false)
    }
    // td.appendChild( iconBox.construct(document.createTextNode('bla')) );

    // Create an inquiry icon if there is proof about this triple
    if (statement) {
      var oneStatementFormula = new UI.rdf.IndexedFormula()
      oneStatementFormula.statements.push(statement) // st.asFormula()
      // The following works because Formula.hashString works fine for
      // one statement formula
      var reasons = kb.each(
        oneStatementFormula,
        kb.sym('http://dig.csail.mit.edu/TAMI/2007/amord/tms#justification')
      )
      if (reasons.length) {
        var inquirySpan = dom.createElement('span')
        if (reasons.length > 1) {
          inquirySpan.innerHTML = ' &times; ' + reasons.length
        }
        inquirySpan.setAttribute('class', 'inquiry')
        inquirySpan.insertBefore(
          UI.utils.AJARImage(
            outlineIcons.src.icon_display_reasons,
            'explain',
            undefined,
            dom
          ),
          inquirySpan.firstChild
        )
        td.appendChild(inquirySpan)
      }
    }
    td.addEventListener('click', selectableTDClickListener)
    return td
  } // outlineObjectTD

  this.outlinePredicateTD = function outlinePredicateTD (
    predicate,
    newTr,
    inverse,
    internal
  ) {
    var predicateTD = dom.createElement('TD')
    predicateTD.setAttribute('about', predicate.toNT())
    predicateTD.setAttribute('class', internal ? 'pred internal' : 'pred')

    let lab
    switch (predicate.termType) {
      case 'BlankNode': // TBD
        predicateTD.className = 'undetermined'
        break
      case 'NamedNode':
        lab = UI.utils.predicateLabelForXML(predicate, inverse)
        break
      case 'Collection': // some choices of predicate
        lab = UI.utils.predicateLabelForXML(predicate.elements[0], inverse)
    }
    lab = lab.slice(0, 1).toUpperCase() + lab.slice(1)
    // if (kb.statementsMatching(predicate,rdf('type'), UI.ns.link('Request')).length) predicateTD.className='undetermined';

    var labelTD = dom.createElement('TD')
    labelTD.setAttribute(
      'style',
      'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
    )
    labelTD.setAttribute('notSelectable', 'true')
    labelTD.appendChild(dom.createTextNode(lab))
    predicateTD.appendChild(labelTD)
    labelTD.style.width = '100%'
    predicateTD.appendChild(termWidget.construct(dom)) // termWidget is global???
    for (var w in outlineIcons.termWidgets) {
      if (!newTr || !newTr.AJAR_statement) break // case for TBD as predicate
      // alert(Icon.termWidgets[w]+'   '+Icon.termWidgets[w].filter)
      if (
        outlineIcons.termWidgets[w].filter &&
        outlineIcons.termWidgets[w].filter(
          newTr.AJAR_statement,
          'pred',
          inverse
        )
      ) {
        termWidget.addIcon(predicateTD, outlineIcons.termWidgets[w])
      }
    }

    try {
      // new YAHOO.util.DDExternalProxy(predicateTD)
    } catch (e) {
      UI.log.error('drag and drop not supported')
    }
    // set DOM methods
    predicateTD.tabulatorSelect = function () {
      setSelected(this, true)
    }
    predicateTD.tabulatorDeselect = function () {
      setSelected(this, false)
    }
    predicateTD.addEventListener('click', selectableTDClickListener)
    return predicateTD
  } // outlinePredicateTD

  /**
   * Render Tabbed set of home app panes
   *
   * @param {Object} [options] A set of options you can provide
   * @param {string} [options.selectedTab] To open a specific dashboard pane
   * @param {Function} [options.onClose] If given, will present an X for the dashboard, and call this method when clicked
   * @returns Promise<{Element}> - the div that holds the dashboard
   */
  async function globalAppTabs (options = {}) {
    console.log('globalAppTabs @@')
    const div = dom.createElement('div')
    const me = UI.authn.currentUser()
    if (!me) {
      alert('Must be logged in for this')
      throw new Error('Not logged in')
    }
    const items = await getDashboardItems()

    function renderTab (div, item) {
      div.dataset.globalPaneName = item.tabName || item.paneName
      div.textContent = item.label
    }

    function renderMain (containerDiv, item) {
      // Items are pane names
      const pane = panes.byName(item.paneName) // 20190701
      containerDiv.innerHTML = ''
      const table = containerDiv.appendChild(dom.createElement('table'))
      const me = UI.authn.currentUser()
      thisOutline.GotoSubject(
        item.subject || me,
        true,
        pane,
        false,
        undefined,
        table
      )
    }

    div.appendChild(
      UI.tabs.tabWidget({
        dom,
        subject: me,
        items,
        renderMain,
        renderTab,
        ordered: true,
        orientation: 0,
        backgroundColor: '#eeeeee', // black?
        selectedTab: options.selectedTab,
        onClose: options.onClose
      })
    )
    return div
  }
  this.getDashboard = globalAppTabs

  async function getDashboardItems () {
    const me = UI.authn.currentUser()
    const div = dom.createElement('div')
    const [books, pods] = await Promise.all([getAddressBooks(), getPods()])
    return [
      {
        paneName: 'home',
        label: 'Your stuff',
        icon: UI.icons.iconBase + 'noun_547570.svg'
      },
      {
        paneName: 'basicPreferences',
        label: 'Preferences',
        icon: UI.icons.iconBase + 'noun_Sliders_341315_00000.svg'
      },
      {
        paneName: 'editProfile',
        label: 'Edit your profile',
        icon: UI.icons.iconBase + 'noun_492246.svg'
      }
    ]
      .concat(books)
      .concat(pods)

    async function getPods () {
      try {
        // need to make sure that profile is loaded
        await kb.fetcher.load(me.doc())
      } catch (err) {
        console.error('Unable to load profile', err)
        return []
      }
      const pods = kb.each(me, ns.space('storage'), null, me.doc())
      return pods.map((pod, index) => {
        const label =
          pods.length > 1 ? pod.uri.split('//')[1].slice(0, -1) : 'Your storage'
        return {
          paneName: 'folder',
          tabName: `folder-${index}`,
          label,
          subject: pod,
          icon: UI.icons.iconBase + 'noun_Cabinet_251723.svg'
        }
      })
    }

    async function getAddressBooks () {
      try {
        const context = await UI.authn.findAppInstances(
          { me, div, dom },
          ns.vcard('AddressBook')
        )
        return (context.instances || []).map((book, index) => ({
          paneName: 'contact',
          tabName: `contact-${index}`,
          label: 'Contacts',
          subject: book,
          icon: UI.icons.iconBase + 'noun_15695.svg'
        }))
      } catch (err) {
        console.error('oops in globalAppTabs AddressBook')
      }
      return []
    }
  }
  this.getDashboardItems = getDashboardItems

  /**
   * Call this method to show the global dashboard.
   *
   * @param {Object} [options] A set of options that can be passed
   * @param {string} [options.pane] To open a specific dashboard pane
   * @returns {Promise<void>}
   */
  async function showDashboard (options = {}) {
    const dashboardContainer = getDashboardContainer()
    const outlineContainer = getOutlineContainer()
    // reuse dashboard if already children already is inserted
    if (dashboardContainer.childNodes.length > 0 && options.pane) {
      outlineContainer.style.display = 'none'
      dashboardContainer.style.display = 'inherit'
      const tab = dashboardContainer.querySelector(
        `[data-global-pane-name="${options.pane}"]`
      )
      if (tab) {
        tab.click()
        return
      }
      console.warn(
        'Did not find the referred tab in global dashboard, will open first one'
      )
    }

    // create a new dashboard if not already present
    const dashboard = await globalAppTabs({
      selectedTab: options.pane,
      onClose: closeDashboard
    })

    // close the dashboard if user log out
    UI.authn.solidAuthClient.trackSession(closeDashboardIfLoggedOut)

    // finally - switch to showing dashboard
    outlineContainer.style.display = 'none'
    dashboardContainer.appendChild(dashboard)

    function closeDashboard () {
      dashboardContainer.style.display = 'none'
      outlineContainer.style.display = 'inherit'
    }

    function closeDashboardIfLoggedOut (session) {
      if (session) {
        return
      }
      closeDashboard()
    }
  }
  this.showDashboard = showDashboard

  function getDashboardContainer () {
    return getOrCreateContainer('GlobalDashboard')
  }

  function getOutlineContainer () {
    return getOrCreateContainer('outline')
  }

  /**
   * Get element with id or create a new on the fly with that id
   *
   * @param {string} id The ID of the element you want to get or create
   * @returns {HTMLElement}
   */
  function getOrCreateContainer (id) {
    return (
      document.getElementById(id) ||
      (() => {
        const dashboardContainer = document.createElement('div')
        dashboardContainer.id = id
        const mainContainer =
          document.querySelector('[role="main"]') || document.body
        return mainContainer.appendChild(dashboardContainer)
      })()
    )
  }

  async function getRelevantPanes (subject, context) {
    const panes = context.session.paneRegistry
    const relevantPanes = panes.list.filter(
      pane => pane.label(subject, context) && !pane.global
    )
    if (relevantPanes.length === 0) {
      // there are no relevant panes, simply return default pane (which ironically is internalPane)
      return [panes.byName('internal')]
    }
    const filteredPanes = await UI.authn.filterAvailablePanes(relevantPanes)
    if (filteredPanes.length === 0) {
      // if no relevant panes are available panes because of user role, we still allow for the most relevant pane to be viewed
      return [relevantPanes[0]]
    }
    const firstRelevantPaneIndex = panes.list.indexOf(relevantPanes[0])
    const firstFilteredPaneIndex = panes.list.indexOf(filteredPanes[0])
    // if the first relevant pane is loaded before the panes available wrt role, we still want to offer the most relevant pane
    return firstRelevantPaneIndex < firstFilteredPaneIndex
      ? [relevantPanes[0]].concat(filteredPanes)
      : filteredPanes
  }

  function getPane (relevantPanes, subject) {
    return (
      relevantPanes.find(
        pane => pane.shouldGetFocus && pane.shouldGetFocus(subject)
      ) || relevantPanes[0]
    )
  }

  async function expandedHeaderTR (subject, requiredPane, options) {
    async function renderPaneIconTray (td, options = {}) {
      const paneShownStyle =
        'width: 24px; border-radius: 0.5em; border-top: solid #222 1px; border-left: solid #222 0.1em; border-bottom: solid #eee 0.1em; border-right: solid #eee 0.1em; margin-left: 1em; padding: 3px; background-color:   #ffd;'
      const paneHiddenStyle =
        'width: 24px; border-radius: 0.5em; margin-left: 1em; padding: 3px'
      const paneIconTray = td.appendChild(dom.createElement('nav'))
      paneIconTray.style =
        'display:flex; justify-content: flex-start; align-items: center;'

      const relevantPanes = options.hideList
        ? []
        : await getRelevantPanes(subject, context)
      tr.firstPane = requiredPane || getPane(relevantPanes, subject)
      const paneNumber = relevantPanes.indexOf(tr.firstPane)

      if (relevantPanes.length !== 1) {
        // if only one, simplify interface
        relevantPanes.forEach((pane, index) => {
          const label = pane.label(subject, context)
          const ico = UI.utils.AJARImage(pane.icon, label, label, dom)
          ico.style = pane === tr.firstPane ? paneShownStyle : paneHiddenStyle // init to something at least
          // ico.setAttribute('align','right');   @@ Should be better, but ffox bug pushes them down
          // ico.style.width = iconHeight
          // ico.style.height = iconHeight
          var listen = function (ico, pane) {
            // Freeze scope for event time
            ico.addEventListener(
              'click',
              function (event) {
                // Find the containing table for this subject
                for (var t = td; t.parentNode; t = t.parentNode) {
                  if (t.nodeName === 'TABLE') break
                }
                if (t.nodeName !== 'TABLE') {
                  throw new Error('outline: internal error.')
                }
                var removePanes = function (specific) {
                  for (var d = t.firstChild; d; d = d.nextSibling) {
                    if (typeof d.pane !== 'undefined') {
                      if (!specific || d.pane === specific) {
                        if (d.paneButton) {
                          d.paneButton.setAttribute('class', 'paneHidden')
                          d.paneButton.style = paneHiddenStyle
                        }
                        removeAndRefresh(d)
                        // If we just delete the node d, ffox doesn't refresh the display properly.
                        // state = 'paneHidden';
                        if (
                          d.pane.requireQueryButton &&
                          t.parentNode.className /* outer table */ &&
                          numberOfPanesRequiringQueryButton === 1 &&
                          dom.getElementById('queryButton')
                        ) {
                          dom
                            .getElementById('queryButton')
                            .setAttribute('style', 'display:none;')
                        }
                      }
                    }
                  }
                }
                var renderPane = function (pane) {
                  var paneDiv
                  UI.log.info('outline: Rendering pane (2): ' + pane.name)
                  if (UI.no_catch_pane_errors) {
                    // for debugging
                    paneDiv = pane.render(subject, context, options)
                  } else {
                    try {
                      paneDiv = pane.render(subject, context, options)
                    } catch (e) {
                      // Easier debugging for pane developers
                      paneDiv = dom.createElement('div')
                      paneDiv.setAttribute('class', 'exceptionPane')
                      var pre = dom.createElement('pre')
                      paneDiv.appendChild(pre)
                      pre.appendChild(
                        dom.createTextNode(UI.utils.stackString(e))
                      )
                    }
                  }
                  if (
                    pane.requireQueryButton &&
                    dom.getElementById('queryButton')
                  ) {
                    dom.getElementById('queryButton').removeAttribute('style')
                  }
                  var second = t.firstChild.nextSibling
                  var row = dom.createElement('tr')
                  var cell = row.appendChild(dom.createElement('td'))
                  cell.appendChild(paneDiv)
                  if (second) t.insertBefore(row, second)
                  else t.appendChild(row)
                  row.pane = pane
                  row.paneButton = ico
                }
                var state = ico.getAttribute('class')
                if (state === 'paneHidden') {
                  if (!event.shiftKey) {
                    // shift means multiple select
                    removePanes()
                  }
                  renderPane(pane)
                  ico.setAttribute('class', 'paneShown')
                  ico.style = paneShownStyle
                } else {
                  removePanes(pane)
                  ico.setAttribute('class', 'paneHidden')
                  ico.style = paneHiddenStyle
                }

                var numberOfPanesRequiringQueryButton = 0
                for (var d = t.firstChild; d; d = d.nextSibling) {
                  if (d.pane && d.pane.requireQueryButton) {
                    numberOfPanesRequiringQueryButton++
                  }
                }
              },
              false
            )
          } // listen

          listen(ico, pane)
          ico.setAttribute(
            'class',
            index !== paneNumber ? 'paneHidden' : 'paneShown'
          )
          if (index === paneNumber) tr.paneButton = ico
          paneIconTray.appendChild(ico)
        })
      }
      return paneIconTray
    } // renderPaneIconTray

    // Body of expandedHeaderTR
    var tr = dom.createElement('tr')
    if (options.hover) {
      // By default no hide till hover as community deems it confusing
      tr.setAttribute('class', 'hoverControl')
    }
    var td = tr.appendChild(dom.createElement('td'))
    td.setAttribute(
      'style',
      'margin: 0.2em; border: none; padding: 0; vertical-align: top;' +
        'display:flex; justify-content: space-between; flex-direction: row;'
    )
    td.setAttribute('notSelectable', 'true')
    td.setAttribute('about', subject.toNT())
    td.setAttribute('colspan', '2')

    // Stuff at the right about the subject
    const header = td.appendChild(dom.createElement('div'))
    header.style =
      'display:flex; justify-content: flex-start; align-items: center; flex-wrap: wrap;'

    const showHeader = !!requiredPane

    if (!options.solo && !showHeader) {
      var icon = header.appendChild(
        UI.utils.AJARImage(
          UI.icons.originalIconBase + 'tbl-collapse.png',
          'collapse',
          undefined,
          dom
        )
      )
      icon.addEventListener('click', collapseMouseDownListener)

      var strong = header.appendChild(dom.createElement('h1'))
      strong.appendChild(dom.createTextNode(UI.utils.label(subject)))
      strong.style =
        'font-size: 150%; margin: 0 0.6em 0 0; padding: 0.1em 0.4em;'
      UI.widgets.makeDraggable(strong, subject)
    }

    header.appendChild(
      await renderPaneIconTray(td, {
        hideList: showHeader
      })
    )

    // set DOM methods
    tr.firstChild.tabulatorSelect = function () {
      setSelected(this, true)
    }
    tr.firstChild.tabulatorDeselect = function () {
      setSelected(this, false)
    }
    return tr
  } // expandedHeaderTR

  // / //////////////////////////////////////////////////////////////////////////

  /*  PANES
   **
   **     Panes are regions of the outline view in which a particular subject is
   ** displayed in a particular way.  They are like views but views are for query results.
   ** subject panes are currently stacked vertically.
   */

  // / ////////////////////  Specific panes are in panes/*.js
  //
  // The defaultPane is the first one registered for which the label method exists
  // Those registered first take priority as a default pane.
  // That is, those earlier in this file

  /**
   * Pane registration
   */

  // the second argument indicates whether the query button is required

  // / ///////////////////////////////////////////////////////////////////////////

  // Remove a node from the DOM so that Firefox refreshes the screen OK
  // Just deleting it cause whitespace to accumulate.
  function removeAndRefresh (d) {
    var table = d.parentNode
    var par = table.parentNode
    var placeholder = dom.createElement('table')
    placeholder.setAttribute('style', 'width: 100%;')
    par.replaceChild(placeholder, table)
    table.removeChild(d)
    par.replaceChild(table, placeholder) // Attempt to
  }

  var propertyTable = (this.propertyTable = function propertyTable (
    subject,
    table,
    pane,
    options
  ) {
    UI.log.debug('Property table for: ' + subject)
    subject = kb.canon(subject)
    // if (!pane) pane = panes.defaultPane;

    if (!table) {
      // Create a new property table
      table = dom.createElement('table')
      table.setAttribute('style', 'width: 100%;')
      expandedHeaderTR(subject, pane, options).then(tr1 => {
        table.appendChild(tr1)

        if (tr1.firstPane) {
          var paneDiv
          try {
            UI.log.info('outline: Rendering pane (1): ' + tr1.firstPane.name)
            paneDiv = tr1.firstPane.render(subject, context, options)
          } catch (e) {
            // Easier debugging for pane developers
            paneDiv = dom.createElement('div')
            paneDiv.setAttribute('class', 'exceptionPane')
            var pre = dom.createElement('pre')
            paneDiv.appendChild(pre)
            pre.appendChild(dom.createTextNode(UI.utils.stackString(e)))
          }

          var row = dom.createElement('tr')
          var cell = row.appendChild(dom.createElement('td'))
          cell.appendChild(paneDiv)
          if (
            tr1.firstPane.requireQueryButton &&
            dom.getElementById('queryButton')
          ) {
            dom.getElementById('queryButton').removeAttribute('style')
          }
          table.appendChild(row)
          row.pane = tr1.firstPane
          row.paneButton = tr1.paneButton
        }
      })

      return table
    } else {
      // New display of existing table, keeping expanded bits
      UI.log.info('Re-expand: ' + table)
      // do some other stuff here
      return table
    }
  }) /* propertyTable */

  function propertyTR (doc, st, inverse) {
    var tr = doc.createElement('TR')
    tr.AJAR_statement = st
    tr.AJAR_inverse = inverse
    // tr.AJAR_variable = null; // @@ ??  was just 'tr.AJAR_variable'
    tr.setAttribute('predTR', 'true')
    var predicateTD = thisOutline.outlinePredicateTD(st.predicate, tr, inverse)
    tr.appendChild(predicateTD) // @@ add 'internal' to predicateTD's class for style? mno
    return tr
  }
  this.propertyTR = propertyTR

  // / ////////// Property list
  function appendPropertyTRs (parent, plist, inverse, predicateFilter) {
    // UI.log.info('@appendPropertyTRs, 'this' is %s, dom is %s, '+ // Gives 'can't access dead object'
    //                   'thisOutline.document is %s', this, dom.location, thisOutline.document.location);
    // UI.log.info('@appendPropertyTRs, dom is now ' + this.document.location);
    // UI.log.info('@appendPropertyTRs, dom is now ' + thisOutline.document.location);
    UI.log.debug('Property list length = ' + plist.length)
    if (plist.length === 0) return ''
    var sel, j, k
    if (inverse) {
      sel = function (x) {
        return x.subject
      }
      plist = plist.sort(UI.utils.RDFComparePredicateSubject)
    } else {
      sel = function (x) {
        return x.object
      }
      plist = plist.sort(UI.utils.RDFComparePredicateObject)
    }

    var max = plist.length
    for (j = 0; j < max; j++) {
      // squishing together equivalent properties I think
      var s = plist[j]
      //      if (s.object == parentSubject) continue; // that we knew

      // Avoid predicates from other panes
      if (predicateFilter && !predicateFilter(s.predicate, inverse)) continue

      var tr = propertyTR(dom, s, inverse)
      parent.appendChild(tr)
      var predicateTD = tr.firstChild // we need to kludge the rowspan later

      var defaultpropview = views.defaults[s.predicate.uri]

      //   LANGUAGE PREFERENCES WAS AVAILABLE WITH FF EXTENSION - get from elsewhere?

      var dups = 0 // How many rows have the same predicate, -1?
      var langTagged = 0 // how many objects have language tags?
      var myLang = 0 // Is there one I like?

      for (
        k = 0;
        k + j < max && plist[j + k].predicate.sameTerm(s.predicate);
        k++
      ) {
        if (k > 0 && sel(plist[j + k]).sameTerm(sel(plist[j + k - 1]))) dups++
        if (sel(plist[j + k]).lang && outline.labeller.LanguagePreference) {
          langTagged += 1
          if (
            sel(plist[j + k]).lang.indexOf(
              outline.labeller.LanguagePreference
            ) >= 0
          ) {
            myLang++
          }
        }
      }

      /* Display only the one in the preferred language
          ONLY in the case (currently) when all the values are tagged.
          Then we treat them as alternatives. */

      if (myLang > 0 && langTagged === dups + 1) {
        for (let k = j; k <= j + dups; k++) {
          if (
            outline.labeller.LanguagePreference &&
            sel(plist[k]).lang.indexOf(outline.labeller.LanguagePreference) >= 0
          ) {
            tr.appendChild(
              thisOutline.outlineObjectTD(
                sel(plist[k]),
                defaultpropview,
                undefined,
                s
              )
            )
            break
          }
        }
        j += dups // extra push
        continue
      }

      tr.appendChild(
        thisOutline.outlineObjectTD(sel(s), defaultpropview, undefined, s)
      )

      /* Note: showNobj shows between n to 2n objects.
       * This is to prevent the case where you have a long list of objects
       * shown, and dangling at the end is '1 more' (which is easily ignored)
       * Therefore more objects are shown than hidden.
       */

      tr.showNobj = function (n) {
        var predDups = k - dups
        var show = 2 * n < predDups ? n : predDups
        var showLaterArray = []
        if (predDups !== 1) {
          predicateTD.setAttribute(
            'rowspan',
            show === predDups ? predDups : n + 1
          )
          var l
          if (show < predDups && show === 1) {
            // what case is this...
            predicateTD.setAttribute('rowspan', 2)
          }
          var displayed = 0 // The number of cells generated-1,
          // all duplicate thing removed
          for (l = 1; l < k; l++) {
            // This detects the same things
            if (
              !kb
                .canon(sel(plist[j + l]))
                .sameTerm(kb.canon(sel(plist[j + l - 1])))
            ) {
              displayed++
              s = plist[j + l]
              defaultpropview = views.defaults[s.predicate.uri]
              var trObj = dom.createElement('tr')
              trObj.style.colspan = '1'
              trObj.appendChild(
                thisOutline.outlineObjectTD(
                  sel(plist[j + l]),
                  defaultpropview,
                  undefined,
                  s
                )
              )
              trObj.AJAR_statement = s
              trObj.AJAR_inverse = inverse
              parent.appendChild(trObj)
              if (displayed >= show) {
                trObj.style.display = 'none'
                showLaterArray.push(trObj)
              }
            } else {
              // ToDo: show all the data sources of this statement
              UI.log.info('there are duplicates here: %s', plist[j + l - 1])
            }
          }
          // @@a quick fix on the messing problem.
          if (show === predDups) {
            predicateTD.setAttribute('rowspan', displayed + 1)
          }
        } // end of if (predDups!==1)

        if (show < predDups) {
          // Add the x more <TR> here
          var moreTR = dom.createElement('tr')
          var moreTD = moreTR.appendChild(dom.createElement('td'))
          moreTD.setAttribute(
            'style',
            'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
          )
          moreTD.setAttribute('notSelectable', 'false')
          if (predDups > n) {
            // what is this for??
            var small = dom.createElement('a')
            moreTD.appendChild(small)

            var predToggle = (function (f) {
              return f(predicateTD, k, dups, n)
            })(function (predicateTD, k, dups, n) {
              return function (display) {
                small.innerHTML = ''
                if (display === 'none') {
                  small.appendChild(
                    UI.utils.AJARImage(
                      UI.icons.originalIconBase + 'tbl-more-trans.png',
                      'more',
                      'See all',
                      dom
                    )
                  )
                  small.appendChild(
                    dom.createTextNode(predDups - n + ' more...')
                  )
                  predicateTD.setAttribute('rowspan', n + 1)
                } else {
                  small.appendChild(
                    UI.utils.AJARImage(
                      UI.icons.originalIconBase + 'tbl-shrink.png',
                      '(less)',
                      undefined,
                      dom
                    )
                  )
                  predicateTD.setAttribute('rowspan', predDups + 1)
                }
                for (var i = 0; i < showLaterArray.length; i++) {
                  var trObj = showLaterArray[i]
                  trObj.style.display = display
                }
              }
            }) // ???
            var current = 'none'
            var toggleObj = function (event) {
              predToggle(current)
              current = current === 'none' ? '' : 'none'
              if (event) event.stopPropagation()
              return false // what is this for?
            }
            toggleObj()
            small.addEventListener('click', toggleObj, false)
          } // if(predDups>n)
          parent.appendChild(moreTR)
        } // if
      } // tr.showNobj

      tr.showAllobj = function () {
        tr.showNobj(k - dups)
      }

      tr.showNobj(10)

      j += k - 1 // extra push
    }
  } //  appendPropertyTRs

  this.appendPropertyTRs = appendPropertyTRs

  /*   termWidget
   **
   */
  var termWidget = {} // @@@@@@ global
  global.termWidget = termWidget
  termWidget.construct = function (dom) {
    dom = dom || document
    var td = dom.createElement('TD')
    td.setAttribute(
      'style',
      'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
    )
    td.setAttribute('class', 'iconTD')
    td.setAttribute('notSelectable', 'true')
    td.style.width = '0px'
    return td
  }
  termWidget.addIcon = function (td, icon, listener) {
    var iconTD = td.childNodes[1]
    if (!iconTD) return
    var width = iconTD.style.width
    var img = UI.utils.AJARImage(icon.src, icon.alt, icon.tooltip, dom)
    width = parseInt(width)
    width = width + icon.width
    iconTD.style.width = width + 'px'
    iconTD.appendChild(img)
    if (listener) {
      img.addEventListener('click', listener)
    }
  }
  termWidget.removeIcon = function (td, icon) {
    var iconTD = td.childNodes[1]
    var baseURI
    if (!iconTD) return
    var width = iconTD.style.width
    width = parseInt(width)
    width = width - icon.width
    iconTD.style.width = width + 'px'
    for (var x = 0; x < iconTD.childNodes.length; x++) {
      var elt = iconTD.childNodes[x]
      var eltSrc = elt.src

      // ignore first '?' and everything after it //Kenny doesn't know what this is for
      try {
        baseURI = dom.location.href.split('?')[0]
      } catch (e) {
        console.log(e)
        baseURI = ''
      }
      var relativeIconSrc = UI.rdf.uri.join(icon.src, baseURI)
      if (eltSrc === relativeIconSrc) {
        iconTD.removeChild(elt)
      }
    }
  }
  termWidget.replaceIcon = function (td, oldIcon, newIcon, listener) {
    termWidget.removeIcon(td, oldIcon)
    termWidget.addIcon(td, newIcon, listener)
  }

  // / /////////////////////////////////////////////////// VALUE BROWSER VIEW

  // / /////////////////////////////////////////////////////// TABLE VIEW

  //  Summarize a thing as a table cell

  /**********************

    query global vars

  ***********************/

  // const doesn't work in Opera
  // const BLANK_QUERY = { pat: kb.formula(), vars: [], orderBy: [] };
  // @ pat: the query pattern in an RDFIndexedFormula. Statements are in pat.statements
  // @ vars: the free variables in the query
  // @ orderBy: the variables to order the table

  function QueryObj () {
    this.pat = kb.formula()
    this.vars = []
    // this.orderBy = []
  }

  var queries = []
  queries[0] = new QueryObj()
  /*
  function querySave () {
    queries.push(queries[0])
    var choices = dom.getElementById('queryChoices')
    var next = dom.createElement('option')
    var box = dom.createElement('input')
    var index = queries.length - 1
    box.setAttribute('type', 'checkBox')
    box.setAttribute('value', index)
    choices.appendChild(box)
    choices.appendChild(dom.createTextNode('Saved query #' + index))
    choices.appendChild(dom.createElement('br'))
    next.setAttribute('value', index)
    next.appendChild(dom.createTextNode('Saved query #' + index))
    dom.getElementById('queryJump').appendChild(next)
  }
*/
  /*
  function resetQuery () {
    function resetOutliner (pat) {
      var n = pat.statements.length
      var pattern, tr
      for (let i = 0; i < n; i++) {
        pattern = pat.statements[i]
        tr = pattern.tr
        // UI.log.debug('tr: ' + tr.AJAR_statement);
        if (typeof tr !== 'undefined') {
          delete tr.AJAR_pattern
          delete tr.AJAR_variable
        }
      }
      for (let x in pat.optional) { resetOutliner(pat.optional[x]) }
    }
    resetOutliner(myQuery.pat)
    UI.utils.clearVariableNames()
    queries[0] = myQuery = new QueryObj()
  }
*/
  function addButtonCallbacks (target, fireOn) {
    UI.log.debug('Button callbacks for ' + fireOn + ' added')
    var makeIconCallback = function (icon) {
      return function IconCallback (req) {
        if (req.indexOf('#') >= 0) {
          console.log(
            '@@ makeIconCallback: Not expecting # in URI whose state changed: ' +
              req
          )
          // alert('Should have no hash in '+req)
        }
        if (!target) {
          return false
        }
        if (!outline.ancestor(target, 'DIV')) return false
        // if (term.termType != 'symbol') { return true } // should always ve
        if (req === fireOn) {
          target.src = icon
          target.title = outlineIcons.tooltips[icon]
        }
        return true
      }
    }
    sf.addCallback('request', makeIconCallback(outlineIcons.src.icon_requested))
    sf.addCallback('done', makeIconCallback(outlineIcons.src.icon_fetched))
    sf.addCallback('fail', makeIconCallback(outlineIcons.src.icon_failed))
  }

  //   Selection support

  function selected (node) {
    var a = node.getAttribute('class')
    if (a && a.indexOf('selected') >= 0) return true
    return false
  }

  // These woulkd be simpler using closer variables below
  function optOnIconMouseDownListener (e) {
    // outlineIcons.src.icon_opton  needed?
    var target = thisOutline.targetOf(e)
    var p = target.parentNode
    termWidget.replaceIcon(
      p.parentNode,
      outlineIcons.termWidgets.optOn,
      outlineIcons.termWidgets.optOff,
      optOffIconMouseDownListener
    )
    p.parentNode.parentNode.removeAttribute('optional')
  }

  function optOffIconMouseDownListener (e) {
    // outlineIcons.src.icon_optoff needed?
    var target = thisOutline.targetOf(e)
    var p = target.parentNode
    termWidget.replaceIcon(
      p.parentNode,
      outlineIcons.termWidgets.optOff,
      outlineIcons.termWidgets.optOn,
      optOnIconMouseDownListener
    )
    p.parentNode.parentNode.setAttribute('optional', 'true')
  }

  function setSelectedParent (node, inc) {
    var onIcon = outlineIcons.termWidgets.optOn
    var offIcon = outlineIcons.termWidgets.optOff
    for (var n = node; n.parentNode; n = n.parentNode) {
      while (true) {
        if (n.getAttribute('predTR')) {
          var num = n.getAttribute('parentOfSelected')
          if (!num) num = 0
          else num = parseInt(num)
          if (num === 0 && inc > 0) {
            termWidget.addIcon(
              n.childNodes[0],
              n.getAttribute('optional') ? onIcon : offIcon,
              n.getAttribute('optional')
                ? optOnIconMouseDownListener
                : optOffIconMouseDownListener
            )
          }
          num = num + inc
          n.setAttribute('parentOfSelected', num)
          if (num === 0) {
            n.removeAttribute('parentOfSelected')
            termWidget.removeIcon(
              n.childNodes[0],
              n.getAttribute('optional') ? onIcon : offIcon
            )
          }
          break
        } else if (n.previousSibling && n.previousSibling.nodeName === 'TR') {
          n = n.previousSibling
        } else break
      }
    }
  }

  this.statusBarClick = function (event) {
    var target = UI.utils.getTarget(event)
    if (target.label) {
      window.content.location = target.label
      // The following alternative does not work in the extension.
      // var s = UI.store.sym(target.label);
      // outline.GotoSubject(s, true);
    }
  }

  this.showURI = function showURI (about) {
    if (about && dom.getElementById('UserURI')) {
      dom.getElementById('UserURI').value =
        about.termType === 'NamedNode' ? about.uri : '' // blank if no URI
    }
  }

  this.showSource = function showSource () {
    if (typeof sourceWidget === 'undefined') return
    // deselect all before going on, this is necessary because you would switch tab,
    // close tab or so on...
    for (var uri in sourceWidget.sources) {
      sourceWidget.sources[uri].setAttribute('class', '')
    } // .class doesn't work. Be careful!
    for (var i = 0; i < selection.length; i++) {
      if (!selection[i].parentNode) {
        console.log('showSource: EH? no parentNode? ' + selection[i] + '\n')
        continue
      }
      var st = selection[i].parentNode.AJAR_statement
      if (!st) continue // for root TD
      var source = st.why
      if (source && source.uri) {
        sourceWidget.highlight(source, true)
      }
    }
  }

  this.getSelection = function getSelection () {
    return selection
  }

  function setSelected (node, newValue) {
    // UI.log.info('selection has ' +selection.map(function(item){return item.textContent;}).join(', '));
    // UI.log.debug('@outline setSelected, intended to '+(newValue?'select ':'deselect ')+node+node.textContent);
    // if (newValue === selected(node)) return; //we might not need this anymore...
    if (node.nodeName !== 'TD') {
      UI.log.debug('down' + node.nodeName)
      throw new Error(
        'Expected TD in setSelected: ' +
          node.nodeName +
          ' : ' +
          node.textContent
      )
    }
    UI.log.debug('pass')
    var cla = node.getAttribute('class')
    if (!cla) cla = ''
    if (newValue) {
      cla += ' selected'
      if (cla.indexOf('pred') >= 0 || cla.indexOf('obj') >= 0) {
        setSelectedParent(node, 1)
      }
      selection.push(node)
      // UI.log.info('Selecting '+node.textContent)

      var about = UI.utils.getTerm(node) // show uri for a newly selectedTd
      thisOutline.showURI(about)

      var st = node.AJAR_statement // show blue cross when the why of that triple is editable
      if (typeof st === 'undefined') st = node.parentNode.AJAR_statement
      // if (typeof st === 'undefined') return; // @@ Kludge?  Click in the middle of nowhere
      if (st) {
        // don't do these for headers or base nodes
        var source = st.why
        // var target = st.why
        var editable = UI.store.updater.editable(source.uri, kb)
        if (!editable) {
          // let target = node.parentNode.AJAR_inverse ? st.object : st.subject
        } // left hand side
        // think about this later. Because we update to the why for now.
        // alert('Target='+target+', editable='+editable+'\nselected statement:' + st)
        if (editable && cla.indexOf('pred') >= 0) {
          termWidget.addIcon(node, outlineIcons.termWidgets.addTri)
        } // Add blue plus
      }
    } else {
      UI.log.debug('cla=$' + cla + '$')
      if (cla === 'selected') cla = '' // for header <TD>
      cla = cla.replace(' selected', '')
      if (cla.indexOf('pred') >= 0 || cla.indexOf('obj') >= 0) {
        setSelectedParent(node, -1)
      }
      if (cla.indexOf('pred') >= 0) {
        termWidget.removeIcon(node, outlineIcons.termWidgets.addTri)
      }

      selection = selection.filter(function (x) {
        return x === node
      })

      UI.log.info('Deselecting ' + node.textContent)
    }
    if (typeof sourceWidget !== 'undefined') thisOutline.showSource() // Update the data sources display
    // UI.log.info('selection becomes [' +selection.map(function(item){return item.textContent;}).join(', ')+']');
    // UI.log.info('Setting className ' + cla);
    node.setAttribute('class', cla)
  }

  function deselectAll () {
    var n = selection.length
    for (let i = n - 1; i >= 0; i--) setSelected(selection[i], false)
    selection = []
  }

  /** Get the target of an event **/
  this.targetOf = function (e) {
    var target
    if (!e) e = window.event
    if (e.target) {
      target = e.target
    } else if (e.srcElement) {
      target = e.srcElement
    } else {
      UI.log.error("can't get target for event " + e)
      return false
    } // fail
    if (target.nodeType === 3) {
      // defeat Safari bug [sic]
      target = target.parentNode
    }
    return target
  } // targetOf

  this.walk = function walk (directionCode, inputTd) {
    var selectedTd = inputTd || selection[0]
    var newSelTd
    switch (directionCode) {
      case 'down':
        try {
          newSelTd = selectedTd.parentNode.nextSibling.lastChild
        } catch (e) {
          this.walk('up')
          return
        } // end
        deselectAll()
        setSelected(newSelTd, true)
        break
      case 'up':
        try {
          newSelTd = selectedTd.parentNode.previousSibling.lastChild
        } catch (e) {
          return
        } // top
        deselectAll()
        setSelected(newSelTd, true)
        break
      case 'right':
        deselectAll()
        if (
          selectedTd.nextSibling ||
          selectedTd.lastChild.tagName === 'strong'
        ) {
          setSelected(selectedTd.nextSibling, true)
        } else {
          var newSelected = dom.evaluate(
            'table/div/tr/td[2]',
            selectedTd,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
          ).singleNodeValue
          setSelected(newSelected, true)
        }
        break
      case 'left':
        deselectAll()
        if (
          selectedTd.previousSibling &&
          selectedTd.previousSibling.className === 'undetermined'
        ) {
          setSelected(selectedTd.previousSibling, true)
          return true // do not shrink signal
        } else {
          setSelected(UI.utils.ancestor(selectedTd.parentNode, 'TD'), true)
        } // supplied by thieOutline.focusTd
        break
      case 'moveTo':
        // UI.log.info(selection[0].textContent+'->'+inputTd.textContent);
        deselectAll()
        setSelected(inputTd, true)
        break
    }
    if (directionCode === 'down' || directionCode === 'up') {
      if (!newSelTd.tabulatorSelect) this.walk(directionCode)
    }
    // return newSelTd;
  }

  // Keyboard Input: we can consider this as...
  // 1. a fast way to modify data - enter will go to next predicate
  // 2. an alternative way to input - enter at the end of a predicate will create a new statement
  this.OutlinerKeypressPanel = function OutlinerKeypressPanel (e) {
    UI.log.info('Key ' + e.keyCode + ' pressed')

    function showURI (about) {
      if (about && dom.getElementById('UserURI')) {
        dom.getElementById('UserURI').value =
          about.termType === 'NamedNode' ? about.uri : '' // blank if no URI
      }
    }

    function setSelectedAfterward (_uri) {
      if (arguments[3]) return true
      walk('right', selectedTd)
      showURI(UI.utils.getAbout(kb, selection[0]))
      return true
    }
    var target, editable

    if (UI.utils.getTarget(e).tagName === 'TEXTAREA') return
    if (UI.utils.getTarget(e).id === 'UserURI') return
    if (selection.length > 1) return
    if (selection.length === 0) {
      if (
        e.keyCode === 13 ||
        e.keyCode === 38 ||
        e.keyCode === 40 ||
        e.keyCode === 37 ||
        e.keyCode === 39
      ) {
        this.walk('right', thisOutline.focusTd)
        showURI(UI.utils.getAbout(kb, selection[0]))
      }
      return
    }
    var selectedTd = selection[0]
    // if not done, Have to deal with redraw...
    sf.removeCallback('done', 'setSelectedAfterward')
    sf.removeCallback('fail', 'setSelectedAfterward')

    switch (e.keyCode) {
      case 13: // enter
        if (UI.utils.getTarget(e).tagName === 'HTML') {
          // I don't know why 'HTML'
          var object = UI.utils.getAbout(kb, selectedTd)
          target = selectedTd.parentNode.AJAR_statement.why
          editable = UI.store.updater.editable(target.uri, kb)
          if (object) {
            // <Feature about='enterToExpand'>
            outline.GotoSubject(object, true)
            /* //deal with this later
            deselectAll();
            var newTr=dom.getElementById('outline').lastChild;
            setSelected(newTr.firstChild.firstChild.childNodes[1].lastChild,true);
            function setSelectedAfterward(uri){
                deselectAll();
                setSelected(newTr.firstChild.firstChild.childNodes[1].lastChild,true);
                showURI(getAbout(kb,selection[0]));
                return true;
            }
            sf.insertCallback('done',setSelectedAfterward);
            sf.insertCallback('fail',setSelectedAfterward);
            */
            // </Feature>
          } else if (editable) {
            // this is a text node and editable
            thisOutline.UserInput.Enter(selectedTd)
          }
        } else {
          // var newSelTd=thisOutline.UserInput.lastModified.parentNode.parentNode.nextSibling.lastChild;
          this.UserInput.Keypress(e)
          this.walk('down') // bug with input at the end
          // dom.getElementById('docHTML').focus(); //have to set this or focus blurs
          e.stopPropagation()
        }
        return
      case 38: // up
        // thisOutline.UserInput.clearInputAndSave();
        // ^^^ does not work because up and down not captured...
        this.walk('up')
        e.stopPropagation()
        e.preventDefault()
        break
      case 40: // down
        // thisOutline.UserInput.clearInputAndSave();
        this.walk('down')
        e.stopPropagation()
        e.preventDefault()
    } // switch

    if (UI.utils.getTarget(e).tagName === 'INPUT') return

    switch (e.keyCode) {
      case 46: // delete
      case 8: // backspace
        target = selectedTd.parentNode.AJAR_statement.why
        editable = UI.store.updater.editable(target.uri, kb)
        if (editable) {
          e.preventDefault() // prevent from going back
          this.UserInput.Delete(selectedTd)
        }
        break
      case 37: // left
        if (this.walk('left')) return
        var titleTd = UI.utils.ancestor(selectedTd.parentNode, 'TD')
        outlineCollapse(selectedTd, UI.utils.getAbout(kb, titleTd))
        break
      case 39: // right
        // @@ TODO: Write away the need for exception on next line
        // eslint-disable-next-line no-case-declarations
        const obj = UI.utils.getAbout(kb, selectedTd)
        if (obj) {
          var walk = this.walk

          if (selectedTd.nextSibling) {
            // when selectedTd is a predicate
            this.walk('right')
            return
          }
          if (selectedTd.firstChild.tagName !== 'TABLE') {
            // not expanded
            sf.addCallback('done', setSelectedAfterward)
            sf.addCallback('fail', setSelectedAfterward)
            outlineExpand(selectedTd, obj, {
              pane: panes.byName('defaultPane')
            })
          }
          setSelectedAfterward()
        }
        break
      case 38: // up
      case 40: // down
        break
      default:
        switch (e.charCode) {
          case 99: // c for Copy
            if (e.ctrlKey) {
              thisOutline.UserInput.copyToClipboard(
                thisOutline.clipboardAddress,
                selectedTd
              )
              break
            }
            break
          case 118: // v
          case 112: // p for Paste
            if (e.ctrlKey) {
              thisOutline.UserInput.pasteFromClipboard(
                thisOutline.clipboardAddress,
                selectedTd
              )
              // dom.getElementById('docHTML').focus(); //have to set this or focus blurs
              // window.focus();
              // e.stopPropagation();
              break
            }
            break
          default:
            if (UI.utils.getTarget(e).tagName === 'HTML') {
              /*
              //<Feature about='typeOnSelectedToInput'>
              thisOutline.UserInput.Click(e,selectedTd);
              thisOutline.UserInput.lastModified.value=String.fromCharCode(e.charCode);
              if (selectedTd.className==='undetermined selected') thisOutline.UserInput.AutoComplete(e.charCode)
              //</Feature>
              */
              // Events are not reliable...
              // var e2=document.createEvent('KeyboardEvent');
              // e2.initKeyEvent('keypress',true,true,null,false,false,false,false,e.keyCode,0);
              // UserInput.lastModified.dispatchEvent(e2);
            }
        }
    } // end of switch

    showURI(UI.utils.getAbout(kb, selection[0]))
    // alert(window);alert(doc);
    /*
    var wm = Components.classes['@mozilla.org/appshell/window-mediator;1']
               .getService(Components.interfaces.nsIWindowMediator);
    var gBrowser = wm.getMostRecentWindow('navigator:browser') */
    // gBrowser.addTab('http://www.w3.org/');
    // alert(gBrowser.addTab);alert(gBrowser.scroll);alert(gBrowser.scrollBy)
    // gBrowser.scrollBy(0,100);

    // var thisHtml=selection[0].owner
    if (selection[0]) {
      var PosY = UI.utils.findPos(selection[0])[1]
      if (
        PosY + selection[0].clientHeight >
        window.scrollY + window.innerHeight
      ) {
        UI.utils.getEyeFocus(selection[0], true, true, window)
      }
      if (PosY < window.scrollY + 54) {
        UI.utils.getEyeFocus(selection[0], true, undefined, window)
      }
    }
  }
  this.OutlinerMouseclickPanel = function (e) {
    switch (thisOutline.UserInput._tabulatorMode) {
      case 0:
        TabulatorMousedown(e)
        break
      case 1:
        thisOutline.UserInput.Click(e)
        break
      default:
    }
  }

  /** things to do onmousedown in outline view **/
  /*
   **   To Do:  This big event handler needs to be replaced by lots
   ** of little ones individually connected to each icon.  This horrible
   ** switch below isn't modular. (Sorry!) - Tim
   */
  // expand
  // collapse
  // refocus
  // select
  // visit/open a page

  function expandMouseDownListener (e) {
    // For icon (UI.icons.originalIconBase + 'tbl-expand-trans.png')
    var target = thisOutline.targetOf(e)
    var p = target.parentNode
    var subject = UI.utils.getAbout(kb, target)
    var pane = e.altKey ? panes.byName('internal') : undefined // set later: was panes.defaultPane

    if (e.shiftKey) {
      // Shift forces a refocus - bring this to the top
      outlineRefocus(p, subject, pane)
    } else {
      if (e.altKey) {
        // To investigate screw ups, dont wait show internals
        outlineExpand(p, subject, {
          pane: panes.byName('internal'),
          immediate: true
        })
      } else {
        outlineExpand(p, subject)
      }
    }
  }

  function collapseMouseDownListener (e) {
    // for icon UI.icons.originalIconBase + 'tbl-collapse.png'
    var target = thisOutline.targetOf(e)
    var subject = UI.utils.getAbout(kb, target)
    var pane = e.altKey ? panes.byName('internal') : undefined
    var p = target.parentNode.parentNode
    outlineCollapse(p, subject, pane)
  }

  function failedIconMouseDownListener (e) {
    // outlineIcons.src.icon_failed
    var target = thisOutline.targetOf(e)
    var uri = target.getAttribute('uri') // Put on access buttons
    if (e.altKey) {
      sf.fetch(UI.rdf.uri.docpart(uri), {
        force: true
      }) // Add 'force' bit?
    } else {
      sf.refresh(kb.sym(UI.rdf.uri.docpart(uri))) // just one
    }
  }

  function fetchedIconMouseDownListener (e) {
    // outlineIcons.src.icon_fetched
    var target = thisOutline.targetOf(e)
    var uri = target.getAttribute('uri') // Put on access buttons
    if (e.altKey) {
      sf.fetch(UI.rdf.uri.docpart(uri), {
        force: true
      })
    } else {
      sf.refresh(kb.sym(UI.rdf.uri.docpart(uri))) // just one
    }
  }

  function unrequestedIconMouseDownListener (e) {
    var target = thisOutline.targetOf(e)
    var uri = target.getAttribute('uri') // Put on access buttons
    sf.fetch(UI.rdf.uri.docpart(uri))
  }

  function removeNodeIconMouseDownListener (e) {
    // icon_remove_node
    var target = thisOutline.targetOf(e)
    var node = target.node
    if (node.childNodes.length > 1) node = target.parentNode // parallel outline view @@ Hack
    removeAndRefresh(node) // @@ update icons for pane?
  }

  function selectableTDClickListener (e) {
    // Is we are in editing mode already
    if (thisOutline.UserInput._tabulatorMode) {
      return thisOutline.UserInput.Click(e)
    }

    var target = thisOutline.targetOf(e)
    // Originally this was set on the whole tree and could happen anywhere
    // var p = target.parentNode
    var node
    for (
      node = UI.utils.ancestor(target, 'TD');
      node && !(node.getAttribute('notSelectable') === 'false'); // Default now is not selectable
      node = UI.utils.ancestor(node.parentNode, 'TD')
    ) {}
    if (!node) return

    // var node = target;

    var sel = selected(node)
    // var cla = node.getAttribute('class')
    UI.log.debug('Was node selected before: ' + sel)
    if (e.altKey) {
      setSelected(node, !selected(node))
    } else if (e.shiftKey) {
      setSelected(node, true)
    } else {
      // setSelected(node, !selected(node))
      deselectAll()
      thisOutline.UserInput.clearInputAndSave(e)
      setSelected(node, true)

      if (e.detail === 2) {
        // double click -> quit TabulatorMousedown()
        e.stopPropagation()
        return
      }
      // if the node is already selected and the corresponding statement is editable,
      // go to UserInput
      var st = node.parentNode.AJAR_statement
      if (!st) return // For example in the title TD of an expanded pane
      const target = st.why
      var editable = UI.store.updater.editable(target.uri, kb)
      if (sel && editable) thisOutline.UserInput.Click(e, selection[0]) // was next 2 lines
      // var text='TabulatorMouseDown@Outline()';
      // HCIoptions['able to edit in Discovery Mode by mouse'].setupHere([sel,e,thisOutline,selection[0]],text);
    }
    UI.log.debug(
      'Was node selected after: ' +
        selected(node) +
        ', count=' +
        selection.length
    )
    // var tr = node.parentNode
    /*
    if (tr.AJAR_statement) {
      // var why = tr.AJAR_statement.why
        // UI.log.info('Information from '+why);
    }
    */
    e.stopPropagation()
    // this is important or conflict between deselect and user input happens
  }

  function TabulatorMousedown (e) {
    UI.log.info('@TabulatorMousedown, dom.location is now ' + dom.location)
    var target = thisOutline.targetOf(e)
    if (!target) return
    var tname = target.tagName
    // UI.log.debug('TabulatorMousedown: ' + tname + ' shift='+e.shiftKey+' alt='+e.altKey+' ctrl='+e.ctrlKey);
    // var p = target.parentNode
    // var about = UI.utils.getAbout(kb, target)
    // var source = null
    if (tname === 'INPUT' || tname === 'TEXTAREA') {
      return
    }

    // not input then clear
    thisOutline.UserInput.clearMenu()

    // ToDo:remove this and recover X
    if (
      thisOutline.UserInput.lastModified &&
      thisOutline.UserInput.lastModified.parentNode.nextSibling
    ) {
      thisOutline.UserInput.backOut()
    }

    // if (typeof rav=='undefined') //uncomment this for javascript2rdf
    // have to put this here or this conflicts with deselectAll()

    if (
      !target.src ||
      (target.src.slice(target.src.indexOf('/icons/') + 1) !==
        outlineIcons.src.icon_show_choices &&
        target.src.slice(target.src.indexOf('/icons/') + 1) !==
          outlineIcons.src.icon_add_triple)
    ) {
      thisOutline.UserInput.clearInputAndSave(e)
    }

    if (
      !target.src ||
      target.src.slice(target.src.indexOf('/icons/') + 1) !==
        outlineIcons.src.icon_show_choices
    ) {
      thisOutline.UserInput.clearMenu()
    }

    if (e) e.stopPropagation()
  } // function TabulatorMousedown

  function setUrlBarAndTitle (subject) {
    dom.title = UI.utils.label(subject)
    if (dom.location.href.startsWith(subject.site().uri)) {
      // dom.location = subject.uri  // No causes reload
    }
  }

  /** Expand an outline view
   * @param p {Element} - container
   */
  function outlineExpand (p, subject1, options) {
    options = options || {}
    var pane = options.pane
    var already = !!options.already
    var immediate = options.immediate

    UI.log.info('@outlineExpand, dom is now ' + dom.location)
    // remove callback to prevent unexpected repaint
    sf.removeCallback('done', 'expand')
    sf.removeCallback('fail', 'expand')

    var subject = kb.canon(subject1)
    // var requTerm = subject.uri ? kb.sym(UI.rdf.uri.docpart(subject.uri)) : subject

    function render () {
      subject = kb.canon(subject)
      if (!p || !p.parentNode || !p.parentNode.parentNode) return false

      var newTable
      UI.log.info('@@ REPAINTING ')
      if (!already) {
        // first expand
        newTable = propertyTable(subject, undefined, pane, options)
      } else {
        UI.log.info(' ... p is  ' + p)
        for (
          newTable = p.firstChild;
          newTable.nextSibling;
          newTable = newTable.nextSibling
        ) {
          UI.log.info(' ... checking node ' + newTable)
          if (newTable.nodeName === 'table') break
        }
        newTable = propertyTable(subject, newTable, pane, options)
      }
      already = true
      if (
        UI.utils.ancestor(p, 'TABLE') &&
        UI.utils.ancestor(p, 'TABLE').style.backgroundColor === 'white'
      ) {
        newTable.style.backgroundColor = '#eee'
      } else {
        newTable.style.backgroundColor = 'white'
      }
      try {
        if (YAHOO.util.Event.off) {
          YAHOO.util.Event.off(p, 'mousedown', 'dragMouseDown')
        }
      } catch (e) {
        console.log('YAHOO ' + e)
      }
      UI.utils.emptyNode(p).appendChild(newTable)
      thisOutline.focusTd = p // I don't know why I couldn't use 'this'...because not defined in callbacks
      UI.log.debug('expand: Node for ' + subject + ' expanded')
      // fetch seeAlso when render()
      // var seeAlsoStats = sf.store.statementsMatching(subject, UI.ns.rdfs('seeAlso'))
      // seeAlsoStats.map(function (x) {sf.lookUpThing(x.object, subject,false);})
      var seeAlsoWhat = kb.each(subject, UI.ns.rdfs('seeAlso'))
      for (var i = 0; i < seeAlsoWhat.length; i++) {
        if (i === 25) {
          UI.log.warn(
            'expand: Warning: many (' +
              seeAlsoWhat.length +
              ') seeAlso links for ' +
              subject
          )
          // break; Not sure what limits the AJAX system has here
        }
        sf.lookUpThing(seeAlsoWhat[i], subject)
      }
    }

    function expand (uri) {
      if (arguments[3]) return true // already fetched indicator
      var cursubj = kb.canon(subject) // canonical identifier may have changed
      UI.log.info(
        '@@ expand: relevant subject=' +
          cursubj +
          ', uri=' +
          uri +
          ', already=' +
          already
      )
      // var term = kb.sym(uri)
      var docTerm = kb.sym(UI.rdf.uri.docpart(uri))
      if (uri.indexOf('#') >= 0) {
        throw new Error('Internal error: hash in ' + uri)
      }

      var relevant = function () {
        // Is the loading of this URI relevam to the display of subject?
        if (!cursubj.uri) return true // bnode should expand()
        var as = kb.uris(cursubj)
        if (!as) return false
        for (var i = 0; i < as.length; i++) {
          // canon'l uri or any alias
          for (
            var rd = UI.rdf.uri.docpart(as[i]);
            rd;
            rd = kb.HTTPRedirects[rd]
          ) {
            if (uri === rd) return true
          }
        }
        if (kb.anyStatementMatching(cursubj, undefined, undefined, docTerm)) {
          return true
        } // Kenny: inverse?
        return false
      }
      if (relevant()) {
        UI.log.success(
          '@@ expand OK: relevant subject=' +
            cursubj +
            ', uri=' +
            uri +
            ', source=' +
            already
        )

        render()
        return false //  @@@@@@@@@@@ Will this allow just the first
      }
      return true
    }
    // Body of outlineExpand

    if (options.solo) {
      setUrlBarAndTitle(subject)
    }
    UI.log.debug('outlineExpand: dereferencing ' + subject)
    var status = dom.createElement('span')
    p.appendChild(status)
    sf.addCallback('done', expand) // @@@@@@@ This can really mess up existing work
    sf.addCallback('fail', expand) // Need to do if there s one a gentle resync of page with store

    var returnConditions = [] // this is quite a general way to do cut and paste programming
    // I might make a class for this
    if (subject.uri && subject.uri.split(':')[0] === 'rdf') {
      // what is this? -tim
      render()
      return
    }

    for (var i = 0; i < returnConditions.length; i++) {
      var returnCode
      if (returnCode === returnConditions[i](subject)) {
        render()
        UI.log.debug('outline 1815')
        if (returnCode[1]) outlineElement.removeChild(outlineElement.lastChild)
        return
      }
    }
    if (
      subject.uri &&
      !immediate &&
      !UI.widgets.isAudio(subject) &&
      !UI.widgets.isVideo(subject) && // Never parse videos as data
      !kb.holds(
        subject,
        UI.ns.rdf('type'),
        $rdf.Util.mediaTypeClass('application/pdf')
      )
    ) {
      // or PDF
      // Wait till at least the main URI is loaded before expanding:
      sf.nowOrWhenFetched(subject.doc(), undefined, function (ok, body) {
        if (ok) {
          sf.lookUpThing(subject)
          render() // inital open, or else full if re-open
          if (options.solo) {
            // Update window title with new information
            // dom.title = UI.utils.label(subject)
            setUrlBarAndTitle(subject)
          }
        } else {
          var message = dom.createElement('pre')
          message.textContent = body
          message.setAttribute('style', 'background-color: #fee;')
          message.textContent =
            'Outline.expand: Unable to fetch ' + subject.doc() + ': ' + body
          p.appendChild(message)
        }
      })
    } else {
      render()
    }
  } // outlineExpand

  function outlineCollapse (p, subject) {
    var row = UI.utils.ancestor(p, 'TR')
    row = UI.utils.ancestor(row.parentNode, 'TR') // two levels up
    if (row) var statement = row.AJAR_statement
    var level // find level (the enclosing TD)
    for (
      level = p.parentNode;
      level.tagName !== 'TD';
      level = level.parentNode
    ) {
      if (typeof level === 'undefined') {
        alert('Not enclosed in TD!')
        return
      }
    }

    UI.log.debug('Collapsing subject ' + subject)
    var myview
    if (statement) {
      UI.log.debug('looking up pred ' + statement.predicate.uri + 'in defaults')
      myview = views.defaults[statement.predicate.uri]
    }
    UI.log.debug('view= ' + myview)
    if (level.parentNode.parentNode.id === 'outline') {
      var deleteNode = level.parentNode
    }
    thisOutline.replaceTD(
      thisOutline.outlineObjectTD(subject, myview, deleteNode, statement),
      level
    )
  } // outlineCollapse

  this.replaceTD = function replaceTD (newTd, replacedTd) {
    var reselect
    if (selected(replacedTd)) reselect = true

    // deselects everything being collapsed. This goes backwards because
    // deselecting an element decreases selection.length
    for (var x = selection.length - 1; x > -1; x--) {
      for (var elt = selection[x]; elt.parentNode; elt = elt.parentNode) {
        if (elt === replacedTd) {
          setSelected(selection[x], false)
        }
      }
    }

    replacedTd.parentNode.replaceChild(newTd, replacedTd)
    if (reselect) setSelected(newTd, true)
  }

  function outlineRefocus (p, subject) {
    // Shift-expand or shift-collapse: Maximize
    var outer = null
    for (var level = p.parentNode; level; level = level.parentNode) {
      UI.log.debug('level ' + level.tagName)
      if (level.tagName === 'TD') outer = level
    } // find outermost td
    UI.utils.emptyNode(outer).appendChild(propertyTable(subject))
    setUrlBarAndTitle(subject)
    // dom.title = UI.utils.label(subject)
    outer.setAttribute('about', subject.toNT())
  } // outlineRefocus

  outline.outlineRefocus = outlineRefocus

  // Inversion is turning the outline view inside-out
  // It may be called eversion
  /*
  function outlineInversion (p, subject) { // re-root at subject
    function move_root (rootTR, childTR) { // swap root with child
      // @@
    }
  }
*/
  this.GotoFormURI_enterKey = function (e) {
    if (e.keyCode === 13) outline.GotoFormURI(e)
  }
  this.GotoFormURI = function (_e) {
    GotoURI(dom.getElementById('UserURI').value)
  }

  function GotoURI (uri) {
    var subject = kb.sym(uri)
    this.GotoSubject(subject, true)
  }
  this.GotoURIinit = function (uri) {
    var subject = kb.sym(uri)
    this.GotoSubject(subject)
  }

  /** Display the subject in an outline view

  @param subject -- RDF term for teh thing to be presented
  @param expand  -- flag -- open the subject rather than keep folded closed
  @param pane    -- optional -- pane to be used for expanded display
  @param solo    -- optional -- the window will be cleared out and only the subject displayed
  @param referer -- optional -- where did we hear about this from anyway?
  @param table   -- option  -- a table element in which to put the outline.
*/
  this.GotoSubject = function (subject, expand, pane, solo, referrer, table) {
    table = table || dom.getElementById('outline') // if does not exist just add one? nowhere to out it
    if (solo) {
      UI.utils.emptyNode(table)
      table.style.width = '100%'
    }

    function GotoSubjectDefault () {
      var tr = dom.createElement('TR')
      tr.style.verticalAlign = 'top'
      table.appendChild(tr)
      var td = thisOutline.outlineObjectTD(subject, undefined, tr)
      tr.appendChild(td)
      return td
    }

    var td = GotoSubjectDefault()

    if (solo) setUrlBarAndTitle(subject) // dom.title = UI.utils.label(subject) // 'Tabulator: '+  No need to advertize

    if (expand) {
      outlineExpand(td, subject, {
        pane: pane,
        solo: solo
      })
      var tr = td.parentNode
      UI.utils.getEyeFocus(tr, false, undefined, window) // instantly: false
    }

    if (
      solo &&
      dom &&
      dom.defaultView &&
      dom.defaultView.history &&
      // Don't add the new location to the history if we arrived here through a direct link
      // (i.e. when static/databrowser.html in node-solid-server called this method):
      document.location.href !== subject.uri
    ) {
      const stateObj = pane ? { paneName: pane.name } : {}
      try {
        // can fail if different origin
        dom.defaultView.history.pushState(stateObj, subject.uri, subject.uri)
      } catch (e) {
        console.log(e)
      }
    }

    return subject
  }

  // / /////////////////////////////////////////////////////
  //
  //
  //                    VIEWS
  //
  //
  // / /////////////////////////////////////////////////////

  var views = {
    properties: [],
    defaults: [],
    classes: []
  } // views

  /** add a property view function **/
  function viewsAddPropertyView (property, pviewfunc, isDefault) {
    if (!views.properties[property]) {
      views.properties[property] = []
    }
    views.properties[property].push(pviewfunc)
    if (isDefault) {
      // will override an existing default!
      views.defaults[property] = pviewfunc
    }
  } // addPropertyView

  var ns = UI.ns
  // view that applies to items that are objects of certain properties.
  // viewsAddPropertyView(property, viewjsfile, default?)
  viewsAddPropertyView(ns.foaf('depiction').uri, viewAsImage, true)
  viewsAddPropertyView(ns.foaf('img').uri, viewAsImage, true)
  viewsAddPropertyView(ns.foaf('thumbnail').uri, viewAsImage, true)
  viewsAddPropertyView(ns.foaf('logo').uri, viewAsImage, true)
  viewsAddPropertyView(ns.foaf('mbox').uri, viewAsMbox, true)
  // viewsAddPropertyView(ns.foaf('based_near').uri, VIEWAS_map, true);
  // viewsAddPropertyView(ns.foaf('birthday').uri, VIEWAS_cal, true);

  // var thisOutline = this   dup
  /** some builtin simple views **/

  function viewAsBoringDefault (obj) {
    // UI.log.debug('entered viewAsBoringDefault...');
    var rep // representation in html

    if (obj.termType === 'Literal') {
      var styles = {
        integer: 'text-align: right;',
        decimal: "text-align: '.';",
        double: "text-align: '.';"
      }
      rep = dom.createElement('span')
      rep.textContent = obj.value
      // Newlines have effect and overlong lines wrapped automatically
      var style = ''
      if (obj.datatype && obj.datatype.uri) {
        var xsd = UI.ns.xsd('').uri
        if (obj.datatype.uri.slice(0, xsd.length) === xsd) {
          style = styles[obj.datatype.uri.slice(xsd.length)]
        }
      }
      rep.setAttribute('style', style || 'white-space: pre-wrap;')
    } else if (obj.termType === 'NamedNode' || obj.termType === 'BlankNode') {
      rep = dom.createElement('span')
      rep.setAttribute('about', obj.toNT())
      thisOutline.appendAccessIcons(kb, rep, obj)

      if (obj.termType === 'NamedNode') {
        if (obj.uri.slice(0, 4) === 'tel:') {
          var num = obj.uri.slice(4)
          var anchor = dom.createElement('a')
          rep.appendChild(dom.createTextNode(num))
          anchor.setAttribute('href', obj.uri)
          anchor.appendChild(
            UI.utils.AJARImage(
              outlineIcons.src.icon_telephone,
              'phone',
              'phone ' + num,
              dom
            )
          )
          rep.appendChild(anchor)
          anchor.firstChild.setAttribute('class', 'phoneIcon')
        } else {
          // not tel:
          rep.appendChild(dom.createTextNode(UI.utils.label(obj)))
          UI.widgets.makeDraggable(rep, obj) // 2017
        }
      } else {
        // bnode
        rep.appendChild(dom.createTextNode(UI.utils.label(obj)))
      }
    } else if (obj.termType === 'Collection') {
      // obj.elements is an array of the elements in the collection
      rep = dom.createElement('table')
      rep.setAttribute('style', 'width: 100%;')
      rep.setAttribute('about', obj.toNT())
      /* Not sure which looks best -- with or without. I think without

                var tr = rep.appendChild(document.createElement('tr'));
                tr.appendChild(document.createTextNode(
                        obj.elements.length ? '(' + obj.elements.length+')' : '(none)'));
        */
      for (var i = 0; i < obj.elements.length; i++) {
        var elt = obj.elements[i]
        var row = rep.appendChild(dom.createElement('tr'))
        var numcell = row.appendChild(dom.createElement('td'))
        numcell.setAttribute(
          'style',
          'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
        )
        numcell.setAttribute('notSelectable', 'false')
        numcell.setAttribute('about', obj.toNT())
        numcell.innerHTML = i + 1 + ')'
        row.appendChild(thisOutline.outlineObjectTD(elt))
      }
    } else if (obj.termType === 'Graph') {
      rep = panes
        .byName('dataContentPane')
        .statementsAsTables(obj.statements, context)
      rep.setAttribute('class', 'nestedFormula')
    } else {
      UI.log.error('Object ' + obj + ' has unknown term type: ' + obj.termType)
      rep = dom.createTextNode('[unknownTermType:' + obj.termType + ']')
    } // boring defaults.
    UI.log.debug('contents: ' + rep.innerHTML)
    return rep
  } // boring_default

  function viewAsImage (obj) {
    var img = UI.utils.AJARImage(
      obj.uri,
      UI.utils.label(obj),
      UI.utils.label(obj),
      dom
    )
    img.setAttribute('class', 'outlineImage')
    return img
  }

  function viewAsMbox (obj) {
    var anchor = dom.createElement('a')
    // previous implementation assumed email address was Literal. fixed.

    // FOAF mboxs must NOT be literals -- must be mailto: URIs.

    var address = obj.termType === 'NamedNode' ? obj.uri : obj.value // this way for now
    if (!address) return viewAsBoringDefault(obj)
    var index = address.indexOf('mailto:')
    address = index >= 0 ? address.slice(index + 7) : address
    anchor.setAttribute('href', 'mailto:' + address)
    anchor.appendChild(dom.createTextNode(address))
    return anchor
  }

  this.createTabURI = function () {
    dom.getElementById('UserURI').value =
      dom.URL + '?uri=' + dom.getElementById('UserURI').value
  }

  // a way to expose variables to UserInput without making them propeties/methods
  this.UserInput.setSelected = setSelected
  this.UserInput.deselectAll = deselectAll
  this.UserInput.views = views
  this.outlineExpand = outlineExpand

  // this.panes = panes; // Allow external panes to register

  return this
} // END OF OUTLINE