michielbdejong/solid-ui

View on GitHub
src/widgets/forms.js

Summary

Maintainability
F
2 wks
Test Coverage
/*       F O R M S
 *
 *      A Vanilla Dom implementation of the form language
 */

/* global alert */

module.exports = {}

var forms = {}

forms.field = {} // Form field functions by URI of field type.

var UI = {
  icons: require('../iconBase'),
  log: require('../log'),
  ns: require('../ns'),
  store: require('../store'),
  style: require('../style'),
  widgets: forms
}
const $rdf = require('rdflib')
const error = require('./error')
const buttons = require('./buttons')
const ns = require('../ns')
const utils = require('../utils')

const checkMarkCharacter = '\u2713'
const cancelCharacter = '\u2715'
const dashCharacter = '-'

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

/*                                  Form Field implementations
 **
 */
/**          Group of different fields
 **
 **  One type of form field is an ordered Group of other fields.
 **  A Form is actually just the same as a group.
 **
 ** @param {Document} dom The HTML Document object aka Document Object Model
 ** @param {Element?} container  If present, the created widget will be appended to this
 ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping
 ** @param {Node} subject The thing about which the form displays/edits data
 ** @param {Node} form The form or field to be rendered
 ** @param {Node} store The web document in which the data is
 ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed?
 **
 ** @returns {Element} The HTML widget created
 */
forms.field[ns.ui('Form').uri] = forms.field[
  ns.ui('Group').uri
] = function (dom, container, already, subject, form, store, callbackFunction) {
  const kb = UI.store
  var box = dom.createElement('div')
  box.setAttribute('style', `padding-left: 2em; border: 0.05em solid ${UI.style.formBorderColor};`) // Indent a group
  const ui = UI.ns.ui
  if (container) container.appendChild(box)

  // Prevent loops
  var key = subject.toNT() + '|' + form.toNT()
  if (already[key]) {
    // been there done that
    box.appendChild(dom.createTextNode('Group: see above ' + key))
    var plist = [$rdf.st(subject, ns.owl('sameAs'), subject)] // @@ need prev subject
    dom.outlineManager.appendPropertyTRs(box, plist)
    return box
  }
  // box.appendChild(dom.createTextNode('Group: first time, key: '+key))
  var already2 = {}
  for (var x in already) already2[x] = 1
  already2[key] = 1

  var parts = kb.any(form, ui('parts'))
  var p2
  if (parts) {
    p2 = parts.elements
  } else {
    parts = kb.each(form, ui('part')) //  Warning: unordered
    p2 = forms.sortBySequence(parts)
  }
  if (!parts) {
    box.appendChild(error.errorMessageBlock(dom, 'No parts to form! '))
    return dom
  }
  var eles = []
  var original = []
  for (var i = 0; i < p2.length; i++) {
    var field = p2[i]
    var t = forms.mostSpecificClassURI(field) // Field type
    if (t === ui('Options').uri) {
      var dep = kb.any(field, ui('dependingOn'))
      if (dep && kb.any(subject, dep)) original[i] = kb.any(subject, dep).toNT()
    }

    var fn = forms.fieldFunction(dom, field)

    var itemChanged = function (ok, body) {
      if (ok) {
        for (var j = 0; j < p2.length; j++) {
          // This is really messy.
          var field = p2[j]
          var t = forms.mostSpecificClassURI(field) // Field type
          if (t === ui('Options').uri) {
            var dep = kb.any(field, ui('dependingOn'))
            var newOne = fn(
              dom,
              box,
              already,
              subject,
              field,
              store,
              callbackFunction
            )
            box.removeChild(newOne)
            box.insertBefore(newOne, eles[j])
            box.removeChild(eles[j])
            original[j] = kb.any(subject, dep).toNT()
            eles[j] = newOne
          }
        }
      }
      callbackFunction(ok, body)
    }
    eles.push(fn(dom, box, already2, subject, field, store, itemChanged))
  }
  return box
}

/**          Options field: Select one or more cases
 **
 ** @param {Document} dom The HTML Document object aka Document Object Model
 ** @param {Element?} container  If present, the created widget will be appended to this
 ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping
 ** @param {Node} subject The thing about which the form displays/edits data
 ** @param {Node} form The form or field to be rendered
 ** @param {Node} store The web document in which the data is
 ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed?
 **
 ** @returns {Element} The HTML widget created
 */

forms.field[ns.ui('Options').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  const kb = UI.store
  var box = dom.createElement('div')
  // box.setAttribute('style', 'padding-left: 2em; border: 0.05em dotted purple;')  // Indent Options
  const ui = UI.ns.ui
  if (container) container.appendChild(box)

  var dependingOn = kb.any(form, ui('dependingOn'))
  if (!dependingOn) {
    dependingOn = ns.rdf('type')
  } // @@ default to type (do we want defaults?)
  var cases = kb.each(form, ui('case'))
  if (!cases) {
    box.appendChild(error.errorMessageBlock(dom, 'No cases to Options form. '))
  }
  var values
  if (dependingOn.sameTerm(ns.rdf('type'))) {
    values = kb.findTypeURIs(subject)
  } else {
    var value = kb.any(subject, dependingOn)
    if (value === undefined) {
      box.appendChild(
        error.errorMessageBlock(
          dom,
          "Can't select subform as no value of: " + dependingOn
        )
      )
    } else {
      values = {}
      values[value.uri] = true
    }
  }
  // @@ Add box.refresh() to sync fields with values
  for (var i = 0; i < cases.length; i++) {
    var c = cases[i]
    var tests = kb.each(c, ui('for')) // There can be multiple 'for'
    for (var j = 0; j < tests.length; j++) {
      if (values[tests[j].uri]) {
        var field = kb.the(c, ui('use'))
        if (!field) {
          box.appendChild(
            error.errorMessageBlock(
              dom,
              'No "use" part for case in form ' + form
            )
          )
          return box
        } else {
          forms.appendForm(
            dom,
            box,
            already,
            subject,
            field,
            store,
            callbackFunction
          )
        }
        break
      }
    }
  }
  return box
}

/**          Multiple field: zero or more similar subFields
 **
 ** @param {Document} dom The HTML Document object aka Document Object Model
 ** @param {Element?} container  If present, the created widget will be appended to this
 ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping
 ** @param {Node} subject The thing about which the form displays/edits data
 ** @param {Node} form The form or field to be rendered
 ** @param {Node} store The web document in which the data is
 ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed?
 **
 ** @returns {Element} The HTML widget created
 */
