/* F O R M S * * A Vanilla Dom implementation of the form language */ /* eslint-disable multiline-ternary */ /* global alert */ import * as buttons from './buttons' import { fieldParams } from './forms/fieldParams' import { field, mostSpecificClassURI, fieldFunction } from './forms/fieldFunction' import { setFieldStyle } from './forms/formStyle' import * as debug from '../debug' import { errorMessageBlock } from './error' import { basicField, renderNameValuePair } from './forms/basic' import { autocompleteField } from './forms/autocomplete/autocompleteField' import * as style from '../style' import { icons } from '../iconBase' import * as log from '../log' import * as ns from '../ns' import * as $rdf from 'rdflib' import { store } from '../logic' import * as utils from '../utils' import * as widgets from '../widgets' // Note default export export { field } from './forms/fieldFunction' export { fieldParams } from './forms/fieldParams' const checkMarkCharacter = '\u2713' const cancelCharacter = '\u2715' const dashCharacter = '-' const kb = store field[ns.ui('AutocompleteField').uri] = autocompleteField // /////////////////////////////////////////////////////////////////////// /* 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} dataDoc The web document in which the data is ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? ** ** @returns {Element} The HTML widget created */ function refreshOpionsSubfieldinGroup (dom, already, subject, dataDoc, callbackFunction, groupDiv, subfields) { const eles = groupDiv.children for (let j = 0; j < subfields.length; j++) { // This is really messy. const field = subfields[j] const t = mostSpecificClassURI(field) // Field type if (t === ns.ui('Options').uri) { const optionsRender = fieldFunction(dom, field) const newOne = optionsRender( dom, null, already, subject, field, dataDoc, callbackFunction ) debug.log('Refreshing Options field by replacing it.') // better to support actual refresh groupDiv.insertBefore(newOne, eles[j]) groupDiv.removeChild(eles[j + 1]) // Remove the old one } } } field[ns.ui('Form').uri] = field[ns.ui('Group').uri] = function (dom, container, already, subject, form, dataDoc, callbackFunction) { const box = dom.createElement('div') const ui = ns.ui if (container) container.appendChild(box) // Prevent loops const key = subject.toNT() + '|' + form.toNT() if (already[key]) { // been there done that box.appendChild(dom.createTextNode('Group: see above ' + key)) const plist = [$rdf.st(subject, ns.owl('sameAs'), subject)] // @@ need prev subject dom.outlineManager.appendPropertyTRs(box, plist) return box } const already2 = {} for (const x in already) already2[x] = 1 already2[key] = 1 const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const weight0 = kb.any(form, ui('weight'), null, formDoc) // Say 0-3 const weight = weight0 ? Number(weight0.value) : 1 if (weight > 3 || weight < 0) return box.appendChild(errorMessageBlock(dom, `Form Group weight ${weight} should be 0-3`)) box.setAttribute('style', style.formGroupStyle[weight]) // Indent a group box.style.display = 'flex' box.style.flexDirection = 'column' box.class = 'form-weight-' + weight let parts = kb.any(form, ui('parts'), null, formDoc) let subfields if (parts) { subfields = parts.elements } else { parts = kb.each(form, ui('part'), null, formDoc) // Warning: unordered subfields = sortBySequence(parts) } if (!parts) { return box.appendChild(errorMessageBlock(dom, 'No parts to form! ')) } for (let i = 0; i < subfields.length; i++) { const field = subfields[i] const subFieldFunction = fieldFunction(dom, field) // const itemChanged = function (ok, body) { if (ok && body && body.widget && body.widget === 'select') { refreshOpionsSubfieldinGroup(dom, already, subject, dataDoc, callbackFunction, box, subfields) } callbackFunction(ok, { widget: 'group', change: body }) } box.appendChild(subFieldFunction(dom, null, already2, subject, field, dataDoc, 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} dataDoc The web document in which the data is ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? ** ** @returns {Element} The HTML widget created */ field[ns.ui('Options').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { const kb = store const box = dom.createElement('div') const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const ui = ns.ui if (container) container.appendChild(box) let dependingOn = kb.any(form, ui('dependingOn')) if (!dependingOn) { dependingOn = ns.rdf('type') } // @@ default to type (do we want defaults?) const cases = kb.each(form, ui('case'), null, formDoc) if (!cases) { box.appendChild(errorMessageBlock(dom, 'No cases to Options form. ')) } let values if (dependingOn.sameTerm(ns.rdf('type'))) { values = Object.keys(kb.findTypeURIs(subject)).map(uri => $rdf.sym(uri)) // Use RDF-S inference } else { values = kb.each(subject, dependingOn) } if (values.length === 0) { box.appendChild( errorMessageBlock( dom, "Can't select subform as no value of: " + dependingOn ) ) } else { for (let i = 0; i < cases.length; i++) { const c = cases[i] const tests = kb.each(c, ui('for'), null, formDoc) // There can be multiple 'for' let match = false for (let j = 0; j < tests.length; j++) { for (const value of values) { const test = tests[j] if (value.sameTerm(tests) || (value.termType === test.termType && value.value === test.value)) { match = true } } } if (match) { const field = kb.the(c, ui('use')) if (!field) { box.appendChild( errorMessageBlock( dom, 'No "use" part for case in form ' + form ) ) return box } else { appendForm( dom, box, already, subject, field, dataDoc, callbackFunction ) } break } } } // @@ Add box.refresh() to sync fields with values 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} dataDoc The web document in which the data is ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? ** ** @returns {Element} The HTML widget created ** ** Form properties: ** @param {Boolean} reverse Make e reverse arc in the data OPS not SPO ** @param {NamedNode} property The property to be written in the data ** @param {Boolean} ordered Is the list an ordered one where the user defined the order */ field[ns.ui('Multiple').uri] = function ( dom, container, already, subject, form, dataDoc, 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 () { const object = newThing(dataDoc) // by default just add new nodes if (ordered) { createListIfNecessary() // Sets list and unsavedList list.elements.push(object) await saveListThenRefresh() } else { // eslint-disable-next-line multiline-ternary const toBeInserted = reverse ? [$rdf.st(object, property, subject, dataDoc)] : [$rdf.st(subject, property, object, dataDoc)] try { await kb.updater.update([], toBeInserted) } catch (err) { const msg = 'Error adding to unordered multiple: ' + err box.appendChild(errorMessageBlock(dom, msg)) debug.error(msg) } refresh() } } /** 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) { debug.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, dataDoc)) { const del = [$rdf.st(subject, property, object, dataDoc)] kb.updater.update(del, [], function (uri, ok, message) { if (ok) { body.removeChild(subField) } else { body.appendChild( 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? debug.log('pre move: ' + debugString(list.elements)) let i for (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 (ok, message) { debug.log(`Item done callback for item ${object.uri.slice(-7)}`) if (!ok) { // when does this happen? errors typically deal with upstream debug.error(' Item done callback: Error: ' + message) } callbackFunction(ok, message) } log.debug('Multiple: render object: ' + object) const fn = fieldFunction(dom, element) const subField = fn(dom, null, already, object, element, dataDoc, itemDone) // subfields 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(dataDoc.uri)) { buttons.deleteButtonWithCheck(dom, subField, utils.label(property), deleteThisItem) if (ordered) { // Add controsl in a frame const frame = dom.createElement('div') frame.style.display = 'grid' frame.style.gridTemplateColumns = 'auto 3em' frame.style.gridTemplateRows = '50% 50%' const moveUpButton = buttons.button( dom, icons.iconBase + 'noun_1369237.svg', 'Move Up', async event => moveThisItem(event, true)) const moveDownButton = buttons.button( dom, icons.iconBase + 'noun_1369241.svg', 'Move Down', async event => moveThisItem(event, false)) const shim = dom.createElement('div') shim.appendChild(subField) // Subfield has its own laytout frame.appendChild(shim) frame.appendChild(moveUpButton) frame.appendChild(moveDownButton) moveUpButton.style.gridColumn = 2 moveDownButton.style.gridColumn = 2 moveUpButton.style.gridRow = 1 moveDownButton.style.padding = '0em' // don't take too much space moveUpButton.style.padding = '0em' moveDownButton.style.gridRow = 2 shim.style.gridColumn = 1 shim.style.gridRowStart = 'span 2' // Cover both rows // shim.style.gridRowEnd = 2 // Cover both rows return frame } } return subField // unused } // renderItem /// ///////// Body of Multiple form field implementation const plusIconURI = icons.iconBase + 'noun_19460_green.svg' // white plus in green circle const kb = store const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const box = (dom.createElement('div')) const shim = box // no shim // 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 = ns.ui if (container) container.appendChild(box) const orderedNode = kb.any(form, ui('ordered')) const ordered = orderedNode ? $rdf.Node.toJS(orderedNode) : false const property = kb.any(form, ui('property')) const reverse = kb.anyJS(form, ui('reverse'), null, formDoc) if (!property) { box.appendChild( errorMessageBlock(dom, 'No property to multiple: ' + form) ) // used for arcs in the data return shim } let min = kb.any(form, ui('min')) // This is the minimum number -- default 0 min = min ? 0 + min.value : 0 const element = kb.any(form, ui('part')) // This is the form to use for each one if (!element) { box.appendChild( errorMessageBlock(dom, 'No part to multiple: ' + form) ) return shim } const body = box.appendChild(dom.createElement('div')) body.style.display = 'flex' body.style.flexDirection = 'column' let list // The RDF collection which keeps the ordered version or null let values // Initial values - always an array. Even when no list yet. values = reverse ? kb.any(null, property, subject, dataDoc) : kb.any(subject, property, null, dataDoc) if (ordered) { list = reverse ? kb.any(null, property, subject, dataDoc) : kb.any(subject, property, null, dataDoc) if (list) { values = list.elements } else { values = [] } } else { values = reverse ? kb.each(null, property, subject, dataDoc) : kb.each(subject, property, null, dataDoc) list = null } // Add control on the bottom for adding more items if (kb.updater.editable(dataDoc.uri)) { const tail = box.appendChild(dom.createElement('div')) tail.style.padding = '0.5em' const 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.predicateLabel(property, reverse) const prompt = tail.appendChild(dom.createElement('span')) prompt.textContent = (values.length === 0 ? 'Add one or more ' : 'Add more ') + utils.predicateLabel(property, reverse) // utils.label(property) tail.addEventListener('click', async _eventNotUsed => { await addItem() }, true) } function createListIfNecessary () { if (!list) { list = new $rdf.Collection() if (reverse) { kb.add(list, property, subject, dataDoc) } else { kb.add(subject, property, list, dataDoc) } } } async function saveListThenRefresh () { debug.log('save list: ' + debugString(list.elements)) // 20191214 createListIfNecessary() try { await kb.fetcher.putBack(dataDoc) } catch (err) { box.appendChild( errorMessageBlock(dom, 'Error trying to put back a list: ' + err) ) return } refresh() } function refresh () { let vals if (ordered) { const li = reverse ? kb.the(null, property, subject, dataDoc) : kb.the(subject, property, null, dataDoc) vals = li ? li.elements : [] } else { vals = reverse ? kb.each(null, property, subject, dataDoc) : kb.each(subject, property, null, dataDoc) vals.sort() // achieve consistency on each refresh } utils.syncTableToArrayReOrdered(body, vals, renderItem) } body.refresh = refresh // Allow live update refresh() async function asyncStuff () { const extra = min - values.length if (extra > 0) { for (let j = 0; j < extra; j++) { debug.log('Adding extra: min ' + min) await addItem() // Add blanks if less than minimum } await saveListThenRefresh() } } asyncStuff().then( () => { debug.log(' Multiple render: async stuff ok') }, (err) => { debug.error(' Multiple render: async stuff fails. #### ', err) } ) // async return shim } // 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 // field[ns.ui('PhoneField').uri] = basicField field[ns.ui('EmailField').uri] = basicField field[ns.ui('ColorField').uri] = basicField field[ns.ui('DateField').uri] = basicField field[ns.ui('DateTimeField').uri] = basicField field[ns.ui('TimeField').uri] = basicField field[ns.ui('NumericField').uri] = basicField field[ns.ui('IntegerField').uri] = basicField field[ns.ui('DecimalField').uri] = basicField field[ns.ui('FloatField').uri] = basicField field[ns.ui('TextField').uri] = basicField field[ns.ui('SingleLineTextField').uri] = basicField field[ns.ui('NamedNodeURIField').uri] = basicField /* Multiline Text field ** */ field[ns.ui('MultiLineTextField').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { const ui = ns.ui const kb = store const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const property = kb.any(form, ui('property')) if (!property) { return errorMessageBlock(dom, 'No property to text field: ' + form) } const box = dom.createElement('div') box.style.display = 'flex' box.style.flexDirection = 'row' const left = box.appendChild(dom.createElement('div')) left.style.width = style.formFieldNameBoxWidth const right = box.appendChild(dom.createElement('div')) left.appendChild(fieldLabel(dom, property, form)) dataDoc = fieldStore(subject, property, dataDoc) const text = kb.anyJS(subject, property, null, dataDoc) || '' const editable = kb.updater.editable(dataDoc.uri) const suppressEmptyUneditable = form && kb.anyJS(form, ns.ui('suppressEmptyUneditable'), null, formDoc) if (!editable && suppressEmptyUneditable && text === '') { box.style.display = 'none' } const field = makeDescription( dom, kb, subject, property, dataDoc, callbackFunction ) right.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, dataDoc, callbackFunction, tristate ) { const ui = ns.ui const kb = store const property = kb.any(form, ui('property')) if (!property) { const errorBlock = errorMessageBlock( dom, 'No property to boolean field: ' + form ) if (container) container.appendChild(errorBlock) return errorBlock } let lab = kb.any(form, ui('label')) if (!lab) lab = utils.label(property, true) // Init capital dataDoc = fieldStore(subject, property, dataDoc) let state = kb.any(subject, property) if (state === undefined) { state = false } // @@ sure we want that -- or three-state? const ins = $rdf.st(subject, property, true, dataDoc) const del = $rdf.st(subject, property, false, dataDoc) const box = buildCheckboxForm(dom, kb, lab, del, ins, form, dataDoc, tristate) if (container) container.appendChild(box) return box } field[ns.ui('BooleanField').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { return booleanField( dom, container, already, subject, form, dataDoc, callbackFunction, false ) } field[ns.ui('TristateField').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { return booleanField( dom, container, already, subject, form, dataDoc, callbackFunction, true ) } /* Classifier field ** ** Nested categories ** ** @@ To do: If a classification changes, then change any dependent Options fields. */ field[ns.ui('Classifier').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { const kb = store const ui = ns.ui const category = kb.any(form, ui('category')) if (!category) { return errorMessageBlock(dom, 'No category for classifier: ' + form) } log.debug('Classifier: dataDoc=' + dataDoc) const checkOptions = function (ok, body) { if (!ok) return callbackFunction(ok, body) return callbackFunction(ok, body) } const box = makeSelectForNestedCategory( dom, kb, subject, category, dataDoc, 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. */ field[ns.ui('Choice').uri] = function ( dom, container, already, subject, form, dataDoc, callbackFunction ) { const ui = ns.ui const kb = store const multiple = false const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know let p const box = dom.createElement('div') // Set flexDirection column? if (container) container.appendChild(box) const lhs = dom.createElement('div') box.appendChild(lhs) const rhs = dom.createElement('div') box.appendChild(rhs) const property = kb.any(form, ui('property')) if (!property) { return box.appendChild(errorMessageBlock(dom, 'No property for Choice: ' + form)) } lhs.appendChild(fieldLabel(dom, property, form)) const from = kb.any(form, ui('from')) if (!from) { return errorMessageBlock(dom, "No 'from' for Choice: " + form) } const subForm = kb.any(form, ui('use')) // Optional const follow = kb.anyJS(form, ui('follow'), null, formDoc) // data doc moves to new subject? let possible = [] let possibleProperties const nullLabel = '--' + utils.label(property) + '-?' const opts = { form, multiple, nullLabel, disambiguate: false } possible = kb.each(undefined, ns.rdf('type'), from, formDoc) for (const x in kb.findMembersNT(from)) { possible.push(kb.fromNT(x)) } // Use rdfs if (from.sameTerm(ns.rdfs('Class'))) { for (p in buttons.allClassURIs()) possible.push(kb.sym(p)) // 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 } let object = kb.any(subject, property) function addSubForm () { object = kb.any(subject, property) fieldFunction(dom, subForm)( dom, rhs, already, object, subForm, follow ? object.doc() : dataDoc, callbackFunction ) } const possible2 = sortByLabel(possible) if (kb.any(form, ui('canMintNew'))) { opts.mint = '* New *' // @@ could be better opts.subForm = subForm } const selector = makeSelectForOptions( dom, kb, subject, property, possible2, opts, dataDoc, callbackFunction ) rhs.appendChild(selector) if (object && subForm) addSubForm() return box } // Documentation - non-interactive fields // field[ns.ui('Comment').uri] = field[ ns.ui('Heading').uri ] = function ( dom, container, already, subject, form, dataDoc, _callbackFunction ) { const ui = ns.ui const kb = store let contents = kb.any(form, ui('contents')) if (!contents) contents = 'Error: No contents in comment field.' const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const uri = mostSpecificClassURI(form) const params = fieldParams[uri] || {} const box = dom.createElement('div') if (container) container.appendChild(box) const p = box.appendChild(dom.createElement(params.element)) p.textContent = contents setFieldStyle(p, form) // Some headings and prompts are only useful to guide user input const suppressIfUneditable = kb.anyJS(form, ns.ui('suppressIfUneditable'), null, formDoc) const editable = kb.updater.editable(dataDoc.uri) if (suppressIfUneditable && !editable) { box.style.display = 'none' } return box } // 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. // export function editFormButton ( dom, container, form, dataDoc, callbackFunction ) { const b = dom.createElement('button') b.setAttribute('type', 'button') b.innerHTML = 'Edit ' + utils.label(ns.ui('Form')) b.addEventListener( 'click', function (_e) { const ff = appendForm( dom, container, {}, form, ns.ui('FormForm'), dataDoc, callbackFunction ) ff.setAttribute( 'style', ns.ui('FormForm').sameTerm(form) ? 'background-color: #fee;' : 'background-color: #ffffe7;' ) b.parentNode.removeChild(b) }, true ) return b } export function appendForm ( dom, container, already, subject, form, dataDoc, itemDone ) { return fieldFunction(dom, form)( dom, container, already, subject, form, dataDoc, 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. */ export function propertiesForClass (kb, c) { const explicit = kb.each(undefined, ns.rdf('range'), c) ;[ ns.rdfs('comment'), ns.dc('title'), // Generic things ns.foaf('name'), ns.foaf('homepage') ].forEach(function (x) { explicit.push(x) }) let members = kb.each(undefined, ns.rdf('type'), c) if (members.length > 60) members = members.slice(0, 60) // Array supports slice? const used = {} for (let i = 0; i < (members.length > 60 ? 60 : members.length); i++) { kb.statementsMatching(members[i], undefined, undefined).forEach(function (st) { used[st.predicate.uri] = true }) } explicit.forEach(function (p) { used[p.uri] = true }) const result = [] for (const uri in used) { result.push(kb.sym(uri)) } return result } /** Find the closest class * @param kb The quad store * @param cla - the URI of the class * @param prop */ export function findClosest (kb, cla, prop) { const agenda = [kb.sym(cla)] // ordered - this is breadth first search while (agenda.length > 0) { const c = agenda.shift() // first const lists = kb.each(c, prop) log.debug('Lists for ' + c + ', ' + prop + ': ' + lists.length) if (lists.length !== 0) return lists const supers = kb.each(c, ns.rdfs('subClassOf')) for (let i = 0; i < supers.length; i++) { agenda.push(supers[i]) log.debug('findClosest: add super: ' + supers[i]) } } return [] } // Which forms apply to a given existing subject? export function formsFor (subject) { const kb = store log.debug('formsFor: subject=' + subject) const t = kb.findTypeURIs(subject) let t1 for (t1 in t) { log.debug(' type: ' + t1) } const bottom = kb.bottomTypeURIs(t) // most specific let candidates = [] for (const b in bottom) { // Find the most specific log.debug('candidatesFor: trying bottom type =' + b) candidates = candidates.concat( findClosest(kb, b, ns.ui('creationForm')) ) candidates = candidates.concat( findClosest(kb, b, ns.ui('annotationForm')) ) } return candidates } export function sortBySequence (list) { const subfields = list.map(function (p) { const k = kb.any(p, ns.ui('sequence')) return [k || 9999, p] }) subfields.sort(function (a, b) { return a[0] - b[0] }) return subfields.map(function (pair) { return pair[1] }) } export function sortByLabel (list) { const subfields = list.map(function (p) { return [utils.label(p).toLowerCase(), p] }) subfields.sort() return subfields.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 dataDoc - optional dataDoc else will prompt for one (unimplemented) */ export function newButton ( dom, kb, subject, predicate, theClass, form, dataDoc, callbackFunction ) { const b = dom.createElement('button') b.setAttribute('type', 'button') b.innerHTML = 'New ' + utils.label(theClass) b.addEventListener( 'click', function (_e) { b.parentNode.appendChild( promptForNew( dom, kb, subject, predicate, theClass, form, dataDoc, 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 dataDoc - The web document being edited // @param callbackFunction - takes (boolean ok, string errorBody) // @returns a dom object with the form DOM */ export function promptForNew ( dom, kb, subject, predicate, theClass, form, dataDoc, callbackFunction ) { const box = dom.createElement('form') if (!form) { const lists = findClosest(kb, theClass.uri, ns.ui('creationForm')) if (lists.length === 0) { const 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." const 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 } log.debug('lists[0] is ' + lists[0]) form = lists[0] // Pick any one } log.debug('form is ' + form) box.setAttribute('style', `border: 0.05em solid ${style.formBorderColor}; color: ${style.formBorderColor}`) // @@color? box.innerHTML = '