diff --git a/src/primitives/components/rdf-form/RDFForm.ts b/src/primitives/components/rdf-form/RDFForm.ts new file mode 100644 index 000000000..0614c4358 --- /dev/null +++ b/src/primitives/components/rdf-form/RDFForm.ts @@ -0,0 +1,108 @@ +import { customElement, property, state } from 'lit/decorators.js' +import { html } from 'lit/html.js' +import WebComponent from '../../lib/WebComponent' +import ns from '../../../lib/ns' +import { loadDocument, sortBySequence } from '../../lib/rdfFormsHelper' +import { sym, Namespace } from 'rdflib' +import { store } from 'solid-logic' + +@customElement('solid-ui-rdf-form') +export default class RDFForm extends WebComponent { + @state() + private accessor _parsedUrl: URL | null = null + + @property({ type: String }) + accessor whichForm = 'this' + + @property({ type: String }) + accessor rdfTurtleFormatSource = '' + + @property({ type: String }) + accessor rdfName = '' + + @property({ type: String }) + set rdfURI (value: string) { + try { + this._parsedUrl = new URL(value) + } catch { + this._parsedUrl = null // Handle invalid URL + } + } + + get rdfURI (): string { + return this._parsedUrl ? this._parsedUrl.href : '' + } + + render () { + // TODO: detect format + loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) + const document = sym(this.rdfURI) // rdflib NamedNode for the document + const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file + const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form + console.log('formThis:', formThis.value) + + const parts = store.each(formThis, ns.ui('parts'), null, document) + const partsBySequence = sortBySequence(store, parts) + const partItems = (partsBySequence || []).flatMap(item => { + if (item && typeof item === 'object' && 'elements' in item && Array.isArray((item as any).elements)) { + return (item as any).elements + } + return [item] + }) + const uiFields = partItems.map(item => { + const types = store.each(item as any, ns.rdf('type'), null, document) + const typeNode = types[0] + const value = typeNode ? ((typeNode as any).value || String(typeNode)) : ((item as any).value || String(item)) + const hashIndex = value.lastIndexOf('#') + return hashIndex >= 0 ? value.slice(hashIndex + 1) : value + }) + console.log('parts:', parts) + console.log('partsBySequence:', partsBySequence) + console.log('partItems:', partItems) + console.log('document:', document) + console.log('exactForm:', exactForm) + console.log('uiFields:', uiFields) + + return html` + ${uiFields.map(part => { + switch (part) { + case 'PhoneField': + case 'EmailField': + case 'ColorField': + case 'DateField': + case 'DateTimeField': + case 'TimeField': + case 'NumericField': + case 'IntegerField': + case 'DecimalField': + case 'FloatField': + case 'TextField': + case 'SingleLineTextField': + case 'NamedNodeURIField': + return html`` + case 'MultiLineTextField': + return html`` + case 'BooleanField': + return html`` + case 'TristateField': + return html`` + case 'Classifier': + return html`` + case 'Choice': + return html`` + case 'Multiple': + return html`` + case 'Options': + return html`` + case 'AutocompleteField': + return html`` + case 'Comment': + case 'Heading': + return html`` + default: + return html`
Unknown part type: ${part}
` + } + })} + ` + } +} diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts new file mode 100644 index 000000000..bcbfeea1d --- /dev/null +++ b/src/primitives/components/rdf-form/RDForm.stories.ts @@ -0,0 +1,92 @@ +import { html } from 'lit' +import { defineStoryRender } from '../../../storybook' +import './RDFForm' + +const meta = { + title: 'Design System/RDF Form', + args: { + rdfTurtleFormatSource: ` + @prefix : . + @prefix dc: . + @prefix ui: . + @prefix vcard: . + + # A Form with 2 fields and a nested subgroup + + :form a ui:Form; + ui:parts (:nameField :emailField :addresses) . + + :nameField a ui:SingleLineTextField ; + ui:property vcard:fn; + ui:label "name" . + + :emailField a ui:EmailField ; + ui:property vcard:hasEmail; # @@ check + ui:label "email" . + + :addresses + a ui:Multiple ; # -- Allows zero or one or more + ui:part :oneAddress ; + ui:property vcard:hasAddress . + + :oneAddress + a ui:Group ; # A subgroup of the main form + ui:parts ( :street :locality :postcode :region :country ). + + :street + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:street-address ; + ui:size "40" . + + :locality + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:locality ; + ui:size "40" . + + :postcode + a ui:SingleLineTextField ; + ui:maxLength "25" ; + ui:property vcard:postal-code ; + ui:size "25" . + + :region + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:region ; + ui:size "40" . + + :country + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:country-name ; + ui:size "40" . + `, + rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', // we need a working URL + whichForm: 'form', + rdfName: 'dummyFormTestFile.ttl' + }, + + argTypes: { + rdfTurtleFormatSource: { control: 'text' }, + rdfURI: { control: 'text' }, + whichForm: { control: 'text' }, + rdfName: { control: 'text' } + }, +} as const + +const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName }) => { + return html` + + + ` +}) + +export default meta + +export const Primary = { render } diff --git a/src/primitives/components/rdf-input/RDFInput.ts b/src/primitives/components/rdf-input/RDFInput.ts new file mode 100644 index 000000000..426d4c5b4 --- /dev/null +++ b/src/primitives/components/rdf-input/RDFInput.ts @@ -0,0 +1,41 @@ +import { customElement, property } from 'lit/decorators.js' +import { html } from 'lit/html.js' +import ns from '../../../lib/ns' +import WebComponent from '../../../primitives/lib/WebComponent' +import { store } from 'solid-logic' +import { NamedNode, Namespace, sym } from 'rdflib' +import { label } from '../../../utils' +import { loadDocument } from '../../lib/rdfFormsHelper' + +// import '../input' + +@customElement('solid-ui-rdf-input') +export default class RDFInput extends WebComponent { + // example RDF Turtle format source: + // :nameField a ui:SingleLineTextField ; + // ui:property vcard:fn; + // ui:label "name" . + + // form here is the subject :nameField + @property({ type: String }) + accessor rdf = '' + + render () { + const exactForm = this.whichForm // nameField + const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form + const document = sym(this.rdfURI) + + const uiProperty = label(store.any(formThis, ns.ui('property')), true) as NamedNode | undefined + const uiLabel = store.any(formThis, ns.ui('label')) + const inputLabel = uiLabel ? uiLabel.value : uiProperty ? uiProperty.value.split('#').pop() : 'Input' + + // TODO: I am not finding suppressEmptyUneditable in ui ontology + const suppressEmptyUneditable = store.anyJS(formThis, ns.ui('suppressEmptyUneditable'), null, document) + + const uri = mostSpecificClassURI(form) + let params = fieldParams[uri] + + return html` + + ` +} diff --git a/src/primitives/lib/rdfFormsHelper.ts b/src/primitives/lib/rdfFormsHelper.ts new file mode 100644 index 000000000..c4ff20879 --- /dev/null +++ b/src/primitives/lib/rdfFormsHelper.ts @@ -0,0 +1,55 @@ +import { sym, LiveStore, parse, NamedNode } from 'rdflib' +import ns from '../../lib/ns' +import { label } from '../../utils' + +const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/' + +// we need to load into the store some additional information about Social Media accounts +export function loadDocument ( + store: LiveStore, + documentSource: string, + documentName: string, + documentURI?: string +) { + const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file + const document = sym(finalDocumentUri) // rdflib NamedNode for the document + + if (!store.holds(undefined, undefined, undefined, document)) { + // we are using the social media form because it contains the information we need + // the form can be used for both use cases: create UI for edit and render UI for display + parse(documentSource, store, finalDocumentUri, 'text/turtle', () => null) // Load doc directly + } +} + +export function sortBySequence ( + store: LiveStore, + list +) { + const subfields = list.map(function (p) { + const k = store.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] + }) +} + +/** + * Which class of field is this? Relies on http://www.w3.org/2000/01/rdf-schema#subClassOf and + * https://linkeddata.github.io/rdflib.js/doc/classes/formula.html#bottomtypeuris + * to find the most specific RDF type if there are multiple. + * + * @param x a form field, e.g. `namedNode('https://timbl.com/timbl/Public/Test/Forms/individualForm.ttl#fullNameField')` + * @returns the URI of the most specific known class, e.g. `http://www.w3.org/ns/ui#SingleLineTextField` + */ +export function mostSpecificClassURI (store: LiveStore,x: Node): string { + const ft = store.findTypeURIs(x as any) + const bot = store.bottomTypeURIs(ft) // most specific + const bots: any[] = [] + for (const b in bot) bots.push(b) + // if (bots.length > 1) throw "Didn't expect "+x+" to have multiple bottom types: "+bots + return bots[0] +}