forms.field[ns.ui('Multiple').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  /** Diagnostic function
  */
  function debugString (values) {
    return values.map(x => x.toString().slice(-7)).join(', ')
  }

  /** Add an item to the local quadstore not the UI or the web
  *
   * @param {Node} object The RDF object to be represented by this item.
   */
  async function addItem (object) {
    if (!object) object = forms.newThing(store) // by default just add new nodes
    if (ordered) {
      createListIfNecessary() // Sets list and unsavedList
      list.elements.push(object)
      await saveListThenRefresh()
    } else {
      const toBeInserted = [$rdf.st(subject, property, object, store)]
      try {
        await kb.updater.update([], toBeInserted)
      } catch (err) {
        const msg = 'Error adding to unordered multiple: ' + err
        box.appendChild(error.errorMessageBlock(dom, msg))
        console.error(msg)
      }
      refresh() // 20191213
    }
  }

  /** Make a dom representation for an item
   * @param {Event} anyEvent if used as an event handler
   * @param {Node} object The RDF object to be represented by this item.
   */
  function renderItem (object) {
    async function deleteThisItem () {
      if (ordered) {
        console.log('pre delete: ' + debugString(list.elements))
        for (let i = 0; i < list.elements.length; i++) {
          if (list.elements[i].sameTerm(object)) {
            list.elements.splice(i, 1)
            await saveListThenRefresh()
            return
          }
        }
      } else {
        // unordered
        if (kb.holds(subject, property, object)) {
          var del = [$rdf.st(subject, property, object, store)]
          kb.updater.update(del, [], function (uri, ok, message) {
            if (ok) {
              body.removeChild(subField)
            } else {
              body.appendChild(
                error.errorMessageBlock(
                  dom,
                  'Multiple: delete failed: ' + message
                )
              )
            }
          })
        }
      }
    }

    /** Move the object up or down in the ordered list
     * @param {Event} anyEvent if used as an event handler
     * @param {Boolean} upwards Move this up (true) or down (false).
     */
    async function moveThisItem (event, upwards) {
      // @@ possibly, allow shift+click to do move to top or bottom?
      console.log('pre move: ' + debugString(list.elements))
      for (var i = 0; i < list.elements.length; i++) {
        // Find object in array
        if (list.elements[i].sameTerm(object)) {
          break
        }
      }
      if (i === list.elements.length) {
        alert('list move: not found element for ' + object)
      }
      if (upwards) {
        if (i === 0) {
          alert('@@ boop - already at top   -temp message') // @@ make boop sound
          return
        }
        list.elements.splice(i - 1, 2, list.elements[i], list.elements[i - 1])
      } else {
        // downwards
        if (i === list.elements.length - 1) {
          alert('@@ boop - already at bottom   -temp message') // @@ make boop sound
          return
        }
        list.elements.splice(i, 2, list.elements[i + 1], list.elements[i])
      }
      await saveListThenRefresh()
    }
    /* A subField has been filled in
    *
    * One possibility is to not actually make the link to the thing until
    * this callback happens to avoid widow links
     */
    function itemDone (uri, ok, message) {
      console.log(`Item ${uri} done callback for item ${object.uri.slice(-7)}`)
      if (!ok) { // when does this happen? errors typically deal with upstream
        console.error('  Item done callback: Error: ' + message)
      } else {
        linkDone(uri, ok, message)
      }
      /*  Put this as a function and call it from only one place
      var ins, del
      // alert('Multiple: item calklback.' + uri)
      if (ok) {
        // @@@ Check IT hasnt alreday been written in
        if (ordered) {
          list = kb.any(subject, property, null, store)
          if (!list) {
            list = new $rdf.Collection([object])
            ins = [$rdf.st(subject, property, list)] // Will this work?
          } else {
            const oldList = new $rdf.Collection(list.elments)
            list.append(object)
            del = [$rdf.st(subject, property, oldList)] // If this doesn't work, kb.saveBack(store)
            ins = [$rdf.st(subject, property, list)]
          }
        } else {
          if (!kb.holds(subject, property, object, store)) {
            ins = [$rdf.st(subject, property, object, store)]
          }
          kb.updater.update(del, ins, linkDone)
        }
      } else {
        box.appendChild(
          error.errorMessageBlock(dom, 'Multiple: item failed: ' + body)
        )
        callbackFunction(ok, message)
      }
      */
    }
    var linkDone = function (uri, ok, message) {
      return callbackFunction(ok, message)
    }

    // if (!object) object = forms.newThing(store)
    UI.log.debug('Multiple: render object: ' + object)
    // var tr = box.insertBefore(dom.createElement('tr'), tail)
    // var ins = []
    // var del = []

    var fn = forms.fieldFunction(dom, element)
    var subField = fn(dom, null, already, object, element, store, itemDone) // p2 was: body.  moving to not passing that
    subField.subject = object // Keep a back pointer between the DOM array and the RDF objects

    // delete button and move buttons
    if (kb.updater.editable(store.uri)) {
      buttons.deleteButtonWithCheck(dom, subField, utils.label(property),
        deleteThisItem)
      if (ordered) {
        subField.appendChild(
          buttons.button(
            dom, UI.icons.iconBase + 'noun_1369237.svg', 'Move Up',
            async event => moveThisItem(event, true))
        )
        subField.appendChild(
          buttons.button(
            dom, UI.icons.iconBase + 'noun_1369241.svg', 'Move Down',
            async event => moveThisItem(event, false))
        )
      }
    }
    return subField // unused
  } // renderItem

  /// ///////// Body of form field implementation

  var plusIconURI = UI.icons.iconBase + 'noun_19460_green.svg' // white plus in green circle

  const kb = UI.store
  kb.updater = kb.updater || new $rdf.UpdateManager(kb)
  var box = dom.createElement('table')
  // We don't indent multiple as it is a sort of a prefix of the next field and has contents of one.
  // box.setAttribute('style', 'padding-left: 2em; border: 0.05em solid green;')  // Indent a multiple
  const ui = UI.ns.ui
  if (container) container.appendChild(box)

  const orderedNode = kb.any(form, ui('ordered'))
  const ordered = orderedNode ? $rdf.Node.toJS(orderedNode) : false

  var property = kb.any(form, ui('property'))
  if (!property) {
    box.appendChild(
      error.errorMessageBlock(dom, 'No property to multiple: ' + form)
    ) // used for arcs in the data
    return box
  }
  var min = kb.any(form, ui('min')) // This is the minimum number -- default 0
  min = min ? 0 + min.value : 0
  // var max = kb.any(form, ui('max')) // This is the minimum number
  // max = max ? max.value : 99999999

  var element = kb.any(form, ui('part')) // This is the form to use for each one
  if (!element) {
    box.appendChild(
      error.errorMessageBlock(dom, 'No part to multiple: ' + form)
    )
    return box
  }

  var body = box.appendChild(dom.createElement('tr')) // 20191207
  var list // The RDF collection which keeps the ordered version
  var values // Initial values - an array.  Even when no list yet.

  // var unsavedList = false // Flag that
  if (ordered) {
    list = kb.any(subject, property)
    if (list) {
      values = list.elements
    } else {
      // unsavedList = true
      values = []
    }
  } else {
    values = kb.each(subject, property)
    list = null
  }
  // Add control on the bottom for adding more items
  if (kb.updater.editable(store.uri)) {
    var tail = box.appendChild(dom.createElement('tr'))
    tail.style.padding = '0.5em'
    var img = tail.appendChild(dom.createElement('img'))
    img.setAttribute('src', plusIconURI) //  plus sign
    img.setAttribute('style', 'margin: 0.2em; width: 1.5em; height:1.5em')
    img.title = 'Click to add one or more ' + utils.label(property)
    var prompt = tail.appendChild(dom.createElement('span'))
    prompt.textContent =
      (values.length === 0 ? 'Add one or more ' : 'Add more ') +
      utils.label(property)
    tail.addEventListener('click', async _eventNotUsed => {
      await addItem()
    }, true)
  }

  function createListIfNecessary () {
    if (!list) {
      list = new $rdf.Collection()
      kb.add(subject, property, list, store)
    }
  }

  async function saveListThenRefresh () {
    console.log('save list: ' + debugString(list.elements)) // 20191214

    createListIfNecessary()
    try {
      await kb.fetcher.putBack(store)
    } catch (err) {
      box.appendChild(
        error.errorMessageBlock(dom, 'Error trying to put back a list: ' + err)
      )
      return
    }
    refresh()
  }

  function refresh () {
    let vals
    if (ordered) {
      const li = kb.the(subject, property)
      vals = li ? li.elements : []
    } else {
      vals = kb.each(subject, property)
      vals.sort() // achieve consistency on each refresh
    }
    utils.syncTableToArrayReOrdered(body, vals, renderItem)
  }
  body.refresh = refresh // Allow live update
  refresh()

  async function asyncStuff () {
    var extra = min - values.length
    if (extra > 0) {
      for (var j = 0; j < extra; j++) {
        console.log('Adding extra: min ' + min)
        await addItem() // Add blanks if less than minimum
      }
      await saveListThenRefresh()
    }
    // if (unsavedList) {
    //     await saveListThenRefresh() // async
    // }
  }
  asyncStuff().then(
    () => { console.log(' Multiple render: async stuff ok') },
    (err) => { console.error(' Multiple render: async stuff fails. #### ', err) }
  ) // async

  return box
} // Multiple

