michielbdejong/solid-ui

View on GitHub
src/pad.js

Summary

Maintainability
F
1 wk
Test Coverage
/** **************
 *   Notepad Widget
 */

/** @module UI.pad
 */

const $rdf = require('rdflib')
var padModule = (module.exports = {})
var UI = {
  authn: require('./authn/authn'),
  icons: require('./iconBase'),
  log: require('./log'),
  ns: require('./ns'),
  pad: padModule,
  rdf: $rdf,
  store: require('./store'),
  widgets: require('./widgets')
}
const kb = UI.store
const ns = UI.ns

const utils = require('./utils')

/** Figure out a random color from my webid
 *
 * @param {NamedNode} author - The author of text being displayed
 * @returns {String} The CSS color generated, constrained to be light for a background color
 */
UI.pad.lightColorHash = function (author) {
  var hash = function (x) {
    return x.split('').reduce(function (a, b) {
      a = (a << 5) - a + b.charCodeAt(0)
      return a & a
    }, 0)
  }
  return author && author.uri
    ? '#' + ((hash(author.uri) & 0xffffff) | 0xc0c0c0).toString(16)
    : '#ffffff' // c0c0c0  forces pale
} // no id -> white

// Manage participation in this session
//
//  This is more general tham the pad.
//
UI.pad.renderPartipants = function (dom, table, padDoc, subject, me, options) {
  table.setAttribute('style', 'margin: 0.8em;')

  var newRowForParticpation = function (parp) {
    var person = kb.any(parp, ns.wf('participant'))
    var tr
    if (!person) {
      tr = dom.createElement('tr')
      tr.textContent = '???' // Don't crash - invalid part'n entry
      return tr
    }
    var bg = kb.anyValue(parp, ns.ui('backgroundColor')) || 'white'

    var block = dom.createElement('div')
    block.setAttribute(
      'style',
      'height: 1.5em; width: 1.5em; margin: 0.3em; border 0.01em solid #888; background-color: ' +
        bg
    )
    tr = UI.widgets.personTR(dom, null, person, options)
    table.appendChild(tr)
    var td = dom.createElement('td')
    td.setAttribute('style', 'vertical-align: middle;')
    td.appendChild(block)
    tr.insertBefore(td, tr.firstChild)
    return tr
  }

  var syncTable = function () {
    var parps = kb.each(subject, ns.wf('participation')).map(function (parp) {
      return [kb.anyValue(parp, UI.ns.cal('dtstart')) || '9999-12-31', parp]
    })
    parps.sort() // List in order of joining
    var participations = parps.map(function (p) {
      return p[1]
    })
    utils.syncTableToArray(table, participations, newRowForParticpation)
  }
  table.refresh = syncTable
  syncTable()
  return table
}

/** Record, or find old, Particpation object
 *
 * A particpaption object is a place to record things specifically about
 * subject and the user, such as preferences, start of membership, etc
 * @param {Node} subject - The thing in which the participation is happening
 * @param {NamedNode} document -  Where to record the data
 * @param {NamedNode} me - The logged in user
 *
 */
UI.pad.participationObject = function (subject, padDoc, me) {
  return new Promise(function (resolve, reject) {
    if (!me) {
      throw new Error('Not user id')
    }

    var parps = kb.each(subject, ns.wf('participation')).filter(function (pn) {
      return kb.holds(pn, ns.wf('participant'), me)
    })
    if (parps.length > 1) {
      throw new Error('Multiple records of your participation')
    }
    if (parps.length) {
      // If I am not already recorded
      resolve(parps[0]) // returns the particpation object
    } else {
      var participation = UI.widgets.newThing(padDoc)
      var ins = [
        UI.rdf.st(subject, ns.wf('participation'), participation, padDoc),

        UI.rdf.st(participation, ns.wf('participant'), me, padDoc),
        UI.rdf.st(participation, ns.cal('dtstart'), new Date(), padDoc),
        UI.rdf.st(
          participation,
          ns.ui('backgroundColor'),
          UI.pad.lightColorHash(me),
          padDoc
        )
      ]
      kb.updater.update([], ins, function (uri, ok, errorMessage) {
        if (!ok) {
          reject(new Error('Error recording your partipation: ' + errorMessage))
        } else {
          resolve(participation)
        }
      })
      resolve(participation)
    }
  })
}

