UNPKG

solid-ui

Version:

UI library for Solid applications

263 lines • 10.5 kB
import { Literal, st } 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, kb, box, form, label) { // 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')), 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, property, fieldInQuestion) { let lab = store.any(fieldInQuestion, 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; 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, predicate, def) { 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, container, already, subject, form, doc, callbackFunction) { 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.style = inputStyle; rhs.appendChild(field); field.setAttribute('type', params.type ? params.type : 'text'); const fieldType = (field.getAttribute('type') || '').toLowerCase(); const deferWhileFocused = fieldType === 'date' || fieldType === 'datetime-local'; 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, doc); let obj = kb.any(subject, property, 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.uri)) { field.readOnly = true // was: disabled. readOnly is better ; field.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) { if (deferWhileFocused && dom.activeElement === field) { if (field.dataset) { field.dataset.deferredChange = 'true'; } return; } // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return; const disabledForSave = !deferWhileFocused; if (disabledForSave) { 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); // 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, result, doc)]; } function updateMany(ds, is, callback) { const docs = []; 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, 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 { callback(uri, ok, body); } }); } updateMany(ds, is, function (uri, ok, body) { // kb.updater.update(ds, is, function (uri, ok, body) { if (ok) { if (disabledForSave) { field.disabled = false; } field.setAttribute('style', inputStyle); } else { box.appendChild(errorMessageBlock(dom, body)); } callbackFunction(ok, body); }); }, true); field.addEventListener('blur', function (_e) { if (deferWhileFocused && field.dataset && field.dataset.deferredChange === 'true') { delete field.dataset.deferredChange; const event = new Event('change', { bubbles: true }); field.dispatchEvent(event); } }, true); return box; } //# sourceMappingURL=basic.js.map