/*          Text field
 **
 */
// For possible date popups see e.g. http://www.dynamicdrive.com/dynamicindex7/jasoncalendar.htm
// or use HTML5: http://www.w3.org/TR/2011/WD-html-markup-20110113/input.date.html
//

forms.fieldParams = {}

forms.fieldParams[ns.ui('ColorField').uri] = {
  size: 9,
  type: 'color',
  dt: 'color'
} // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color
forms.fieldParams[
  ns.ui('ColorField').uri
].pattern = /^\s*#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]([0-9a-f][0-9a-f])?\s*$/

forms.fieldParams[ns.ui('DateField').uri] = {
  size: 20,
  type: 'date',
  dt: 'date'
}
forms.fieldParams[
  ns.ui('DateField').uri
].pattern = /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?Z?\s*$/

forms.fieldParams[ns.ui('DateTimeField').uri] = {
  size: 20,
  type: 'date',
  dt: 'dateTime'
}
forms.fieldParams[
  ns.ui('DateTimeField').uri
].pattern = /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?(T[0-2][0-9]:[0-5][0-9](:[0-5][0-9])?)?Z?\s*$/

forms.fieldParams[ns.ui('TimeField').uri] = {
  size: 10,
  type: 'time',
  dt: 'time'
}
forms.fieldParams[
  ns.ui('TimeField').uri
].pattern = /^\s*([0-2]?[0-9]:[0-5][0-9](:[0-5][0-9])?)\s*$/

forms.fieldParams[ns.ui('IntegerField').uri] = {
  size: 12,
  style: 'text-align: right',
  dt: 'integer'
}
forms.fieldParams[ns.ui('IntegerField').uri].pattern = /^\s*-?[0-9]+\s*$/

forms.fieldParams[ns.ui('DecimalField').uri] = {
  size: 12,
  style: 'text-align: right',
  dt: 'decimal'
}
forms.fieldParams[
  ns.ui('DecimalField').uri
].pattern = /^\s*-?[0-9]*(\.[0-9]*)?\s*$/

forms.fieldParams[ns.ui('FloatField').uri] = {
  size: 12,
  style: 'text-align: right',
  dt: 'float'
}
forms.fieldParams[
  ns.ui('FloatField').uri
].pattern = /^\s*-?[0-9]*(\.[0-9]*)?((e|E)-?[0-9]*)?\s*$/

forms.fieldParams[ns.ui('SingleLineTextField').uri] = {}
forms.fieldParams[ns.ui('NamedNodeURIField').uri] = { namedNode: true }
forms.fieldParams[ns.ui('TextField').uri] = {}

forms.fieldParams[ns.ui('PhoneField').uri] = { size: 20, uriPrefix: 'tel:' }
forms.fieldParams[ns.ui('PhoneField').uri].pattern = /^\+?[\d-]+[\d]*$/

forms.fieldParams[ns.ui('EmailField').uri] = {
  size: 30,
  uriPrefix: 'mailto:'
}
forms.fieldParams[ns.ui('EmailField').uri].pattern = /^\s*.*@.*\..*\s*$/ // @@ Get the right regexp here

/** Render a basic form field
 *
 ** @param {Document} dom The HTML Document object aka Document Object Model
 ** @param {Element?} container  If present, the created widget will be appended to this
 ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping
 ** @param {Node} subject The thing about which the form displays/edits data
 ** @param {Node} form The form or field to be rendered
 ** @param {Node} store The web document in which the data is
 ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed?
 **
 ** @returns {Element} The HTML widget created
 **
 ** The same function is used for many similar one-value fields, with different
 ** regexps used to validate.
 */
function basicField (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  const ui = UI.ns.ui
  const kb = UI.store

  var box = dom.createElement('tr')
  if (container) container.appendChild(box)
  var lhs = dom.createElement('td')
  lhs.setAttribute('class', 'formFieldName')
  lhs.setAttribute('style', '  vertical-align: middle;')
  box.appendChild(lhs)
  var rhs = dom.createElement('td')
  rhs.setAttribute('class', 'formFieldValue')
  box.appendChild(rhs)

  var property = kb.any(form, ui('property'))
  if (!property) {
    box.appendChild(
      dom.createTextNode('Error: No property given for text field: ' + form)
    )
    return box
  }
  lhs.appendChild(forms.fieldLabel(dom, property, form))
  var uri = forms.mostSpecificClassURI(form)
  var params = forms.fieldParams[uri]
  if (params === undefined) params = {} // non-bottom field types can do this
  var style = params.style || UI.style.textInputStyle || 'font-size: 100%; margin: 0.1em; padding: 0.1em;'
  // box.appendChild(dom.createTextNode(' uri='+uri+', pattern='+ params.pattern))
  var field = dom.createElement('input')
  field.style = UI.style.textInputStyle // Do we have to override length etc?
  rhs.appendChild(field)
  field.setAttribute('type', params.type ? params.type : 'text')

  var size = kb.any(form, ui('size')) // Form has precedence
  field.setAttribute(
    'size',
    size ? '' + size : params.size ? '' + params.size : '20'
  )
  var maxLength = kb.any(form, ui('maxLength'))
  field.setAttribute('maxLength', maxLength ? '' + maxLength : '4096')

  store = store || forms.fieldStore(subject, property, store)

  var obj = kb.any(subject, property, undefined, store)
  if (!obj) {
    obj = kb.any(form, ui('default'))
  }
  if (obj && obj.uri && params.uriPrefix) {
    // eg tel: or mailto:
    field.value = decodeURIComponent(obj.uri.replace(params.uriPrefix, '')) // should have no spaces but in case
      .replace(/ /g, '')
  } else if (obj) {
    field.value = obj.value || obj.uri || ''
  }
  field.setAttribute('style', style)

  if (!kb.updater.editable(store.uri)) {
    field.readonly = true // was: disabled. readonly is better
    return box
  }

  // read-write:
  field.addEventListener(
    'keyup',
    function (_e) {
      if (params.pattern) {
        field.setAttribute(
          'style',
          style +
            (field.value.match(params.pattern)
              ? 'color: green;'
              : 'color: red;')
        )
      }
    },
    true
  )
  field.addEventListener(
    'change',
    function (_e) {
      // i.e. lose focus with changed data
      if (params.pattern && !field.value.match(params.pattern)) return
      field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker.
      field.setAttribute('style', style + 'color: gray;') // pending
      var ds = kb.statementsMatching(subject, property) // remove any multiple values
      var result
      if (params.namedNode) {
        result = kb.sym(field.value)
      } else if (params.uriPrefix) {
        result = encodeURIComponent(field.value.replace(/ /g, ''))
        result = kb.sym(params.uriPrefix + field.value)
      } else {
        if (params.dt) {
          result = new $rdf.Literal(
            field.value.trim(),
            undefined,
            ns.xsd(params.dt)
          )
        } else {
          result = new $rdf.Literal(field.value)
        }
      }
      var is = ds.map(st => $rdf.st(st.subject, st.predicate, result, st.why)) // can include >1 doc
      if (is.length === 0) {
        // or none
        is = [$rdf.st(subject, property, result, store)]
      }

      function updateMany (ds, is, callback) {
        var docs = []
        is.forEach(st => {
          if (!docs.includes(st.why.uri)) docs.push(st.why.uri)
        })
        ds.forEach(st => {
          if (!docs.includes(st.why.uri)) docs.push(st.why.uri)
        })
        if (docs.length === 0) {
          throw new Error('updateMany has no docs to patch')
        }
        if (docs.length === 1) {
          return kb.updater.update(ds, is, callback)
        }
        // return kb.updater.update(ds, is, callback)

        const doc = docs.pop()
        const is1 = is.filter(st => st.why.uri === doc)
        const is2 = is.filter(st => st.why.uri !== doc)
        const ds1 = ds.filter(st => st.why.uri === doc)
        const ds2 = ds.filter(st => st.why.uri !== doc)
        kb.updater.update(ds1, is1, function (uri, ok, body) {
          if (ok) {
            updateMany(ds2, is2, callback)
          } else {
            console.log('Update many failed on: ' + doc)
            callback(uri, ok, body)
          }
        })
      }

      updateMany(ds, is, function (uri, ok, body) {
        // kb.updater.update(ds, is, function (uri, ok, body) {
        if (ok) {
          field.disabled = false
          field.setAttribute('style', style)
        } else {
          box.appendChild(error.errorMessageBlock(dom, body))
        }
        callbackFunction(ok, body)
      })
    },
    true
  )
  return box
}

