Skip to content

Commit 2a495a1

Browse files
author
Tim Berners-Lee
committed
starting to connect the peices together
1 parent 669a483 commit 2a495a1

3 files changed

Lines changed: 149 additions & 103 deletions

File tree

src/widgets/forms/autocomplete/autocompleteField.ts

Lines changed: 81 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,28 @@ import { renderAutocompleteControl } from './autocompletBar'
88
import { NamedNode, BlankNode, Variable, st } from 'rdflib'
99

1010
/**
11-
* Render a autocomplete form field
12-
*
13-
* The same function is used for many similar one-value fields, with different
14-
* regexps used to validate.
15-
*
16-
* @param dom The HTML Document object aka Document Object Model
17-
* @param container If present, the created widget will be appended to this
18-
* @param already A hash table of (form, subject) kept to prevent recursive forms looping
19-
* @param subject The thing about which the form displays/edits data
20-
* @param form The form or field to be rendered
21-
* @param doc The web document in which the data is
22-
* @param callbackFunction Called when data is changed?
23-
*
24-
* @returns The HTML widget created
11+
* Render a autocomplete form field
12+
*
13+
* Teh autocomplete form searches for an iobject in a definitive public database,
14+
* and allows the user to search for it by name, displaying a list of objects whose names match
15+
* the input to date, and letting the user either click on one of the list,
16+
* or just go on untill there is only one. The process then returns two values,
17+
* the URiI of the object and its name.
18+
*
19+
* @param dom The HTML Document object aka Document Object Model
20+
* @param container If present, the created widget will be appended to this
21+
* @param already A hash table of (form, subject) kept to prevent recursive forms looping
22+
* @param subject The thing about which the form displays/edits data
23+
* @param form The form or field to be rendered
24+
* @param doc The web document in which the data is
25+
* @param callbackFunction Called when data is changed so other parts can be refreshed.
26+
*
27+
* Form properties:
28+
* @param ui:property The property to store the object itself
29+
* @param ui:labelProperty The property used to store the name of the object
30+
* @param ui:categoory The class of objects to be searched, if fixed (else dep on class of subject)
31+
*
32+
* @returns The HTML widget created
2533
*/
2634
// eslint-disable-next-line complexity
2735
export function autocompleteField (
@@ -34,15 +42,15 @@ export function autocompleteField (
3442
callbackFunction: (ok: boolean, errorMessage: string) => void
3543
): HTMLElement {
3644
async function addOneIdAndRefresh (result, _name) {
37-
const ds = kb.statementsMatching(subject, property as any) // remove any multiple values
45+
const deletables = kb.statementsMatching(subject, property as any, null, null) // remove any multiple values in any doc
3846

39-
let is = ds.map(statement => st(statement.subject, statement.predicate, result, statement.why)) // can include >1 doc
40-
if (is.length === 0) {
47+
let insertables = deletables.map(statement => st(statement.subject, statement.predicate, result, statement.why)) // can include >1 doc
48+
if (insertables.length === 0) {
4149
// or none
42-
is = [st(subject, property as any, result, doc)]
50+
insertables = [st(subject, property as any, result, doc)]
4351
}
4452
try {
45-
await kb.updater.updateMany(ds, is)
53+
await kb.updater.updateMany(deletables, insertables)
4654
} catch (err) {
4755
callbackFunction(false, err)
4856
box.appendChild(widgets.errorMessageBlock(dom, 'Autocomplete form data write error:' + err))
@@ -66,70 +74,87 @@ export function autocompleteField (
6674

6775
const property = kb.any(form, ns.ui('property'))
6876
if (!property) {
69-
box.appendChild(
77+
return box.appendChild(
7078
dom.createTextNode('Error: No property given for autocomplete field: ' + form)
7179
)
72-
return box
7380
}
81+
const labelProperty = kb.any(form, ns.ui('labelProperty')) || ns.schema('name')
82+
7483
const searchClass = kb.any(form, ns.ui('searchClass'))
7584
if (!searchClass) {
7685
return box.appendChild(
7786
dom.createTextNode('Error: No searchClass given for autocomplete field: ' + form)
7887
)
7988
}
80-
const endPoint = kb.any(form, ns.ui('endPoint'))
81-
if (!endPoint) {
89+
// Parse the data source into query options
90+
91+
const dataSource = kb.any(form, ns.ui('dataSource'))
92+
if (!dataSource) {
8293
return box.appendChild(
83-
dom.createTextNode('Error: No SPARQL endPoint given for autocomplete field: ' + form)
94+
dom.createTextNode('Error: No data source given for autocomplete field: ' + form)
8495
)
8596
}
97+
const queryParams = {} // @@ const?
98+
queryParams.targetClass = kb.any(dataSource, ns.ui('targetClass'), null, dataSource.doc()) // Different ontology?
99+
queryParams.name = kb.anyJS(dataSource, ns.schema('name'), null, dataSource.doc()) // Different ontology?
100+
queryParams.logo = kb.anyJS(dataSource, ns.schema('logo'), null, dataSource.doc()) // Different ontology?
101+
102+
if (!queryParams.targetClass) {
103+
queryParams.targetClass = kb.any(subject, ns.rdf('type')) // @@ be more selective of which class if many
104+
}
105+
const endPoint = kb.any(dataSource, ns.ui('endPoint'), null, dataSource.doc())
106+
if (endPoint) { // SPARQL
107+
queryParams.endpoint = endPoint
108+
109+
queryParams.searchByNameQuery = kb.the(dataSource, ns.ui('searchByNameQuery'), null, dataSource.doc())
110+
if (!queryParams.searchByNameQuery) {
111+
return box.appendChild(
112+
dom.createTextNode('Error: No searchByNameQuery given for data Source: ' + form))
113+
}
114+
queryParams.insitituteDetailsQuery = kb.any(dataSource, ns.ui('insitituteDetailsQuery'), null, dataSource.doc())
115+
} else {
116+
return box.appendChild(
117+
dom.createTextNode('Error: No SPARQL endPoint given for autocomplete field: ' + form))
118+
}
86119
const queryTemplate = kb.any(form, ns.ui('queryTemplate'))
87120
if (!queryTemplate) {
88-
box.appendChild(
121+
return box.appendChild(
89122
dom.createTextNode('Error: No queryTemplate given for autocomplete field: ' + form)
90123
)
91-
return box
92124
}
93125
// It can be cleaner to just remove empty fields if you can't edit them anyway
94126
const suppressEmptyUneditable = kb.anyJS(form, ns.ui('suppressEmptyUneditable'), null, formDoc)
95127
const editable = kb.updater.editable((doc as NamedNode).uri)
96-
lhs.appendChild(widgets.fieldLabel(dom, property as any, form))
97-
const uri = widgets.mostSpecificClassURI(form)
98-
let params = widgets.fieldParams[uri]
99-
if (params === undefined) params = {} // non-bottom field types can do this
100-
// const theStyle = params.style || style.textInputStyle
101-
const klass = kb.the(form, ns.ui('category'), null, formDoc)
102-
/*
103-
{ label: string;
104-
logo: string;
105-
searchByNameQuery?: string;
106-
searchByNameURI?: string;
107-
insitituteDetailsQuery?: string;
108-
endPoint?: string;
109-
class: object
110-
}
111-
*/
112-
113-
const searchByNameQuery = kb.the(form, ns.ui('searchByNameQuery'), null, formDoc)
114-
const queryParams = {
115-
label: 'from form',
116-
logo: '',
117-
class: klass,
118-
endPoint: endPoint.uri,
119-
searchByNameQuery
120-
}
121128

122129
const options = { // cancelButton?: HTMLElement,
123130
// acceptButton?: HTMLElement,
124-
class: klass,
131+
targetClass: dataSource.targetClass, // @@ simplify?
125132
queryParams
126133
}
134+
let obj = kb.any(subject, property as any, undefined, doc)
135+
if (!obj) {
136+
obj = kb.any(form, ns.ui('default'))
137+
if (obj) {
138+
options.currentObject = obj
139+
options.currentName = kb.the(obj, labelProperty, null, form.doc())
140+
} else { // No data or default. Should we suprress the whole field?
141+
if (suppressEmptyUneditable && !editable) {
142+
box.style.display = 'none' // clutter removal
143+
return box
144+
}
145+
}
146+
} else { // get object and name from target data:
147+
options.currentObject = obj
148+
options.currentName = kb.the(obj, labelProperty, null, subject.doc())
149+
}
127150

128-
rhs.appendChild(await renderAutocompleteControl(dom, subject, options, addOneIdAndRefresh))
151+
lhs.appendChild(widgets.fieldLabel(dom, property as any, form))
129152

130-
// @@ set existing value is any
131-
// renderAutoComplete(dom, options, addOneIdAndRefresh).then(acWiget => rhs.appendChild(acWiget))
153+
// const searchByNameQuery = kb.the(form, ns.ui('searchByNameQuery'), null, formDoc)
132154

155+
rhs.appendChild(await renderAutocompleteControl(dom, subject, options, addOneIdAndRefresh))
156+
157+
/*
133158
const field = dom.createElement('input')
134159
;(field as any).style = style.textInputStyle // Do we have to override length etc?
135160
rhs.appendChild(field)
@@ -144,28 +169,7 @@ export function autocompleteField (
144169
field.setAttribute('maxLength', maxLength ? '' + maxLength : '4096')
145170
146171
doc = doc || widgets.fieldStore(subject, property as any, doc)
147-
148-
let obj = kb.any(subject, property as any, undefined, doc)
149-
if (!obj) {
150-
obj = kb.any(form, ns.ui('default'))
151-
}
152-
if (obj) {
153-
field.value = obj.value || ''
154-
}
155-
field.setAttribute('style', style)
156-
if (!kb.updater) {
157-
throw new Error('kb has no updater')
158-
}
159-
if (!editable) {
160-
field.readOnly = true // was: disabled. readOnly is better
161-
;(field as any).style = style.textInputStyleUneditable
162-
// backgroundColor = textInputBackgroundColorUneditable
163-
if (suppressEmptyUneditable && field.value === '') {
164-
box.style.display = 'none' // clutter
165-
}
166-
return box
167-
}
168-
172+
*/
169173
return box
170174
}
171175

src/widgets/forms/autocomplete/autocompletePicker.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,23 @@ const AUTOCOMPLETE_DEBOUNCE_MS = 300
2121
// const autocompleteRowStyle = 'border: 0.2em solid straw;' // @@ white
2222

2323
/*
24-
Autocomplete happens in four phases:
24+
Autocomplete happens in 6 phases:
2525
1. The search string is too small to bother
2626
2. The search string is big enough, and we have not loaded the array
2727
3. The search string is big enough, and we have loaded array up to the limit
2828
Display them and wait for more user input
2929
4. The search string is big enough, and we have loaded array NOT to the limit
3030
but including all matches. No more fetches.
3131
If user gets more precise, wait for them to select one - or reduce to a single
32-
5. Optionally waiting for accept button to be pressed
32+
5. Single one selected. Optionally waiting for accept button to be pressed, or can change string and go to 5 or 2
33+
6. Locked with a value. Press 'edit' button to return to 5
3334
*/
3435

3536
type AutocompleteOptions = { cancelButton?: HTMLElement,
3637
acceptButton?: HTMLElement,
3738
class: NamedNode,
39+
currentObject: NamedNode,
40+
currentLabel: string,
3841
queryParams: QueryParameters }
3942

4043
interface Callback1 {
@@ -147,6 +150,7 @@ export async function renderAutoComplete (dom: HTMLDocument, options:Autocomplet
147150
}
148151
inputEventHandlerLock = true
149152
const languagePrefs = await getPreferredLanguages()
153+
const language = languagePrefs[0] // if have to pick one
150154
const filter = searchInput.value.trim().toLowerCase()
151155
if (filter.length < AUTOCOMPLETE_THRESHOLD) { // too small
152156
clearList()
@@ -160,7 +164,7 @@ export async function renderAutoComplete (dom: HTMLDocument, options:Autocomplet
160164
}
161165
let bindings
162166
try {
163-
bindings = await queryPublicDataByName(filter, OrgClass, options.queryParams)
167+
bindings = await queryPublicDataByName(filter, OrgClass, languagePrefs, options.queryParams)
164168
// bindings = await queryDbpedia(sparql)
165169
} catch (err) {
166170
complain('Error querying db of organizations: ' + err)
@@ -225,6 +229,12 @@ export async function renderAutoComplete (dom: HTMLDocument, options:Autocomplet
225229
const cell = head.appendChild(dom.createElement('td'))
226230
const searchInput = cell.appendChild(dom.createElement('input'))
227231
searchInput.setAttribute('type', 'text')
232+
if (options.currentObject) { // If have existing value then jump into the endgame of the autocomplete
233+
searchInput.value = options.currentLabel || ''
234+
foundName = options.currentLabel
235+
lastFilter = options.currentLabel
236+
foundObject = options.currentObject
237+
}
228238
const searchInputStyle = style.searchInputStyle ||
229239
'border: 0.1em solid #444; border-radius: 0.5em; width: 100%; font-size: 100%; padding: 0.1em 0.6em' // @
230240
searchInput.setAttribute('style', searchInputStyle)

src/widgets/forms/autocomplete/publicData.ts

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export const wikidataClasses = {
5454
SportsOrganization: 'http://www.wikidata.org/entity/Q4438121'
5555
}
5656

57+
export const fetcherOptionsJsonPublicData = {
58+
credentials: 'omit', // try to avoid CORS problems. Data is public so no auth
59+
headers: { Accept: 'application/json' }
60+
}
61+
5762
export async function getPreferredLanguages () {
5863
return ['fr', 'en', 'de', 'it'] // @@ testing only -- code me later
5964
}
@@ -63,6 +68,7 @@ export const escoParameters:QueryParameters = {
6368
searchByNameQuery: null, // No sparql endpoint
6469
searchByNameURI: 'https://ec.europa.eu/esco/api/search?language=$(language)&type=occupation&text=$(name)',
6570
endpoint: null,
71+
returnFormat: 'ESCO',
6672
class: {}
6773
}
6874

@@ -225,6 +231,15 @@ export function loadFromBindings (kb, solidSubject:NamedNode, bindings, doc, pre
225231

226232
/* ESCO sopecific
227233
*/
234+
export function ESCOResultToBindings (json: Object): Bindings {
235+
const results = json._embedded.results // Array
236+
const bindings = results.map(result => {
237+
const name = result.title
238+
const uri = result.uri // like http://data.europa.eu/esco/occupation/57af9090-55b4-4911-b2d0-86db01c00b02
239+
return { name: { value: name, type: 'literal' }, uri: { type: 'IRI', value: uri } } // simulate SPARQL bindings
240+
})
241+
return bindings
242+
}
228243

229244
/* Query all entities of given class and partially matching name
230245
*/
@@ -235,39 +250,54 @@ export async function queryESCODataByName (filter: string, theClass:NamedNode, q
235250
.replace('$(class)', theClass)
236251
console.log('Querying ESCO data - uri: ' + queryURI)
237252

238-
const options = {
239-
credentials: 'omit',
240-
headers: { Accept: 'application/json' }
241-
} // CORS
242-
const response = await kb.fetcher.webOperation('GET', queryURI, options)
243-
// complain('Error querying db of organizations: ' + err)
253+
const response = await kb.fetcher.webOperation('GET', queryURI, fetcherOptionsJsonPublicData)
244254
const text = response.responseText
245255
console.log(' Query result text' + text.slice(0, 500) + '...')
246256
if (text.length === 0) throw new Error('Wot no text back from ESCO query ' + queryURI)
247257
const json = JSON.parse(text)
248-
console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0, 500) + '...')
249-
250-
const results = json._embedded.results // Array
251-
const bindings = results.map(result => {
252-
const name = result.title
253-
const uri = result.uri // like http://data.europa.eu/esco/occupation/57af9090-55b4-4911-b2d0-86db01c00b02
254-
return { name: { value: name, type: 'literal' }, uri: { type: 'IRI', value: uri } } // simulate SPARQL bindings
255-
})
256-
return bindings
257-
// return queryPublicDataSelect(sparql, queryTarget)
258+
console.log(' ESCO Query result JSON' + JSON.stringify(json, null, 4).slice(0, 500) + '...')
259+
return ESCOResultToBindings(json)
258260
}
259261

260262
/* Query all entities of given class and partially matching name
261263
*/
262-
export async function queryPublicDataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
263-
const sparql = queryTarget.searchByNameQuery
264-
.replace('$(name)', filter)
265-
.replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
266-
.replace('$(class)', theClass)
267-
console.log('Querying public data - sparql: ' + sparql)
268-
return queryPublicDataSelect(sparql, queryTarget)
264+
export async function queryPublicDataByName (
265+
filter: string,
266+
theClass:NamedNode,
267+
languages: Array<string>,
268+
queryTarget: QueryParameters): Promise<Bindings> {
269+
function substituteStrings (template: string):string {
270+
return template.replace('$(name)', filter)
271+
.replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
272+
.replace('$(class)', theClass)
273+
}
274+
if (queryTarget.searchByNameQuery) {
275+
const sparql = substituteStrings(queryTarget.searchByNameQuery)
276+
console.log('Querying public data - sparql: ' + sparql)
277+
return queryPublicDataSelect(sparql, queryTarget)
278+
} else if (queryTarget.searchByNameURI) { // not sparql - random API
279+
const queryURI = substituteStrings(queryTarget.searchByNameURI)
280+
const response = await kb.fetcher.webOperation('GET', queryURI, fetcherOptionsJsonPublicData)
281+
const text = response.responseText
282+
console.log(' Query result text' + text.slice(0, 500) + '...')
283+
if (text.length === 0) throw new Error('Wot no text back from ESCO query ' + queryURI)
284+
const json = JSON.parse(text)
285+
console.log(' API Query result JSON' + JSON.stringify(json, null, 4).slice(0, 500) + '...')
286+
if (json._embedded) {
287+
console.log(' Looks like ESCO')
288+
const bindings = ESCOResultToBindings(json)
289+
return bindings
290+
} else {
291+
alert('Code me: unrecognized API return format')
292+
console.log('*** Need to add code to parse unrecognized API JSON return\n' + JSON.stringify(json), null, 4)
293+
}
294+
} else {
295+
throw new Error('Query source must have either rest API or SPARQL endpoint.')
296+
}
269297
}
270298

299+
/* Query a database using SPARQL SELECT
300+
*/
271301
export async function queryPublicDataSelect (sparql: string, queryTarget: QueryParameters): Promise<Bindings> {
272302
const myUrlWithParams = new URL(queryTarget.endpoint)
273303
myUrlWithParams.searchParams.append('query', sparql)
@@ -289,6 +319,8 @@ export async function queryPublicDataSelect (sparql: string, queryTarget: QueryP
289319
return bindings
290320
}
291321

322+
/* Load from a database using SPARQL CONSTRUCT
323+
*/
292324
export async function queryPublicDataConstruct (sparql: string, pubicId: NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
293325
console.log('queryPublicDataConstruct: sparql:', sparql)
294326
const myUrlWithParams = new URL(queryTarget.endpoint)

0 commit comments

Comments
 (0)