import { BlankNode, Literal, NamedNode, Node, st, Store, Variable } from 'rdflib' import { solidLogicSingleton } from 'solid-logic' import ns from '../../ns' import { style } from '../../style' import styleConstants from '../../styleConstants' import { label } from '../../utils' import { errorMessageBlock } from '../error' import { mostSpecificClassURI } from './fieldFunction' import { fieldParams } from './fieldParams' const store = solidLogicSingleton.store /* Style and create a name, value pair */ export function renderNameValuePair (dom: HTMLDocument, kb: Store, box: HTMLElement, form: NamedNode, label?: string):HTMLElement { // const property = kb.any(form, ns.ui('property')) box.style.display = 'flex' box.style.flexDirection = 'row' const lhs = box.appendChild(dom.createElement('div')) lhs.style.width = styleConstants.formFieldNameBoxWidth const rhs = box.appendChild(dom.createElement('div')) lhs.setAttribute('class', 'formFieldName') lhs.setAttribute('style', style.formFieldNameBoxStyle) rhs.setAttribute('class', 'formFieldValue') if (label) { lhs.appendChild(dom.createTextNode(label)) } else if (kb.any(form, ns.ui('property'))) { // Assume more space for error on right lhs.appendChild(fieldLabel(dom, kb.any(form, ns.ui('property')) as NamedNode, form)) } else { rhs.appendChild(errorMessageBlock(dom, 'No property or label given for form field: ' + form)) lhs.appendChild(dom.createTextNode('???')) } return rhs } /** * Create an anchor element with a label as the anchor text. * * @param dom The DOM * @param property href for the anchor element * @param fieldInQuestion field to produce a label for * * @internal exporting this only for unit tests */ export function fieldLabel (dom: HTMLDocument, property: NamedNode | undefined, fieldInQuestion: Node): HTMLElement | Text { let lab = store.any(fieldInQuestion as any, ns.ui('label')) if (!lab) lab = label(property, true) // Init capital if (property === undefined) { return dom.createTextNode('@@Internal error: undefined property') } const anchor = dom.createElement('a') /* istanbul ignore next */ 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 as any return anchor } /** * Returns the document for the first quad that matches * the subject and predicate provided, or default if that * store is not editable. * * @param subject Subject about which we want to find an editable RDF document * @param predicate Predicate about which we want to find an editable RDF document * @param def default RDF document to return if none found * * @internal exporting this only for unit tests */ export function fieldStore (subject: NamedNode | BlankNode | Variable, predicate: NamedNode | Variable, def: NamedNode | undefined): NamedNode | undefined { const sts = store.statementsMatching(subject, predicate) if (sts.length === 0) return def // can used default as no data yet if (!store.updater) { throw new Error('Store has no updater') } if ( sts.length > 0 && sts[0].why.value && store.updater.editable(sts[0].why.value, store) ) { return store.sym(sts[0].why.value) } return def } /** * Render a basic form field * * The same function is used for many similar one-value fields, with different * regexps used to validate. * * @param dom The HTML Document object aka Document Object Model * @param container If present, the created widget will be appended to this * @param already A hash table of (form, subject) kept to prevent recursive forms looping * @param subject The thing about which the form displays/edits data * @param form The form or field to be rendered * @param doc The web document in which the data is * @param callbackFunction Called when data is changed? * * @returns The HTML widget created */ export function basicField ( dom: HTMLDocument, container: HTMLElement | undefined, already, subject: NamedNode | BlankNode | Variable, form: NamedNode, doc: NamedNode | undefined, callbackFunction: (_ok: boolean, _errorMessage: string) => void ): HTMLElement { const kb = store const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know const box = dom.createElement('div') const property = kb.any(form, ns.ui('property')) if (container) container.appendChild(box) if (!property) { return box.appendChild( errorMessageBlock(dom, 'Error: No property given for text field: ' + form) ) } const rhs = renderNameValuePair(dom, kb, box, form) // It can be cleaner to just remove empty fields if you can't edit them anyway const suppressEmptyUneditable = kb.anyJS(form, ns.ui('suppressEmptyUneditable'), null, formDoc) const uri = mostSpecificClassURI(form) let params = fieldParams[uri] if (params === undefined) params = { style: '' } // non-bottom field types can do this const paramStyle = params.style || '' const inputStyle = style.textInputStyle + paramStyle const field = dom.createElement('input') ;(field as any).style = inputStyle rhs.appendChild(field) field.setAttribute('type', params.type ? params.type : 'text') const size = kb.anyJS(form, ns.ui('size')) || styleConstants.textInputSize || 20 field.setAttribute('size', size) const maxLength = kb.any(form, ns.ui('maxLength')) field.setAttribute('maxLength', maxLength ? '' + maxLength : styleConstants.basicMaxLength) doc = doc || fieldStore(subject, property as any, doc) let obj = kb.any(subject, property as any, undefined, doc) if (!obj) { obj = kb.any(form, ns.ui('default')) } if (obj && obj.value && params.uriPrefix) { // eg tel: or mailto: field.value = decodeURIComponent(obj.value.replace(params.uriPrefix, '')) // should have no spaces but in case .replace(/ /g, '') } else if (obj) { /* istanbul ignore next */ field.value = obj.value || obj.value || '' } field.setAttribute('style', inputStyle) if (!kb.updater) { throw new Error('kb has no updater') } if (!kb.updater.editable((doc as NamedNode).uri)) { field.readOnly = true // was: disabled. readOnly is better ;(field as any).style = style.textInputStyleUneditable + paramStyle if (suppressEmptyUneditable && field.value === '') { box.style.display = 'none' // clutter } return box } // read-write: field.addEventListener( 'keyup', function (_e) { if (params.pattern) { field.setAttribute( 'style', inputStyle + (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', inputStyle + 'color: gray;') // pending const ds = kb.statementsMatching(subject, property as any) // remove any multiple values let 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 Literal( field.value.trim(), undefined, ns.xsd(params.dt) ) } else { result = new Literal(field.value) } } let is = ds.map(statement => st(statement.subject, statement.predicate, result, statement.why)) // can include >1 doc if (is.length === 0) { // or none is = [st(subject, property as any, result, doc)] } function updateMany (ds, is: { why: { uri: string } }[], callback) { const docs: any[] = [] is.forEach(st => { if (!docs.includes(st.why.uri)) docs.push(st.why.uri) }) ds.forEach(st => { /* istanbul ignore next */ if (!docs.includes(st.why.uri)) docs.push(st.why.uri) }) /* istanbul ignore next */ if (docs.length === 0) { throw new Error('updateMany has no docs to patch') } if (!kb.updater) { throw new Error('kb has no updater') } if (docs.length === 1) { return kb.updater.update(ds, is as any, 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 as any, function (uri, ok, body) { if (ok) { updateMany(ds2, is2, callback) } else { callback(uri, ok, body) } }) } updateMany(ds, is as any, function (uri, ok, body) { // kb.updater.update(ds, is, function (uri, ok, body) { if (ok) { field.disabled = false field.setAttribute('style', inputStyle) } else { box.appendChild(errorMessageBlock(dom, body)) } callbackFunction(ok, body) }) }, true ) return box }