forms.field[ns.ui('PhoneField').uri] = basicField
forms.field[ns.ui('EmailField').uri] = basicField
forms.field[ns.ui('ColorField').uri] = basicField
forms.field[ns.ui('DateField').uri] = basicField
forms.field[ns.ui('DateTimeField').uri] = basicField
forms.field[ns.ui('TimeField').uri] = basicField
forms.field[ns.ui('NumericField').uri] = basicField
forms.field[ns.ui('IntegerField').uri] = basicField
forms.field[ns.ui('DecimalField').uri] = basicField
forms.field[ns.ui('FloatField').uri] = basicField
forms.field[ns.ui('TextField').uri] = basicField
forms.field[ns.ui('SingleLineTextField').uri] = basicField
forms.field[ns.ui('NamedNodeURIField').uri] = basicField

/*          Multiline Text field
 **
 */

forms.field[ns.ui('MultiLineTextField').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  const ui = UI.ns.ui
  const kb = UI.store
  var property = kb.any(form, ui('property'))
  if (!property) {
    return error.errorMessageBlock(dom, 'No property to text field: ' + form)
  }
  const box = dom.createElement('div')
  box.appendChild(forms.fieldLabel(dom, property, form))
  store = forms.fieldStore(subject, property, store)
  var field = forms.makeDescription(
    dom,
    kb,
    subject,
    property,
    store,
    callbackFunction
  )
  // box.appendChild(dom.createTextNode('<-@@ subj:'+subject+', prop:'+property))
  box.appendChild(field)
  if (container) container.appendChild(box)
  return box
}

/*          Boolean field  and Tri-state version (true/false/null)
 **
 ** @@ todo: remove tristate param
 */
function booleanField (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction,
  tristate
) {
  const ui = UI.ns.ui
  const kb = UI.store
  var property = kb.any(form, ui('property'))
  if (!property) {
    const errorBlock = error.errorMessageBlock(
      dom,
      'No property to boolean field: ' + form
    )
    if (container) container.appendChild(errorBlock)
    return errorBlock
  }
  var lab = kb.any(form, ui('label'))
  if (!lab) lab = utils.label(property, true) // Init capital
  store = forms.fieldStore(subject, property, store)
  var state = kb.any(subject, property)
  if (state === undefined) {
    state = false
  } // @@ sure we want that -- or three-state?
  // UI.log.debug('store is '+store)
  var ins = $rdf.st(subject, property, true, store)
  var del = $rdf.st(subject, property, false, store)
  var box = buildCheckboxForm(dom, kb, lab, del, ins, form, store, tristate)
  if (container) container.appendChild(box)
  return box
}
forms.field[ns.ui('BooleanField').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  return booleanField(
    dom,
    container,
    already,
    subject,
    form,
    store,
    callbackFunction,
    false
  )
}

forms.field[ns.ui('TristateField').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  return booleanField(
    dom,
    container,
    already,
    subject,
    form,
    store,
    callbackFunction,
    true
  )
}

/*          Classifier field
 **
 **  Nested categories
 **
 ** @@ To do: If a classification changes, then change any dependent Options fields.
 */

forms.field[ns.ui('Classifier').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  const kb = UI.store
  const ui = UI.ns.ui
  var category = kb.any(form, ui('category'))
  if (!category) {
    return error.errorMessageBlock(dom, 'No category for classifier: ' + form)
  }
  UI.log.debug('Classifier: store=' + store)
  var checkOptions = function (ok, body) {
    if (!ok) return callbackFunction(ok, body)

    /*
    var parent = kb.any(undefined, ui('part'), form)
    if (!parent) return callbackFunction(ok, body)
    var kids = kb.each(parent, ui('part')); // @@@@@@@@@ Garbage
    kids = kids.filter(function(k){return kb.any(k, ns.rdf('type'), ui('Options'))})
    if (kids.length) UI.log.debug('Yes, found related options: '+kids[0])
    */
    return callbackFunction(ok, body)
  }
  var box = forms.makeSelectForNestedCategory(
    dom,
    kb,
    subject,
    category,
    store,
    checkOptions
  )
  if (container) container.appendChild(box)
  return box
}

/**         Choice field
 **
 **  Not nested.  Generates a link to something from a given class.
 **  Optional subform for the thing selected.
 **  Alternative implementatons caould be:
 ** -- pop-up menu (as here)
 ** -- radio buttons
 ** -- auto-complete typing
 **
 ** Todo: Deal with multiple.  Maybe merge with multiple code.
 */

