forked from SolidOS/solid-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathautocompleteField.ts
More file actions
224 lines (207 loc) · 9.75 KB
/
Copy pathautocompleteField.ts
File metadata and controls
224 lines (207 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/* Form field for doing autocompleete
*/
import * as ns from '../../../ns'
import { store } from 'solid-logic'
import * as widgets from '../../../widgets'
import * as style from '../../../style'
import { renderAutocompleteControl } from './autocompleteBar'
import { QueryParameters } from './publicData'
import { NamedNode, BlankNode, Literal, Variable, st } from 'rdflib'
import { AutocompleteOptions } from './autocompletePicker'
/**
* Render a autocomplete form field
*
* The autocomplete form searches for an object in a definitive public database,
* and allows the user to search for it by name, displaying a list of objects whose names match
* the input to date, and letting the user either click on one of the list,
* or just go on untill there is only one. The process then returns two values,
* the URiI of the object and its name.
*
* @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 so other parts can be refreshed.
*
* Form properties:
* @param ui:property The property to store the object itself
* @param ui:labelProperty The property used to store the name of the object
* @param ui:category The class of objects to be searched, if fixed (else dep on class of subject)
*
* @returns The HTML widget created
*/
// eslint-disable-next-line complexity
export function autocompleteField (
dom: HTMLDocument,
container: HTMLElement | undefined,
already,
subject: NamedNode | BlankNode | Variable,
form: NamedNode,
doc: NamedNode | undefined,
callbackFunction: (_ok: boolean, _errorMessage: string) => void
): HTMLElement {
async function addOneIdAndRefresh (result:NamedNode | Literal, name: Literal) {
if (!name) {
throw new Error('autocompleteField: No name set.')
}
const oldValue = kb.the(subject, property as any, null, doc)
if (oldValue) {
const oldName = kb.any(oldValue as any, labelProperty as any, null, doc)
if (oldValue.equals(result) && oldName && oldName.sameTerm(name)) {
// console.log('No change: same values.')
return
}
}
const deletables = oldValue
? kb.statementsMatching(subject, property as any, oldValue, doc)
.concat(kb.statementsMatching(oldValue as any, labelProperty as any, null, doc))
: []
// console.log('autocompleteField Deletables ' + deletables.map(st => st.toNT()))
const insertables = [st(subject, property as any, result, doc),
st(result as any, labelProperty as any, name, doc)] // @@ track the language of the name too!
// console.log(`AC form: ${deletables.length} to delete and ${insertables.length} to insert`)
try {
// console.log('@@@ AC updating ', deletables, insertables)
await kb.updater?.updateMany(deletables, insertables)
} catch (err) {
callbackFunction(false, err)
box.appendChild(widgets.errorMessageBlock(dom, 'Autocomplete form data update error:' + err, null, err))
return
}
callbackFunction(true, '')
}
async function deleteOne (_result:NamedNode | Literal, _name: Literal) {
const oldValue = kb.the(subject, property as any, null, doc)
if (!oldValue) {
callbackFunction(false, 'NO data to elete')
box.appendChild(widgets.errorMessageBlock(dom, 'Autocomplete delete: no old data!'))
return
}
// const oldName = kb.any(oldValue as any, labelProperty as any, null, doc)
const deletables = kb.statementsMatching(subject, property as any, oldValue, doc)
.concat(kb.statementsMatching(oldValue as any, labelProperty as any, null, doc))
// console.log('autocompleteField Deletables ' + deletables.map(st => st.toNT()))
const insertables = []
// console.log(`AC form delete: ${deletables.length} to delete and ${insertables.length} to insert`)
try {
// console.log('@@@ AC updating ', deletables, insertables)
await kb.updater?.updateMany(deletables, insertables)
} catch (err) {
const e2 = new Error('Autocomplete form data delete error:' + err)
callbackFunction(false, err)
box.appendChild(widgets.errorMessageBlock(dom, e2, null, err))
return
}
callbackFunction(true, '') // changed
}
if (subject.termType !== 'NamedNode') {
throw new Error('Sorry this field only works on NamedNode subjects (for editable)')
}
const kb = store
const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know
const box = dom.createElement('div')
if (container) container.appendChild(box)
const lhs = dom.createElement('div')
lhs.setAttribute('class', 'formFieldName')
lhs.setAttribute('style', style.formFieldNameBoxStyle)
box.appendChild(lhs)
const rhs = dom.createElement('div')
rhs.setAttribute('class', 'formFieldValue')
box.appendChild(rhs)
const property = kb.any(form, ns.ui('property'))
if (!property) {
return box.appendChild(
widgets.errorMessageBlock(dom, 'Error: No property given for autocomplete field: ' + form)
)
}
const labelProperty = kb.any(form, ns.ui('labelProperty')) || ns.schema('name')
// Parse the data source into query options
const dataSource = kb.any(form, ns.ui('dataSource')) as NamedNode | undefined
if (!dataSource) {
// console.log('@@ connectedStatements ACF ', kb.connectedStatements(form).map(x => x.toNT()).join('\n'))
return box.appendChild(
widgets.errorMessageBlock(dom, 'Error: No data source given for autocomplete field: ' + form)
)
}
const queryParams:QueryParameters = {
// targetClass: kb.any(dataSource, ns.ui('targetClass'), null, dataSource.doc()) as NamedNode | undefined,
label: kb.anyJS(dataSource, ns.schema('name'), null, dataSource.doc()),
logo: (kb.any(dataSource, ns.schema('logo'), null, dataSource.doc())) as NamedNode | undefined
}
// @@ Should we pass the target class in from the data source definition or use a current type of the subject
const targetClass = (kb.any(form, ns.ui('targetClass'), null, form.doc()) || // class in form takes pecedence
kb.any(dataSource, ns.ui('targetClass'), null, dataSource.doc())) as NamedNode | undefined
if (targetClass) {
queryParams.targetClass = targetClass
}
queryParams.objectURIBase = (kb.any(dataSource, ns.ui('objectURIBase'), null, dataSource.doc()) || undefined) as NamedNode | undefined
/*
if (!queryParams.targetClass) {
const klass = kb.any(subject, ns.rdf('type')) as NamedNode | undefined
// @@ be more selective of which class if many
// @@ todo: Take ALL classes, and compare them with those the data source knows about
// with translation where necessary. Find most specific of the classes for the search.
if (!klass) throw new Error('Autocomplete: No class specified or is current type of' + subject)
queryParams.targetClass = klass
}
*/
const endpoint = kb.anyJS(dataSource, ns.ui('endpoint'), null, dataSource.doc()) as string | undefined
if (endpoint) { // SPARQL
queryParams.endpoint = endpoint
queryParams.searchByNameQuery = kb.anyJS(dataSource, ns.ui('searchByNameQuery'), null, dataSource.doc())
if (!queryParams.searchByNameQuery) {
return box.appendChild(
widgets.errorMessageBlock(dom, 'Error: No searchByNameQuery given for endpoint data Source: ' + form))
}
queryParams.insitituteDetailsQuery = kb.anyJS(dataSource, ns.ui('insitituteDetailsQuery'), null, dataSource.doc())
} else {
// return box.appendChild(
// widgets.errorMessageBlock(dom, 'Error: No SPARQL endpoint given for autocomplete field: ' + form))
const searchByNameURI = kb.anyJS(dataSource, ns.ui('searchByNameURI'))
if (!searchByNameURI) {
return box.appendChild(
widgets.errorMessageBlock(dom, 'Error: No searchByNameURI OR sparql endpoint given for dataSource: ' + dataSource)
)
}
queryParams.searchByNameURI = searchByNameURI
}
// 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 editable = kb.updater?.editable((doc as NamedNode).uri)
const autocompleteOptions:AutocompleteOptions = { // cancelButton?: HTMLElement,
permanent: true,
targetClass: queryParams.targetClass, // @@ simplify?
queryParams
}
autocompleteOptions.size = kb.anyJS(form, ns.ui('size'), null, formDoc) || undefined
let obj = kb.any(subject, property as any, undefined, doc)
if (!obj) {
obj = kb.any(form, ns.ui('default'))
if (obj) {
autocompleteOptions.currentObject = obj as NamedNode
autocompleteOptions.currentName = kb.any(autocompleteOptions.currentObject, labelProperty, null, doc) as Literal
} else { // No data or default. Should we suprress the whole field?
if (suppressEmptyUneditable && !editable) {
box.style.display = 'none' // clutter removal
return box
}
}
} else { // get object and name from target data:
autocompleteOptions.currentObject = obj as NamedNode
autocompleteOptions.currentName = kb.any(autocompleteOptions.currentObject, labelProperty, null, doc) as Literal
}
lhs.appendChild(widgets.fieldLabel(dom, property as any, form))
const barOptions = {
editable,
dbLookup: true
}
renderAutocompleteControl(dom, subject as NamedNode, barOptions, autocompleteOptions, addOneIdAndRefresh, deleteOne).then((control) => {
rhs.appendChild(control)
}, (err) => {
rhs.appendChild(widgets.errorMessageBlock(dom, `Error rendering autocomplete ${form}: ${err}`, '#fee', err)) //
})
return box
}
// ends