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