forms.field[ns.ui('Choice').uri] = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  callbackFunction
) {
  var ns = UI.ns
  const ui = UI.ns.ui
  const kb = UI.store
  var multiple = false
  var p
  var box = dom.createElement('tr')
  if (container) container.appendChild(box)
  var lhs = dom.createElement('td')
  box.appendChild(lhs)
  var rhs = dom.createElement('td')
  box.appendChild(rhs)
  var property = kb.any(form, ui('property'))
  if (!property) {
    return error.errorMessageBlock(dom, 'No property for Choice: ' + form)
  }
  lhs.appendChild(forms.fieldLabel(dom, property, form))
  var from = kb.any(form, ui('from'))
  if (!from) {
    return error.errorMessageBlock(dom, "No 'from' for Choice: " + form)
  }
  var subForm = kb.any(form, ui('use')) // Optional
  var possible = []
  var possibleProperties
  var np = '--' + utils.label(property) + '-?'
  var opts = { multiple: multiple, nullLabel: np, disambiguate: false }
  possible = kb.each(undefined, ns.rdf('type'), from)
  for (var x in kb.findMembersNT(from)) {
    possible.push(kb.fromNT(x))
    // box.appendChild(dom.createTextNode("RDFS: adding "+x))
  } // Use rdfs
  // UI.log.debug("%%% Choice field: possible.length 1 = "+possible.length)
  if (from.sameTerm(ns.rdfs('Class'))) {
    for (p in buttons.allClassURIs()) possible.push(kb.sym(p))
    // UI.log.debug("%%% Choice field: possible.length 2 = "+possible.length)
  } else if (from.sameTerm(ns.rdf('Property'))) {
    possibleProperties = buttons.propertyTriage(kb)
    for (p in possibleProperties.op) possible.push(kb.fromNT(p))
    for (p in possibleProperties.dp) possible.push(kb.fromNT(p))
    opts.disambiguate = true // This is a big class, and the labels won't be enough.
  } else if (from.sameTerm(ns.owl('ObjectProperty'))) {
    possibleProperties = buttons.propertyTriage(kb)
    for (p in possibleProperties.op) possible.push(kb.fromNT(p))
    opts.disambiguate = true
  } else if (from.sameTerm(ns.owl('DatatypeProperty'))) {
    possibleProperties = buttons.propertyTriage(kb)
    for (p in possibleProperties.dp) possible.push(kb.fromNT(p))
    opts.disambiguate = true
  }
  var object = kb.any(subject, property)
  function addSubForm () {
    object = kb.any(subject, property)
    forms.fieldFunction(dom, subForm)(
      dom,
      rhs,
      already,
      object,
      subForm,
      store,
      callbackFunction
    )
  }
  // box.appendChild(dom.createTextNode('Choice: subForm='+subForm))
  var possible2 = forms.sortByLabel(possible)
  if (kb.any(form, ui('canMintNew'))) {
    opts.mint = '* New *' // @@ could be better
    opts.subForm = subForm
  }
  var selector = forms.makeSelectForOptions(
    dom,
    kb,
    subject,
    property,
    possible2,
    opts,
    store,
    callbackFunction
  )
  rhs.appendChild(selector)
  if (object && subForm) addSubForm()
  return box
}

//          Documentation - non-interactive fields
//

forms.fieldParams[ns.ui('Comment').uri] = {
  element: 'p',
  style: `padding: 0.1em 1.5em; color: ${UI.style.formHeadingColor}; white-space: pre-wrap;`
}
forms.fieldParams[ns.ui('Heading').uri] = {
  element: 'h3',
  style: `font-size: 110%; color: ${UI.style.formHeadingColor};`
}

forms.field[ns.ui('Comment').uri] = forms.field[
  ns.ui('Heading').uri
] = function (
  dom,
  container,
  already,
  subject,
  form,
  _store,
  _callbackFunction
) {
  const ui = UI.ns.ui
  const kb = UI.store
  var contents = kb.any(form, ui('contents'))
  if (!contents) contents = 'Error: No contents in comment field.'

  var uri = forms.mostSpecificClassURI(form)
  var params = forms.fieldParams[uri]
  if (params === undefined) {
    params = {}
  } // non-bottom field types can do this

  var box = dom.createElement('div')
  if (container) container.appendChild(box)
  var p = box.appendChild(dom.createElement(params.element))
  p.textContent = contents

  var style = kb.any(form, ui('style'))
  if (style === undefined) {
    style = params.style ? params.style : ''
  }
  if (style) p.setAttribute('style', style)

  return box
}

/// ////////////// Form-related functions

/** Which class of field is this?
 * @param x a field
 * @returns the URI of the most specific class
 */

forms.mostSpecificClassURI = function (x) {
  const kb = UI.store
  var ft = kb.findTypeURIs(x)
  var bot = kb.bottomTypeURIs(ft) // most specific
  var bots = []
  for (var b in bot) bots.push(b)
  // if (bots.length > 1) throw "Didn't expect "+x+" to have multiple bottom types: "+bots
  return bots[0]
}

forms.fieldFunction = function (dom, field) {
  const uri = forms.mostSpecificClassURI(field) // What type
  // const uri = field.uri
  var fun = forms.field[uri]
  UI.log.debug(
    'paneUtils: Going to implement field ' + field + ' of type ' + uri
  )
  if (!fun) {
    return function () {
      return error.errorMessageBlock(
        dom,
        'No handler for field ' + field + ' of type ' + uri
      )
    }
  }
  return fun
}

// A button for editing a form (in place, at the moment)
//
//  When editing forms, make it yellow, when editing thr form form, pink
// Help people understand how many levels down they are.
//
forms.editFormButton = function (
  dom,
  container,
  form,
  store,
  callbackFunction
) {
  var b = dom.createElement('button')
  b.setAttribute('type', 'button')
  b.innerHTML = 'Edit ' + utils.label(ns.ui('Form'))
  b.addEventListener(
    'click',
    function (_e) {
      var ff = forms.appendForm(
        dom,
        container,
        {},
        form,
        ns.ui('FormForm'),
        store,
        callbackFunction
      )
      ff.setAttribute(
        'style',
        ns.ui('FormForm').sameTerm(form)
          ? 'background-color: #fee;'
          : 'background-color: #ffffe7;'
      )
      b.parentNode.removeChild(b)
    },
    true
  )
  return b
}

forms.appendForm = function (
  dom,
  container,
  already,
  subject,
  form,
  store,
  itemDone
) {
  return forms.fieldFunction(dom, form)(
    dom,
    container,
    already,
    subject,
    form,
    store,
    itemDone
  )
}

/**          Find list of properties for class
//
// Three possible sources: Those mentioned in schemas, which exludes many
// those which occur in the data we already have, and those predicates we
// have come across anywhere and which are not explicitly excluded from
// being used with this class.
*/

forms.propertiesForClass = function (kb, c) {
  var ns = UI.ns
  var explicit = kb.each(undefined, ns.rdf('range'), c)
  ;[
    ns.rdfs('comment'),
    ns.dc('title'), // Generic things
    ns.foaf('name'),
    ns.foaf('homepage')
  ].map(function (x) {
    explicit.push(x)
  })
  var members = kb.each(undefined, ns.rdf('type'), c)
  if (members.length > 60) members = members.slice(0, 60) // Array supports slice?
  var used = {}
  for (var i = 0; i < (members.length > 60 ? 60 : members.length); i++) {
    kb.statementsMatching(members[i], undefined, undefined).map(function (st) {
      used[st.predicate.uri] = true
    })
  }
  explicit.map(function (p) {
    used[p.uri] = true
  })
  var result = []
  for (var uri in used) {
    result.push(kb.sym(uri))
  }
  return result
}

/** Find the closest class
* @param kb The store
* @param cla - the URI of the class
* @param prop
*/
forms.findClosest = function findClosest (kb, cla, prop) {
  var agenda = [kb.sym(cla)] // ordered - this is breadth first search
  while (agenda.length > 0) {
    var c = agenda.shift() // first
    // if (c.uri && (c.uri == ns.owl('Thing').uri || c.uri == ns.rdf('Resource').uri )) continue
    var lists = kb.each(c, prop)
    UI.log.debug('Lists for ' + c + ', ' + prop + ': ' + lists.length)
    if (lists.length !== 0) return lists
    var supers = kb.each(c, ns.rdfs('subClassOf'))
    for (var i = 0; i < supers.length; i++) {
      agenda.push(supers[i])
      UI.log.debug('findClosest: add super: ' + supers[i])
    }
  }
  return []
}

