UNPKG

solid-ui

Version:

UI library for writing Solid read-write-web applications

1,514 lines (1,206 loc) • 48.8 kB
"use strict"; /* */ 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 }; var error = require('./error'); var buttons = require('./buttons'); var utils = require('../utils'); var checkMarkCharacter = "\u2713"; var cancelCharacter = "\u2715"; var dashCharacter = '-'; // /////////////////////////////////////////////////////////////////////// /* Form Field implementations ** */ /* Group of different fields ** */ forms.field[UI.ns.ui('Form').uri] = forms.field[UI.ns.ui('Group').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var kb = UI.store; var box = dom.createElement('div'); box.setAttribute('style', 'padding-left: 2em; border: 0.05em solid brown;'); // Indent a group var ui = UI.ns.ui; 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, UI.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.bottomURI(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 itemChanged(ok, body) { if (ok) { for (var j = 0; j < p2.length; j++) { // This is really messy. var field = p2[j]; var t = forms.bottomURI(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: Select one or more cases ** */ forms.field[UI.ns.ui('Options').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var kb = UI.store; var box = dom.createElement('div'); // box.setAttribute('style', 'padding-left: 2em; border: 0.05em dotted purple;') // Indent Options var ui = UI.ns.ui; container.appendChild(box); var dependingOn = kb.any(form, ui('dependingOn')); if (!dependingOn) { dependingOn = UI.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(UI.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 similar fields (unordered) ** */ forms.field[UI.ns.ui('Multiple').uri] = function (dom, container, already, subject, form, store, callbackFunction) { // var plusIcon = UI.icons.originalIconBase + 'tango/22-list-add.png' // blue plus var plusIconURI = UI.icons.iconBase + 'noun_19460_green.svg'; // white plus in green circle var 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 o fthe next field and has contents of one. // box.setAttribute('style', 'padding-left: 2em; border: 0.05em solid green;') // Indent a multiple var ui = UI.ns.ui; container.appendChild(box); 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 ? 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; } // box.appendChild(dom.createElement('h3')).textContent = "Fields:". var body = box.appendChild(dom.createElement('tr')); var tail = box.appendChild(dom.createElement('tr')); var addItem = function addItem(e, object) { UI.log.debug('Multiple add: ' + object); // ++count if (!object) object = forms.newThing(store); var tr = box.insertBefore(dom.createElement('tr'), tail); var itemDone = function itemDone(uri, ok, message) { if (ok) { // @@@ Check IT hasnt alreday been written in if (!kb.holds(subject, property, object, store)) { var ins = [$rdf.st(subject, property, object, store)]; kb.updater.update([], ins, linkDone); } } else { tr.appendChild(error.errorMessageBlock(dom, 'Multiple: item failed: ' + body)); callbackFunction(ok, message); } }; var linkDone = function linkDone(uri, ok, message) { return callbackFunction(ok, message); }; var fn = forms.fieldFunction(dom, element); var subField = fn(dom, body, already, object, element, store, itemDone); // delete button var deleteItem = function deleteItem() { 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)); } }); } }; if (kb.updater.editable(store.uri)) { buttons.deleteButtonWithCheck(dom, subField, utils.label(property), deleteItem); } }; var values = kb.each(subject, property); if (kb.updater.editable(store.uri)) { var img = tail.appendChild(dom.createElement('img')); img.setAttribute('src', plusIconURI); // plus sign img.setAttribute('style', 'margin: 0.2em; width: 1em; height:1em'); 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', addItem, true); // img.addEventListener('click', addItem, true) } values.map(function (obj) { addItem(null, obj); }); var extra = min - values.length; for (var j = 0; j < extra; j++) { console.log('Adding extra: min ' + min); addItem(); // Add blanks if less than minimum } return box; }; /* 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[UI.ns.ui('ColorField').uri] = { 'size': 9, 'type': 'color', dt: 'color' }; // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color forms.fieldParams[UI.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[UI.ns.ui('DateField').uri] = { 'size': 20, 'type': 'date', 'dt': 'date' }; forms.fieldParams[UI.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[UI.ns.ui('DateTimeField').uri] = { 'size': 20, 'type': 'date', 'dt': 'dateTime' }; forms.fieldParams[UI.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[UI.ns.ui('TimeField').uri] = { 'size': 10, 'type': 'time', 'dt': 'time' }; forms.fieldParams[UI.ns.ui('TimeField').uri].pattern = /^\s*([0-2]?[0-9]:[0-5][0-9](:[0-5][0-9])?)\s*$/; forms.fieldParams[UI.ns.ui('IntegerField').uri] = { 'size': 12, 'style': 'text-align: right', 'dt': 'integer' }; forms.fieldParams[UI.ns.ui('IntegerField').uri].pattern = /^\s*-?[0-9]+\s*$/; forms.fieldParams[UI.ns.ui('DecimalField').uri] = { 'size': 12, 'style': 'text-align: right', 'dt': 'decimal' }; forms.fieldParams[UI.ns.ui('DecimalField').uri].pattern = /^\s*-?[0-9]*(\.[0-9]*)?\s*$/; forms.fieldParams[UI.ns.ui('FloatField').uri] = { 'size': 12, 'style': 'text-align: right', 'dt': 'float' }; forms.fieldParams[UI.ns.ui('FloatField').uri].pattern = /^\s*-?[0-9]*(\.[0-9]*)?((e|E)-?[0-9]*)?\s*$/; forms.fieldParams[UI.ns.ui('SingleLineTextField').uri] = {}; forms.fieldParams[UI.ns.ui('NamedNodeURIField').uri] = { namedNode: true }; forms.fieldParams[UI.ns.ui('TextField').uri] = {}; forms.fieldParams[UI.ns.ui('PhoneField').uri] = { 'size': 20, 'uriPrefix': 'tel:' }; forms.fieldParams[UI.ns.ui('PhoneField').uri].pattern = /^\+?[\d-]+[\d]*$/; forms.fieldParams[UI.ns.ui('EmailField').uri] = { 'size': 30, 'uriPrefix': 'mailto:' }; forms.fieldParams[UI.ns.ui('EmailField').uri].pattern = /^\s*.*@.*\..*\s*$/; // @@ Get the right regexp here forms.field[UI.ns.ui('PhoneField').uri] = forms.field[UI.ns.ui('EmailField').uri] = forms.field[UI.ns.ui('ColorField').uri] = forms.field[UI.ns.ui('DateField').uri] = forms.field[UI.ns.ui('DateTimeField').uri] = forms.field[UI.ns.ui('TimeField').uri] = forms.field[UI.ns.ui('NumericField').uri] = forms.field[UI.ns.ui('IntegerField').uri] = forms.field[UI.ns.ui('DecimalField').uri] = forms.field[UI.ns.ui('FloatField').uri] = forms.field[UI.ns.ui('TextField').uri] = forms.field[UI.ns.ui('SingleLineTextField').uri] = forms.field[UI.ns.ui('NamedNodeURIField').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var ui = UI.ns.ui; var kb = UI.store; var box = dom.createElement('tr'); 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.bottomURI(form); var params = forms.fieldParams[uri]; if (params === undefined) params = {}; // non-bottom field types can do this var style = params.style || 'font-size: 100%; margin: 0.1em; padding: 0.1em;'; // box.appendChild(dom.createTextNode(' uri='+uri+', pattern='+ params.pattern)) var field = dom.createElement('input'); 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.disabled = true; 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, UI.ns.xsd(params.dt)); } else { result = new $rdf.Literal(field.value); } } var is = ds.map(function (st) { return $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(function (st) { if (!docs.includes(st.why.uri)) docs.push(st.why.uri); }); ds.forEach(function (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); console.log('Update many: ' + docs); var doc = docs.pop(); var is1 = is.filter(function (st) { return st.why.uri === doc; }); var is2 = is.filter(function (st) { return st.why.uri !== doc; }); var ds1 = ds.filter(function (st) { return st.why.uri === doc; }); var ds2 = ds.filter(function (st) { return 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; }; /* Multiline Text field ** */ forms.field[UI.ns.ui('MultiLineTextField').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var ui = UI.ns.ui; var kb = UI.store; var property = kb.any(form, ui('property')); if (!property) { return error.errorMessageBlock(dom, 'No property to text field: ' + form); } container.appendChild(forms.fieldLabel(dom, property, form)); store = forms.fieldStore(subject, property, store); var box = forms.makeDescription(dom, kb, subject, property, store, callbackFunction); // box.appendChild(dom.createTextNode('<-@@ subj:'+subject+', prop:'+property)) container.appendChild(box); return box; }; /* Boolean field and Tri-state version (true/false/null) ** */ function booleanField(dom, container, already, subject, form, store, callbackFunction, tristate) { var ui = UI.ns.ui; var kb = UI.store; var property = kb.any(form, ui('property')); if (!property) { return container.appendChild(error.errorMessageBlock(dom, 'No property to boolean field: ' + form)); } 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); container.appendChild(box); return box; } forms.field[UI.ns.ui('BooleanField').uri] = function (dom, container, already, subject, form, store, callbackFunction, tristate) { return booleanField(dom, container, already, subject, form, store, callbackFunction, false); }; forms.field[UI.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[UI.ns.ui('Classifier').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var kb = UI.store; var 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 checkOptions(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); 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[UI.ns.ui('Choice').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var ui = UI.ns.ui; var ns = UI.ns; var kb = UI.store; var multiple = false; var p; var box = dom.createElement('tr'); 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 forms.allClassURIs()) { possible.push(kb.sym(p)); } // UI.log.debug("%%% Choice field: possible.length 2 = "+possible.length) } else if (from.sameTerm(ns.rdf('Property'))) { possibleProperties = forms.propertyTriage(); 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 = forms.propertyTriage(); for (p in possibleProperties.op) { possible.push(kb.fromNT(p)); } opts.disambiguate = true; } else if (from.sameTerm(ns.owl('DatatypeProperty'))) { possibleProperties = forms.propertyTriage(); for (p in possibleProperties.dp) { possible.push(kb.fromNT(p)); } opts.disambiguate = true; } var object = kb.any(subject, property); function addSubForm(ok, body) { 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(true, ''); return box; }; // Documentation - non-interactive fields // forms.fieldParams[UI.ns.ui('Comment').uri] = { 'element': 'p', 'style': 'padding: 0.1em 1.5em; color: brown; white-space: pre-wrap;' }; forms.fieldParams[UI.ns.ui('Heading').uri] = { 'element': 'h3', 'style': 'font-size: 110%; color: brown;' }; forms.field[UI.ns.ui('Comment').uri] = forms.field[UI.ns.ui('Heading').uri] = function (dom, container, already, subject, form, store, callbackFunction) { var ui = UI.ns.ui; var kb = UI.store; var contents = kb.any(form, ui('contents')); if (!contents) contents = 'Error: No contents in comment field.'; var uri = forms.bottomURI(form); var params = forms.fieldParams[uri]; if (params === undefined) { params = {}; } ; // non-bottom field types can do this var box = dom.createElement('div'); 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 forms.bottomURI = function (x) { var 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) { var uri = forms.bottomURI(field); 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(UI.ns.ui('Form')); b.addEventListener('click', function (e) { var ff = forms.appendForm(dom, container, {}, form, UI.ns.ui('FormForm'), store, callbackFunction); ff.setAttribute('style', UI.ns.ui('FormForm').sameTerm(form) ? 'background-color: #fee;' : 'background-color: #ffffe7;'); container.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 anywahere 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; }; // @param cla - the URI of the class // @proap 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, UI.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; var 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, UI.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, 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 brown; color: brown'); box.innerHTML = '<h3>New ' + utils.label(theClass) + '</h3>'; var formFunction = forms.fieldFunction(dom, form); var object = forms.newThing(store); var gotButton = false; var itemDone = function itemDone(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) }; var linkDone = 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, undefined); // 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 = '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); 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); 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]; // UI.log.debug('Select element: '+ sub) if (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 getActual() { actual = {}; if (predicate.sameTerm(UI.ns.rdf('type'))) { actual = kb.findTypeURIs(subject); } else { kb.each(subject, predicate).map(function (x) { actual[x.uri] = true; }); } return actual; }; var actual = getActual(); // var newObject = null var onChange = function onChange(e) { select.disabled = true; // until data written back - gives user feedback too var ds = []; var is = []; var removeValue = function removeValue(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).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, UI.ns.owl('disjointUnionOf')); var subs; var multiple = false; if (!du) { subs = kb.each(undefined, UI.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, UI.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. // Callback 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 onChange(ok, body) { if (ok) update(); callbackFunction(ok, body); }; select = forms.makeSelectForCategory(dom, kb, subject, category, store, onChange); container.appendChild(select); var update = function update() { // UI.log.info("Selected is now: "+select.currentURI) if (child) { container.removeChild(child); child = null; } if (select.currentURI && kb.any(kb.sym(select.currentURI), UI.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) { var missing = a.filter(function (st) { return !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; var defa = kb.any(form, UI.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 boxHandler(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 ".concat(toDelete)); console.log(" Inserting ".concat(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 ".concat(input.state, " to ").concat(input.newState, ":\n\n").concat(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, UI.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; //# sourceMappingURL=forms.js.map