/* 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 = '

New ' + utils.label(theClass) + '

' 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