// Which forms apply to a given existing subject?

forms.formsFor = function (subject) {
  var ns = UI.ns
  const kb = UI.store

  UI.log.debug('formsFor: subject=' + subject)
  var t = kb.findTypeURIs(subject)
  var t1
  for (t1 in t) {
    UI.log.debug('   type: ' + t1)
  }
  var bottom = kb.bottomTypeURIs(t) // most specific
  var candidates = []
  for (var b in bottom) {
    // Find the most specific
    UI.log.debug('candidatesFor: trying bottom type =' + b)
    candidates = candidates.concat(
      forms.findClosest(kb, b, ns.ui('creationForm'))
    )
    candidates = candidates.concat(
      forms.findClosest(kb, b, ns.ui('annotationForm'))
    )
  }
  return candidates
}

forms.sortBySequence = function (list) {
  var p2 = list.map(function (p) {
    var k = UI.store.any(p, ns.ui('sequence'))
    return [k || 9999, p]
  })
  p2.sort(function (a, b) {
    return a[0] - b[0]
  })
  return p2.map(function (pair) {
    return pair[1]
  })
}

forms.sortByLabel = function (list) {
  var p2 = list.map(function (p) {
    return [utils.label(p).toLowerCase(), p]
  })
  p2.sort()
  return p2.map(function (pair) {
    return pair[1]
  })
}

/** Button to add a new whatever using a form
//
// @param form - optional form , else will look for one
// @param store - optional store else will prompt for one (unimplemented)
*/
forms.newButton = function (
  dom,
  kb,
  subject,
  predicate,
  theClass,
  form,
  store,
  callbackFunction
) {
  var b = dom.createElement('button')
  b.setAttribute('type', 'button')
  b.innerHTML = 'New ' + utils.label(theClass)
  b.addEventListener(
    'click',
    function (_e) {
      b.parentNode.appendChild(
        forms.promptForNew(
          dom,
          kb,
          subject,
          predicate,
          theClass,
          form,
          store,
          callbackFunction
        )
      )
    },
    false
  )
  return b
}

/**      Prompt for new object of a given class
//
// @param dom - the document DOM for the user interface
// @param kb - the graph which is the knowledge base we are working with
// @param subject - a term, Thing this should be linked to when made. Optional.
// @param predicate - a term, the relationship for the subject link. Optional.
// @param theClass - an RDFS class containng the object about which the new information is.
// @param form  - the form to be used when a new one. null means please find one.
// @param store - The web document being edited
// @param callbackFunction - takes (boolean ok, string errorBody)
// @returns a dom object with the form DOM
*/
forms.promptForNew = function (
  dom,
  kb,
  subject,
  predicate,
  theClass,
  form,
  store,
  callbackFunction
) {
  var ns = UI.ns
  var box = dom.createElement('form')

  if (!form) {
    var lists = forms.findClosest(kb, theClass.uri, UI.ns.ui('creationForm'))
    if (lists.length === 0) {
      var p = box.appendChild(dom.createElement('p'))
      p.textContent =
        'I am sorry, you need to provide information about a ' +
        utils.label(theClass) +
        " but I don't know enough information about those to ask you."
      var b = box.appendChild(dom.createElement('button'))
      b.setAttribute('type', 'button')
      b.setAttribute('style', 'float: right;')
      b.innerHTML = 'Goto ' + utils.label(theClass)
      b.addEventListener(
        'click',
        function (_e) {
          dom.outlineManager.GotoSubject(
            theClass,
            true,
            undefined,
            true,
            undefined
          )
        },
        false
      )
      return box
    }
    UI.log.debug('lists[0] is ' + lists[0])
    form = lists[0] // Pick any one
  }
  UI.log.debug('form is ' + form)
  box.setAttribute('style', `border: 0.05em solid ${UI.style.formBorderColor}; color: ${UI.style.formBorderColor}`) // @@color?
  box.innerHTML = '<h3>New ' + utils.label(theClass) + '</h3>'

  var formFunction = forms.fieldFunction(dom, form)
  var object = forms.newThing(store)
  var gotButton = false
  var itemDone = function (ok, body) {
    if (!ok) return callbackFunction(ok, body)
    var insertMe = []
    if (subject && !kb.holds(subject, predicate, object, store)) {
      insertMe.push($rdf.st(subject, predicate, object, store))
    }
    if (subject && !kb.holds(object, ns.rdf('type'), theClass, store)) {
      insertMe.push($rdf.st(object, ns.rdf('type'), theClass, store))
    }
    if (insertMe.length) {
      UI.store.updater.update([], insertMe, linkDone)
    } else {
      callbackFunction(true, body)
    }
    if (!gotButton) {
      gotButton = box.appendChild(forms.linkButton(dom, object))
    }
    // tabulator.outline.GotoSubject(object, true, undefined, true, undefined)
  }
  function linkDone (uri, ok, body) {
    return callbackFunction(ok, body)
  }
  UI.log.info('paneUtils Object is ' + object)
  var f = formFunction(dom, box, {}, object, form, store, itemDone)
  var rb = forms.removeButton(dom, f)
  rb.setAttribute('style', 'float: right;')
  box.AJAR_subject = object
  return box
}

forms.makeDescription = function (
  dom,
  kb,
  subject,
  predicate,
  store,
  callbackFunction
) {
  var group = dom.createElement('div')

  var sts = kb.statementsMatching(subject, predicate, null, store) // Only one please
  if (sts.length > 1) {
    return error.errorMessageBlock(
      dom,
      'Should not be ' + sts.length + ' i.e. >1 ' + predicate + ' of ' + subject
    )
  }
  var desc = sts.length ? sts[0].object.value : undefined

  var field = dom.createElement('textarea')
  group.appendChild(field)
  field.rows = desc ? desc.split('\n').length + 2 : 2
  field.cols = 80
  var style = UI.style.multilineTextInputStyle ||
    'font-size:100%; white-space: pre-wrap; background-color: white;' +
    ' border: 0.07em solid gray; padding: 1em 0.5em; margin: 1em 1em;'
  field.setAttribute('style', style)
  if (sts.length) {
    field.value = desc
  } else {
    // Unless you can make the predicate label disappear with the first click then this is over-cute
    // field.value = utils.label(predicate); // Was"enter a description here"
    field.select() // Select it ready for user input -- doesn't work
  }

  group.refresh = function () {
    var v = kb.any(subject, predicate, null, store)
    if (v && v.value !== field.value) {
      field.value = v.value // don't touch widget if no change
      // @@ this is the place to color the field from the user who chanaged it
    }
  }
  function saveChange (_e) {
    submit.disabled = true
    submit.setAttribute('style', 'visibility: hidden; float: right;') // Keep UI clean
    field.disabled = true
    field.setAttribute('style', style + 'color: gray;') // pending
    var ds = kb.statementsMatching(subject, predicate, null, store)
    var is = $rdf.st(subject, predicate, field.value, store)
    UI.store.updater.update(ds, is, function (uri, ok, body) {
      if (ok) {
        field.setAttribute('style', style + 'color: black;')
        field.disabled = false
      } else {
        group.appendChild(
          error.errorMessageBlock(
            dom,
            'Error (while saving change to ' + store.uri + '): ' + body
          )
        )
      }
      if (callbackFunction) {
        callbackFunction(ok, body)
      }
    })
  }

  var br = dom.createElement('br')
  group.appendChild(br)

  var editable = UI.store.updater.editable(store.uri)
  if (editable) {
    var submit = dom.createElement('input')
    submit.setAttribute('type', 'submit')
    submit.disabled = true // until the filled has been modified
    submit.setAttribute('style', 'visibility: hidden; float: right;') // Keep UI clean
    submit.value = 'Save ' + utils.label(predicate) // @@ I18n
    group.appendChild(submit)

    field.addEventListener(
      'keyup',
      function (_e) {
        // Green means has been changed, not saved yet
        field.setAttribute('style', style + 'color: green;')
        if (submit) {
          submit.disabled = false
          submit.setAttribute('style', 'float: right;') // Remove visibility: hidden
        }
      },
      true
    )
    field.addEventListener('change', saveChange, true)
    submit.addEventListener('click', saveChange, false)
  } else {
    field.disabled = true
  }
  return group
}