/** Record my participation and display participants
 *
 * @param {NamedNode} subject - the thing in which participation is happening
 * @param {NamedNode} padDoc - The document into which the particpation should be recorded
 * @param {DOMNode} refreshable - A DOM element whose refresh() is to be called if the change works
 *
 */
UI.pad.recordParticipation = function (subject, padDoc, refreshable) {
  var me = UI.authn.currentUser()
  if (!me) return // Not logged in

  var parps = kb.each(subject, ns.wf('participation')).filter(function (pn) {
    return kb.holds(pn, ns.wf('participant'), me)
  })
  if (parps.length > 1) {
    throw new Error('Multiple records of your participation')
  }
  if (parps.length) {
    // If I am not already recorded
    return parps[0] // returns the particpation object
  } else {
    var participation = UI.widgets.newThing(padDoc)
    var ins = [
      UI.rdf.st(subject, ns.wf('participation'), participation, padDoc),

      UI.rdf.st(participation, ns.wf('participant'), me, padDoc),
      UI.rdf.st(participation, UI.ns.cal('dtstart'), new Date(), padDoc),
      UI.rdf.st(
        participation,
        ns.ui('backgroundColor'),
        UI.pad.lightColorHash(me),
        padDoc
      )
    ]
    kb.updater.update([], ins, function (uri, ok, errorMessage) {
      if (!ok) {
        throw new Error('Error recording your partipation: ' + errorMessage)
      }
      if (refreshable && refreshable.refresh) {
        refreshable.refresh()
      }
      // UI.pad.renderPartipants(dom, table, padDoc, subject, me, options)
    })
    return participation
  }
}

// Record my participation and display participants
//
UI.pad.manageParticipation = function (
  dom,
  container,
  padDoc,
  subject,
  me,
  options
) {
  var table = dom.createElement('table')
  container.appendChild(table)
  UI.pad.renderPartipants(dom, table, padDoc, subject, me, options)
  try {
    UI.pad.recordParticipation(subject, padDoc, table)
  } catch (e) {
    container.appendChild(
      UI.widgets.errorMessageBlock(
        dom,
        'Error recording your partipation: ' + e
      )
    ) // Clean up?
  }
  return table
}

