-
Notifications
You must be signed in to change notification settings - Fork 1
/
basic.ts
267 lines (249 loc) · 9.24 KB
/
basic.ts
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import { st, BlankNode, Literal, Node, NamedNode, Variable, Store } from 'rdflib'
import { solidLogicSingleton } from '../../logic'
import * as ns from '../../ns'
import { textInputSize, textInputStyle, textInputStyleUneditable, formFieldNameBoxWidth, formFieldNameBoxStyle } from '../../style'
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):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 = formFieldNameBoxWidth
const rhs = box.appendChild(dom.createElement('div'))
lhs.setAttribute('class', 'formFieldName')
lhs.setAttribute('style', formFieldNameBoxStyle)
rhs.setAttribute('class', 'formFieldValue')
if (!property) { // Assume more space for error on right
rhs.appendChild(errorMessageBlock(dom, 'No property given for form field: ' + form))
lhs.appendChild(dom.createTextNode('???'))
} else {
lhs.appendChild(fieldLabel(dom, property as NamedNode, form))
}
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
*/
// eslint-disable-next-line complexity
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 style = textInputStyle + paramStyle
const field = dom.createElement('input')
;(field as any).style = style
rhs.appendChild(field)
field.setAttribute('type', params.type ? params.type : 'text')
const size = kb.anyJS(form, ns.ui('size')) || textInputSize || 20
field.setAttribute('size', size)
const maxLength = kb.any(form, ns.ui('maxLength'))
field.setAttribute('maxLength', maxLength ? '' + maxLength : '4096')
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', style)
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 = textInputStyleUneditable + paramStyle
// backgroundColor = textInputBackgroundColorUneditable
if (suppressEmptyUneditable && field.value === '') {
box.style.display = 'none' // clutter
}
return box
}
// read-write:
field.addEventListener(
'keyup',
function (_e) {
if (params.pattern) {
field.setAttribute(
'style',
style +
(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', style + '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', style)
} else {
box.appendChild(errorMessageBlock(dom, body))
}
callbackFunction(ok, body)
})
},
true
)
return box
}