/** Make SELECT element to select options
//
// @param subject - a term, the subject of the statement(s) being edited.
// @param predicate - a term, the predicate of the statement(s) being edited
// @param possible - a list of terms, the possible value the object can take
// @param options.multiple - Boolean - Whether more than one at a time is allowed
// @param options.nullLabel - a string to be displayed as the
//                        option for none selected (for non multiple)
// @param options.mint - User may create thing if this sent to the prompt string eg "New foo"
// @param options.subForm - If mint, then the form to be used for minting the new thing
// @param store - The web document being edited
// @param callbackFunction - takes (boolean ok, string errorBody)
*/
forms.makeSelectForOptions = function (
  dom,
  kb,
  subject,
  predicate,
  possible,
  options,
  store,
  callbackFunction
) {
  UI.log.debug('Select list length now ' + possible.length)
  var n = 0
  var uris = {} // Count them
  var editable = UI.store.updater.editable(store.uri)

  for (var i = 0; i < possible.length; i++) {
    var sub = possible[i] // @@ Maybe; make this so it works with blank nodes too
    if (!sub.uri) console.warn(`makeSelectForOptions: option does not have an uri: ${sub}, with predicate: ${predicate}`)
    if (!sub.uri || sub.uri in uris) continue
    uris[sub.uri] = true
    n++
  } // uris is now the set of possible options
  if (n === 0 && !options.mint) {
    return error.errorMessageBlock(
      dom,
      "Can't do selector with no options, subject= " +
        subject +
        ' property = ' +
        predicate +
        '.'
    )
  }
  UI.log.debug('makeSelectForOptions: store=' + store)

  var getActual = function () {
    actual = {}
    if (predicate.sameTerm(ns.rdf('type'))) {
      actual = kb.findTypeURIs(subject)
    } else {
      kb.each(subject, predicate, null, store).map(function (x) {
        actual[x.uri] = true
      })
    }
    return actual
  }
  var actual = getActual()

  // var newObject = null

  var onChange = function (_e) {
    select.disabled = true // until data written back - gives user feedback too
    var ds = []
    var is = []
    var removeValue = function (t) {
      if (kb.holds(subject, predicate, t, store)) {
        ds.push($rdf.st(subject, predicate, t, store))
      }
    }
    for (var i = 0; i < select.options.length; i++) {
      var opt = select.options[i]
      if (opt.selected && opt.AJAR_mint) {
        var newObject
        if (options.mintClass) {
          var thisForm = forms.promptForNew(
            dom,
            kb,
            subject,
            predicate,
            options.mintClass,
            null,
            store,
            function (ok, body) {
              if (!ok) {
                callbackFunction(ok, body) // @@ if ok, need some form of refresh of the select for the new thing
              }
            }
          )
          select.parentNode.appendChild(thisForm)
          newObject = thisForm.AJAR_subject
        } else {
          newObject = forms.newThing(store)
        }
        is.push($rdf.st(subject, predicate, newObject, store))
        if (options.mintStatementsFun) {
          is = is.concat(options.mintStatementsFun(newObject))
        }
      }
      if (!opt.AJAR_uri) continue // a prompt or mint
      if (opt.selected && !(opt.AJAR_uri in actual)) {
        // new class
        is.push($rdf.st(subject, predicate, kb.sym(opt.AJAR_uri), store))
      }
      if (!opt.selected && opt.AJAR_uri in actual) {
        // old class
        removeValue(kb.sym(opt.AJAR_uri))
        // ds.push($rdf.st(subject, predicate, kb.sym(opt.AJAR_uri), store ))
      }
      if (opt.selected) select.currentURI = opt.AJAR_uri
    }
    var sel = select.subSelect // All subclasses must also go
    while (sel && sel.currentURI) {
      removeValue(kb.sym(sel.currentURI))
      sel = sel.subSelect
    }
    sel = select.superSelect // All superclasses are redundant
    while (sel && sel.currentURI) {
      removeValue(kb.sym(sel.currentURI))
      sel = sel.superSelect
    }
    function doneNew (ok, body) {
      callbackFunction(ok, body)
    }
    UI.log.info('selectForOptions: stote = ' + store)
    UI.store.updater.update(ds, is, function (uri, ok, body) {
      actual = getActual() // refresh
      // kb.each(subject, predicate, null, store).map(function(x){actual[x.uri] = true})
      if (ok) {
        select.disabled = false // data written back
        if (newObject) {
          var fn = forms.fieldFunction(dom, options.subForm)
          fn(
            dom,
            select.parentNode,
            {},
            newObject,
            options.subForm,
            store,
            doneNew
          )
        }
      }
      if (callbackFunction) callbackFunction(ok, body)
    })
  }

  var select = dom.createElement('select')
  select.setAttribute('style', 'margin: 0.6em 1.5em;')
  if (options.multiple) select.setAttribute('multiple', 'true')
  select.currentURI = null

  select.refresh = function () {
    actual = getActual() // refresh
    for (var i = 0; i < select.children.length; i++) {
      var option = select.children[i]
      if (option.AJAR_uri) {
        option.selected = option.AJAR_uri in actual
      }
    }
    select.disabled = false // unlocked any conflict we had got into
  }

  for (var uri in uris) {
    var c = kb.sym(uri)
    var option = dom.createElement('option')
    if (options.disambiguate) {
      option.appendChild(dom.createTextNode(utils.labelWithOntology(c, true))) // Init. cap
    } else {
      option.appendChild(dom.createTextNode(utils.label(c, true))) // Init.
    }
    var backgroundColor = kb.any(
      c,
      kb.sym('http://www.w3.org/ns/ui#backgroundColor')
    )
    if (backgroundColor) {
      option.setAttribute(
        'style',
        'background-color: ' + backgroundColor.value + '; '
      )
    }
    option.AJAR_uri = uri
    if (uri in actual) {
      option.setAttribute('selected', 'true')
      select.currentURI = uri
      // dump("Already in class: "+ uri+"\n")
    }
    select.appendChild(option)
  }
  if (editable && options.mint) {
    var mint = dom.createElement('option')
    mint.appendChild(dom.createTextNode(options.mint))
    mint.AJAR_mint = true // Flag it
    select.insertBefore(mint, select.firstChild)
  }
  if (select.currentURI == null && !options.multiple) {
    var prompt = dom.createElement('option')
    prompt.appendChild(dom.createTextNode(options.nullLabel))
    select.insertBefore(prompt, select.firstChild)
    prompt.selected = true
  }
  if (editable) {
    select.addEventListener('change', onChange, false)
  }
  return select
} // makeSelectForOptions