UI.pad.notepad = function (dom, padDoc, subject, me, options) {
  options = options || {}
  var exists = options.exists
  var table = dom.createElement('table')
  var kb = UI.store
  var ns = UI.ns

  if (me && !me.uri) throw new Error('UI.pad.notepad:  Invalid userid')

  var updater = UI.store.updater

  var PAD = $rdf.Namespace('http://www.w3.org/ns/pim/pad#')

  table.setAttribute(
    'style',
    'padding: 1em; overflow: auto; resize: horizontal; min-width: 40em;'
  )

  var upstreamStatus = null
  var downstreamStatus = null

  if (options.statusArea) {
    var t = options.statusArea.appendChild(dom.createElement('table'))
    var tr = t.appendChild(dom.createElement('tr'))
    upstreamStatus = tr.appendChild(dom.createElement('td'))
    downstreamStatus = tr.appendChild(dom.createElement('td'))
    upstreamStatus.setAttribute('style', 'width:50%')
    downstreamStatus.setAttribute('style', 'width:50%')
  }

  var complain = function (message, upstream) {
    console.log(message)
    if (options.statusArea) {
      ;(upstream ? upstreamStatus : downstreamStatus).appendChild(
        UI.widgets.errorMessageBlock(dom, message, 'pink')
      )
    }
  }

  var clearStatus = function (_upsteam) {
    if (options.statusArea) {
      options.statusArea.innerHTML = ''
    }
  }

  var setPartStyle = function (part, colors, pending) {
    var chunk = part.subject
    colors = colors || ''
    var baseStyle =
      'font-size: 100%; font-family: monospace; width: 100%; border: none; white-space: pre-wrap;'
    var headingCore =
      'font-family: sans-serif; font-weight: bold;  border: none;'
    var headingStyle = [
      'font-size: 110%;  padding-top: 0.5em; padding-bottom: 0.5em; width: 100%;',
      'font-size: 120%; padding-top: 1em; padding-bottom: 1em; width: 100%;',
      'font-size: 150%; padding-top: 1em; padding-bottom: 1em; width: 100%;'
    ]

    var author = kb.any(chunk, ns.dc('author'))
    if (!colors && author) {
      // Hash the user webid for now -- later allow user selection!
      var bgcolor = UI.pad.lightColorHash(author)
      colors =
        'color: ' +
        (pending ? '#888' : 'black') +
        '; background-color: ' +
        bgcolor +
        ';'
    }

    var indent = kb.any(chunk, PAD('indent'))

    indent = indent ? indent.value : 0
    var style =
      indent >= 0
        ? baseStyle + 'text-indent: ' + indent * 3 + 'em;'
        : headingCore + headingStyle[-1 - indent]
    // ? baseStyle + 'padding-left: ' + (indent * 3) + 'em;'
    part.setAttribute('style', style + colors)
  }

  var removePart = function (part) {
    var chunk = part.subject
    if (!chunk) throw new Error('No chunk for line to be deleted!') // just in case
    var prev = kb.any(undefined, PAD('next'), chunk)
    var next = kb.any(chunk, PAD('next'))
    if (prev.sameTerm(subject) && next.sameTerm(subject)) {
      // Last one
      console.log("You can't delete the only line.")
      return
    }

    var del = kb
      .statementsMatching(chunk, undefined, undefined, padDoc)
      .concat(kb.statementsMatching(undefined, undefined, chunk, padDoc))
    var ins = [$rdf.st(prev, PAD('next'), next, padDoc)]
    var label = chunk.uri.slice(-4)
    console.log('Deleting line ' + label)

    updater.update(del, ins, function (uri, ok, errorMessage, response) {
      if (ok) {
        var row = part.parentNode
        var before = row.previousSibling
        row.parentNode.removeChild(row)
        console.log('    deleted line ' + label + ' ok ' + part.value)
        if (before && before.firstChild) {
          before.firstChild.focus()
        }
      } else if (response && response.status === 409) {
        // Conflict
        setPartStyle(part, 'color: black;  background-color: #ffd;') // yellow
        part.state = 0 // Needs downstream refresh
        utils.beep(0.5, 512) // Ooops clash with other person
        setTimeout(function () {
          // Ideally, beep! @@
          reloadAndSync() // Throw away our changes and
          // updater.requestDownstreamAction(padDoc, reloadAndSync)
        }, 1000)
      } else {
        console.log('    removePart FAILED ' + chunk + ': ' + errorMessage)
        console.log("    removePart was deleteing :'" + del)
        setPartStyle(part, 'color: black;  background-color: #fdd;') // failed
        const res = response ? response.status : ' [no response field] '
        complain('Error ' + res + ' saving changes: ' + errorMessage.true) // upstream,
        // updater.requestDownstreamAction(padDoc, reloadAndSync);
      }
    })
  } // removePart

  var changeIndent = function (part, chunk, delta) {
    var del = kb.statementsMatching(chunk, PAD('indent'))
    var current = del.length ? Number(del[0].object.value) : 0
    if (current + delta < -3) return //  limit negative indent
    var newIndent = current + delta
    var ins = $rdf.st(chunk, PAD('indent'), newIndent, padDoc)
    updater.update(del, ins, function (uri, ok, errorBody) {
      if (!ok) {
        console.log(
          "Indent change FAILED '" +
            newIndent +
            "' for " +
            padDoc +
            ': ' +
            errorBody
        )
        setPartStyle(part, 'color: black;  background-color: #fdd;') // failed
        updater.requestDownstreamAction(padDoc, reloadAndSync)
      } else {
        setPartStyle(part) // Implement the indent
      }
    })
  }

  // Use this sort of code to split the line when return pressed in the middle @@
  /*
  function doGetCaretPosition doGetCaretPosition (oField) {
    var iCaretPos = 0
        // IE Support
    if (document.selection) {
            // Set focus on the element to avoid IE bug
      oField.focus()

            // To get cursor position, get empty selection range
      var oSel = document.selection.createRange()

            // Move selection start to 0 position
      oSel.moveStart('character', -oField.value.length)

            // The caret position is selection length
      iCaretPos = oSel.text.length

        // Firefox suppor
    } else if (oField.selectionStart || oField.selectionStart === '0') {
      iCaretPos = oField.selectionStart
    }
        // Return results
    return (iCaretPos)
  }
*/
  var addListeners = function (part, chunk) {
    part.addEventListener('keydown', function (event) {
      var queueProperty, queue
      //  up 38; down 40; left 37; right 39     tab 9; shift 16; escape 27
      switch (event.keyCode) {
        case 13: // Return
          var before = event.shiftKey
          console.log('enter') // Shift-return inserts before -- only way to add to top of pad.
          if (before) {
            queue = kb.any(undefined, PAD('next'), chunk)
            queueProperty = 'newlinesAfter'
          } else {
            queue = kb.any(chunk, PAD('next'))
            queueProperty = 'newlinesBefore'
          }
          queue[queueProperty] = queue[queueProperty] || 0
          queue[queueProperty] += 1
          if (queue[queueProperty] > 1) {
            console.log('    queueing newline queue = ' + queue[queueProperty])
            return
          }
          console.log('    go ahead line before ' + queue[queueProperty])
          newChunk(part, before) // was document.activeElement
          break

        case 8: // Delete
          if (part.value.length === 0) {
            console.log(
              'Delete key line ' + chunk.uri.slice(-4) + ' state ' + part.state
            )

            switch (part.state) {
              case 1: // contents being sent
              case 2: // contents need to be sent again
                part.state = 4 // delete me
                return
              case 3: // being deleted already
              case 4: // already deleme state
                return
              case undefined:
              case 0:
                part.state = 3 // being deleted
                removePart(part)
                event.preventDefault()
                break // continue
              default:
                throw new Error('pad: Unexpected state ' + part)
            }
          }
          break
        case 9: // Tab
          var delta = event.shiftKey ? -1 : 1
          changeIndent(part, chunk, delta)
          event.preventDefault() // default is to highlight next field
          break
        case 27: // ESC
          console.log('escape')
          updater.requestDownstreamAction(padDoc, reloadAndSync)
          event.preventDefault()
          break

        case 38: // Up
          if (part.parentNode.previousSibling) {
            part.parentNode.previousSibling.firstChild.focus()
            event.preventDefault()
          }
          break

        case 40: // Down
          if (part.parentNode.nextSibling) {
            part.parentNode.nextSibling.firstChild.focus()
            event.preventDefault()
          }
          break

        default:
      }
    })

    var updateStore = function (part) {
      var chunk = part.subject
      setPartStyle(part, undefined, true)
      var old = kb.any(chunk, ns.sioc('content')).value
      var del = [$rdf.st(chunk, ns.sioc('content'), old, padDoc)]
      var ins = [$rdf.st(chunk, ns.sioc('content'), part.value, padDoc)]
      var newOne = part.value

      // DEBUGGING ONLY
      if (part.lastSent) {
        if (old !== part.lastSent) {
          throw new Error(
            "Out of order, last sent expected '" +
              old +
              "' but found '" +
              part.lastSent +
              "'"
          )
        }
      }
      part.lastSent = newOne

      console.log(
        ' Patch proposed to ' +
          chunk.uri.slice(-4) +
          " '" +
          old +
          "' -> '" +
          newOne +
          "' "
      )
      updater.update(del, ins, function (uri, ok, errorBody, xhr) {
        if (!ok) {
          // alert("clash " + errorBody);
          console.log(
            '    patch FAILED ' +
              xhr.status +
              " for '" +
              old +
              "' -> '" +
              newOne +
              "': " +
              errorBody
          )
          if (xhr.status === 409) {
            // Conflict -  @@ we assume someone else
            setPartStyle(part, 'color: black;  background-color: #fdd;')
            part.state = 0 // Needs downstream refresh
            utils.beep(0.5, 512) // Ooops clash with other person
            setTimeout(function () {
              updater.requestDownstreamAction(padDoc, reloadAndSync)
            }, 1000)
          } else {
            setPartStyle(part, 'color: black;  background-color: #fdd;') // failed pink
            part.state = 0
            complain(
              '    Error ' + xhr.status + ' sending data: ' + errorBody,
              true
            )
            utils.beep(1.0, 128) // Other
            // @@@   Do soemthing more serious with other errors eg auth, etc
          }
        } else {
          clearStatus(true) // upstream
          setPartStyle(part) // synced
          console.log("    Patch ok '" + old + "' -> '" + newOne + "' ")

          if (part.state === 4) {
            //  delete me
            part.state = 3
            removePart(part)
          } else if (part.state === 3) {
            // being deleted
            // pass
          } else if (part.state === 2) {
            part.state = 1 // pending: lock
            updateStore(part)
          } else {
            part.state = 0 // clear lock
          }
        }
      })
    }

    part.addEventListener('input', function inputChangeListener (_event) {
      // console.log("input changed "+part.value);
      setPartStyle(part, undefined, true) // grey out - not synced
      console.log(
        'Input event state ' + part.state + " value '" + part.value + "'"
      )
      switch (part.state) {
        case 3: // being deleted
          return
        case 4: // needs to be deleted
          return
        case 2: // needs content updating, we know
          return
        case 1:
          part.state = 2 // lag we need another patch
          return
        case 0:
        case undefined:
          part.state = 1 // being upadted
          updateStore(part)
      }
    }) // listener
  } // addlisteners

  var newPartAfter = function (tr1, chunk, before) {
    // @@ take chunk and add listeners
    var text = kb.any(chunk, ns.sioc('content'))
    text = text ? text.value : ''
    var tr = dom.createElement('tr')
    if (before) {
      table.insertBefore(tr, tr1)
    } else {
      // after
      if (tr1 && tr1.nextSibling) {
        table.insertBefore(tr, tr1.nextSibling)
      } else {
        table.appendChild(tr)
      }
    }
    var part = tr.appendChild(dom.createElement('input'))
    part.subject = chunk
    part.setAttribute('type', 'text')
    part.value = text
    if (me) {
      setPartStyle(part, '')
      addListeners(part, chunk)
    } else {
      setPartStyle(part, 'color: #222; background-color: #fff')
      console.log("Note can't add listeners - not logged in")
    }
    return part
  }

  var newChunk = function (ele, before) {
    // element of chunk being split
    var kb = UI.store
    var indent = 0
    var queueProperty = null
    var here, prev, next, queue, tr1

    if (ele) {
      if (ele.tagName.toLowerCase() !== 'input') {
        console.log('return pressed when current document is: ' + ele.tagName)
      }
      here = ele.subject
      indent = kb.any(here, PAD('indent'))
      indent = indent ? Number(indent.value) : 0
      if (before) {
        prev = kb.any(undefined, PAD('next'), here)
        next = here
        queue = prev
        queueProperty = 'newlinesAfter'
      } else {
        prev = here
        next = kb.any(here, PAD('next'))
        queue = next
        queueProperty = 'newlinesBefore'
      }
      tr1 = ele.parentNode
    } else {
      prev = subject
      next = subject
      tr1 = undefined
    }

    var chunk = UI.widgets.newThing(padDoc)
    var label = chunk.uri.slice(-4)

    var del = [$rdf.st(prev, PAD('next'), next, padDoc)]
    var ins = [
      $rdf.st(prev, PAD('next'), chunk, padDoc),
      $rdf.st(chunk, PAD('next'), next, padDoc),
      $rdf.st(chunk, ns.dc('author'), me, padDoc),
      $rdf.st(chunk, ns.sioc('content'), '', padDoc)
    ]
    if (indent > 0) {
      // Do not inherit
      ins.push($rdf.st(chunk, PAD('indent'), indent, padDoc))
    }

    console.log('    Fresh chunk ' + label + ' proposed')
    updater.update(del, ins, function (uri, ok, errorBody, _xhr) {
      if (!ok) {
        // alert("Error writing new line " + label + ": " + errorBody);
        console.log('    ERROR writing new line ' + label + ': ' + errorBody)
      } else {
        var newPart = newPartAfter(tr1, chunk, before)
        setPartStyle(newPart)
        newPart.focus() // Note this is delayed
        if (queueProperty) {
          console.log(
            '    Fresh chunk ' +
              label +
              ' updated, queue = ' +
              queue[queueProperty]
          )
          queue[queueProperty] -= 1
          if (queue[queueProperty] > 0) {
            console.log(
              '    Implementing queued newlines = ' + next.newLinesBefore
            )
            newChunk(newPart, before)
          }
        }
      }
    })
  }

  var consistencyCheck = function () {
    var found = []
    var failed = 0
    function complain2 (msg) {
      complain(msg)
      failed++
    }

    if (!kb.the(subject, PAD('next'))) {
      complain2('No initial next pointer')
      return false // can't do linked list
    }
    // var chunk = kb.the(subject, PAD('next'))
    var prev = subject
    var chunk
    for (;;) {
      chunk = kb.the(prev, PAD('next'))
      if (!chunk) {
        complain2('No next pointer from ' + prev)
      }
      if (chunk.sameTerm(subject)) {
        break
      }
      prev = chunk
      var label = chunk.uri.split('#')[1]
      if (found[chunk.uri]) {
        complain2('Loop!')
        return false
      }

      found[chunk.uri] = true
      var k = kb.each(chunk, PAD('next')).length
      if (k !== 1) {
        complain2('Should be 1 not ' + k + ' next pointer for ' + label)
      }

      k = kb.each(chunk, PAD('indent')).length
      if (k > 1) {
        complain2('Should be 0 or 1 not ' + k + ' indent for ' + label)
      }

      k = kb.each(chunk, ns.sioc('content')).length
      if (k !== 1) {
        complain2('Should be 1 not ' + k + ' contents for ' + label)
      }

      k = kb.each(chunk, ns.dc('author')).length
      if (k !== 1) {
        complain2('Should be 1 not ' + k + ' author for ' + label)
      }

      var sts = kb.statementsMatching(undefined, ns.sioc('contents'))
      sts.map(function (st) {
        if (!found[st.subject.uri]) {
          complain2('Loose chunk! ' + st.subject.uri)
        }
      })
    }
    return !failed
  }

  // Ensure that the display matches the current state of the
  var sync = function () {
    // var first = kb.the(subject, PAD('next'))
    if (kb.each(subject, PAD('next')).length !== 1) {
      var msg =
        'Pad: Inconsistent data - NEXT pointers: ' +
        kb.each(subject, PAD('next')).length
      console.log(msg)
      if (options.statusAra) {
        options.statusArea.textContent += msg
      }
      return
    }
    // var last = kb.the(undefined, PAD('previous'), subject)
    // var chunk = first //  = kb.the(subject, PAD('next'));
    var row

    // First see which of the logical chunks have existing physical manifestations
    var manif = []
    // Find which lines correspond to existing chunks

    for (
      let chunk = kb.the(subject, PAD('next'));
      !chunk.sameTerm(subject);
      chunk = kb.the(chunk, PAD('next'))
    ) {
      for (let i = 0; i < table.children.length; i++) {
        var tr = table.children[i]
        if (tr.firstChild.subject.sameTerm(chunk)) {
          manif[chunk.uri] = tr.firstChild
        }
      }
    }

    // Remove any deleted lines
    for (let i = table.children.length - 1; i >= 0; i--) {
      row = table.children[i]
      if (!manif[row.firstChild.subject.uri]) {
        table.removeChild(row)
      }
    }
    // Insert any new lines and update old ones
    row = table.firstChild // might be null
    for (
      let chunk = kb.the(subject, PAD('next'));
      !chunk.sameTerm(subject);
      chunk = kb.the(chunk, PAD('next'))
    ) {
      var text = kb.any(chunk, ns.sioc('content')).value
      // superstitious -- don't mess with unchanged input fields
      // which may be selected by the user
      if (row && manif[chunk.uri]) {
        var part = row.firstChild
        if (text !== part.value) {
          part.value = text
        }
        setPartStyle(part)
        part.state = 0 // Clear the state machine
        delete part.lastSent // DEBUG ONLY
        row = row.nextSibling
      } else {
        newPartAfter(row, chunk, true) // actually before
      }
    }
  }

  // Refresh the DOM tree

  var refreshTree = function (root) {
    if (root.refresh) {
      root.refresh()
      return
    }
    for (var i = 0; i < root.children.length; i++) {
      refreshTree(root.children[i])
    }
  }

  var reloading = false

  var checkAndSync = function () {
    console.log('    reloaded OK')
    clearStatus()
    if (!consistencyCheck()) {
      complain('CONSITENCY CHECK FAILED')
    } else {
      refreshTree(table)
    }
  }

  var reloadAndSync = function () {
    if (reloading) {
      console.log('   Already reloading - stop')
      return // once only needed
    }
    reloading = true
    var retryTimeout = 1000 // ms
    var tryReload = function () {
      console.log('try reload - timeout = ' + retryTimeout)
      updater.reload(updater.store, padDoc, function (ok, message, xhr) {
        reloading = false
        if (ok) {
          checkAndSync()
        } else {
          if (xhr.status === 0) {
            complain(
              'Network error refreshing the pad. Retrying in ' +
                retryTimeout / 1000
            )
            reloading = true
            retryTimeout = retryTimeout * 2
            setTimeout(tryReload, retryTimeout)
          } else {
            complain(
              'Error ' +
                xhr.status +
                'refreshing the pad:' +
                message +
                '. Stopped. ' +
                padDoc
            )
          }
        }
      })
    }
    tryReload()
  }

  table.refresh = sync // Catch downward propagating refresh events
  table.reloadAndSync = reloadAndSync

  if (!me) console.log('Warning: must be logged in for pad to be edited')

  if (exists) {
    console.log('Existing pad.')
    if (consistencyCheck()) {
      sync()
      if (kb.holds(subject, PAD('next'), subject)) {
        // Empty list untenable
        newChunk() // require at least one line
      }
    } else {
      console.log((table.textContent = 'Inconsistent data. Abort'))
    }
  } else {
    // Make new pad
    console.log('No pad exists - making new one.')
    var insertables = [
      $rdf.st(subject, ns.rdf('type'), PAD('Notepad'), padDoc),
      $rdf.st(subject, ns.dc('author'), me, padDoc),
      $rdf.st(subject, ns.dc('created'), new Date(), padDoc),
      $rdf.st(subject, PAD('next'), subject, padDoc)
    ]

    updater.update([], insertables, function (uri, ok, errorBody) {
      if (!ok) {
        complain(errorBody)
      } else {
        console.log('Initial pad created')
        newChunk() // Add a first chunck
        // getResults();
      }
    })
  }
  return table
}