// Make SELECT element to select subclasses
//
// If there is any disjoint union it will so a mutually exclusive dropdown
// Failing that it will do a multiple selection of subclasses.
// Callback takes (boolean ok, string errorBody)

forms.makeSelectForCategory = function (
  dom,
  kb,
  subject,
  category,
  store,
  callbackFunction
) {
  var log = UI.log
  var du = kb.any(category, ns.owl('disjointUnionOf'))
  var subs
  var multiple = false
  if (!du) {
    subs = kb.each(undefined, ns.rdfs('subClassOf'), category)
    multiple = true
  } else {
    subs = du.elements
  }
  log.debug('Select list length ' + subs.length)
  if (subs.length === 0) {
    return error.errorMessageBlock(
      dom,
      "Can't do " +
        (multiple ? 'multiple ' : '') +
        'selector with no subclasses of category: ' +
        category
    )
  }
  if (subs.length === 1) {
    return error.errorMessageBlock(
      dom,
      "Can't do " +
        (multiple ? 'multiple ' : '') +
        'selector with only 1 subclass of category: ' +
        category +
        ':' +
        subs[1]
    )
  }
  return forms.makeSelectForOptions(
    dom,
    kb,
    subject,
    ns.rdf('type'),
    subs,
    { multiple: multiple, nullPrompt: '--classify--' },
    store,
    callbackFunction
  )
}

/** Make SELECT element to select subclasses recurively
//
// It will so a mutually exclusive dropdown, with another if there are nested
// disjoint unions.
//
// @param  callbackFunction takes (boolean ok, string errorBody)
*/
forms.makeSelectForNestedCategory = function (
  dom,
  kb,
  subject,
  category,
  store,
  callbackFunction
) {
  var container = dom.createElement('span') // Container
  var child = null
  var select
  var onChange = function (ok, body) {
    if (ok) update()
    callbackFunction(ok, body)
  }
  select = forms.makeSelectForCategory(
    dom,
    kb,
    subject,
    category,
    store,
    onChange
  )
  container.appendChild(select)
  var update = function () {
    // UI.log.info("Selected is now: "+select.currentURI)
    if (child) {
      container.removeChild(child)
      child = null
    }
    if (
      select.currentURI &&
      kb.any(kb.sym(select.currentURI), ns.owl('disjointUnionOf'))
    ) {
      child = forms.makeSelectForNestedCategory(
        dom,
        kb,
        subject,
        kb.sym(select.currentURI),
        store,
        callbackFunction
      )
      select.subSelect = child.firstChild
      select.subSelect.superSelect = select
      container.appendChild(child)
    }
  }
  update()
  return container
}

/*  Build a checkbox from a given statement(s)
 **
 **  If the source document is editable, make the checkbox editable
 **
 **  ins and sel are either statements *or arrays of statements* which should be
 **  made if the checkbox is checed and unchecked respectively.
 **  tristate: Allow ins, or del, or neither
 */
function buildCheckboxForm (dom, kb, lab, del, ins, form, store, tristate) {
  // 20190115
  var box = dom.createElement('div')
  var tx = dom.createTextNode(lab)
  var editable = UI.store.updater.editable(store.uri)
  tx.style =
    'colour: black; font-size: 100%; padding-left: 0.5 em; padding-right: 0.5 em;'
  box.appendChild(tx)
  var input
  input = dom.createElement('button')

  input.setAttribute(
    'style',
    'font-size: 150%; height: 1.2em; width: 1.2em; background-color: #eef; margin: 0.1em'
  )
  box.appendChild(input)

  function fix (x) {
    if (!x) return [] // no statements
    if (x.object) {
      if (!x.why) {
        x.why = store // be back-compaitible  with old code
      }
      return [x] // one statements
    }
    if (x instanceof Array) return x
    throw new Error('buildCheckboxForm: bad param ' + x)
  }
  ins = fix(ins)
  del = fix(del)

  function holdsAll (a) {
    const missing = a.filter(
      st => !kb.holds(st.subject, st.predicate, st.object, st.why)
    )
    return missing.length === 0
  }
  function refresh () {
    var state = holdsAll(ins)
    var displayState = state
    if (del.length) {
      var negation = holdsAll(del)
      if (state && negation) {
        box.appendChild(
          UI.widgets.errorMessageBlock(
            dom,
            'Inconsistent data in store!\n' + ins + ' and\n' + del
          )
        )
        return box
      }
      if (!state && !negation) {
        state = null
        const defa = kb.any(form, ns.ui('default'))
        displayState = defa ? defa.value === '1' : tristate ? null : false
      }
    }
    input.state = state
    input.textContent = {
      true: checkMarkCharacter,
      false: cancelCharacter,
      null: dashCharacter
    }[displayState]
  }

  refresh()
  if (!editable) return box

  var boxHandler = function (_e) {
    tx.style = 'color: #bbb;' // grey -- not saved yet
    var toDelete = input.state === true ? ins : input.state === false ? del : []
    input.newState =
      input.state === null
        ? true
        : input.state === true
          ? false
          : tristate
            ? null
            : true

    var toInsert =
      input.newState === true ? ins : input.newState === false ? del : []
    console.log(`  Deleting  ${toDelete}`)
    console.log(`  Inserting ${toInsert}`)
    UI.store.updater.update(toDelete, toInsert, function (
      uri,
      success,
      errorBody
    ) {
      if (!success) {
        if (toDelete.why) {
          var hmmm = kb.holds(
            toDelete.subject,
            toDelete.predicate,
            toDelete.object,
            toDelete.why
          )
          if (hmmm) {
            console.log(' @@@@@ weird if 409 - does hold statement')
          }
        }
        tx.style = 'color: #black; background-color: #fee;'
        box.appendChild(
          error.errorMessageBlock(
            dom,
            `Checkbox: Error updating store from ${input.state} to ${
              input.newState
            }:\n\n${errorBody}`
          )
        )
      } else {
        tx.style = 'color: #black;'
        input.state = input.newState
        input.textContent = {
          true: checkMarkCharacter,
          false: cancelCharacter,
          null: dashCharacter
        }[input.state] // @@
      }
    })
  }
  input.addEventListener('click', boxHandler, false)
  return box
}
forms.buildCheckboxForm = buildCheckboxForm

forms.fieldLabel = function (dom, property, form) {
  var lab = UI.store.any(form, ns.ui('label'))
  if (!lab) lab = utils.label(property, true) // Init capital
  if (property === undefined) {
    return dom.createTextNode('@@Internal error: undefined property')
  }
  var anchor = dom.createElement('a')
  if (property.uri) anchor.setAttribute('href', property.uri)
  anchor.setAttribute('style', 'color: #3B5998; text-decoration: none;') // Not too blue and no underline
  anchor.textContent = lab
  return anchor
}

forms.fieldStore = function (subject, predicate, def) {
  var sts = UI.store.statementsMatching(subject, predicate)
  if (sts.length === 0) return def // can used default as no data yet
  if (
    sts.length > 0 &&
    sts[0].why.uri &&
    UI.store.updater.editable(sts[0].why.uri, UI.store)
  ) {
    return UI.store.sym(sts[0].why.uri)
  }
  return def
}

/** Mint local ID using timestamp
 * @param {NamedNode} doc - the document in which the ID is to be generated
 */
forms.newThing = function (doc) {
  var now = new Date()
  return $rdf.sym(doc.uri + '#' + 'id' + ('' + now.getTime()))
}

module.exports = forms