@ulb-darmstadt/shacl-form 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/config.d.ts +4 -5
- package/dist/constants.d.ts +15 -12
- package/dist/constraints.d.ts +5 -4
- package/dist/exports.d.ts +2 -1
- package/dist/form-bootstrap.d.ts +1 -1
- package/dist/form-bootstrap.js +361 -2
- package/dist/form-default.d.ts +1 -1
- package/dist/form-default.js +350 -2
- package/dist/form-material.d.ts +1 -1
- package/dist/form-material.js +670 -2
- package/dist/form.d.ts +3 -2
- package/dist/node-template.d.ts +17 -0
- package/dist/node.d.ts +5 -1
- package/dist/plugins/leaflet.d.ts +2 -4
- package/dist/plugins/leaflet.js +720 -2
- package/dist/plugins/mapbox.d.ts +2 -2
- package/dist/plugins/mapbox.js +2764 -2
- package/dist/property-template.d.ts +2 -1
- package/dist/property.d.ts +6 -2
- package/dist/theme.d.ts +2 -2
- package/dist/themes/default.d.ts +3 -3
- package/dist/themes/material.d.ts +2 -3
- package/package.json +26 -14
- package/src/config.ts +11 -10
- package/src/constants.ts +4 -1
- package/src/constraints.ts +75 -31
- package/src/exports.ts +2 -1
- package/src/form.ts +32 -17
- package/src/group.ts +1 -1
- package/src/loader.ts +12 -13
- package/src/node-template.ts +82 -0
- package/src/node.ts +88 -63
- package/src/plugins/leaflet.ts +2 -2
- package/src/plugins/mapbox.ts +4 -4
- package/src/property-template.ts +19 -8
- package/src/property.ts +168 -63
- package/src/serialize.ts +14 -1
- package/src/styles.css +8 -10
- package/src/theme.ts +5 -5
- package/src/themes/bootstrap.ts +1 -1
- package/src/themes/default.css +2 -2
- package/src/themes/default.ts +12 -3
- package/src/themes/material.ts +12 -3
- package/src/util.ts +12 -14
- package/dist/form-bootstrap.js.LICENSE.txt +0 -69
- package/dist/form-default.js.LICENSE.txt +0 -69
- package/dist/form-material.js.LICENSE.txt +0 -69
- package/dist/plugins/file-upload.js +0 -1
- package/dist/plugins/fixed-list.js +0 -1
- package/dist/plugins/leaflet.js.LICENSE.txt +0 -4
- package/dist/plugins/mapbox.js.LICENSE.txt +0 -10
package/src/property.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
|
|
1
|
+
import { BlankNode, DataFactory, NamedNode, Quad, Store } from 'n3'
|
|
2
2
|
import { Term } from '@rdfjs/types'
|
|
3
3
|
import { ShaclNode } from './node'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createShaclOrConstraint, resolveShaclOrConstraintOnProperty } from './constraints'
|
|
5
|
+
import { findInstancesOf, focusFirstInputElement } from './util'
|
|
6
6
|
import { Config } from './config'
|
|
7
7
|
import { ShaclPropertyTemplate } from './property-template'
|
|
8
|
-
import { Editor, fieldFactory } from './theme'
|
|
8
|
+
import { Editor, fieldFactory, InputListEntry } from './theme'
|
|
9
9
|
import { toRDF } from './serialize'
|
|
10
10
|
import { findPlugin } from './plugin'
|
|
11
|
-
import { RDF_PREDICATE_TYPE, SHACL_PREDICATE_TARGET_CLASS } from './constants'
|
|
11
|
+
import { DATA_GRAPH, RDF_PREDICATE_TYPE, SHACL_PREDICATE_TARGET_CLASS } from './constants'
|
|
12
|
+
import { RokitButton, RokitSelect } from '@ro-kit/ui-widgets'
|
|
12
13
|
|
|
13
14
|
export class ShaclProperty extends HTMLElement {
|
|
14
15
|
template: ShaclPropertyTemplate
|
|
15
|
-
addButton:
|
|
16
|
+
addButton: RokitSelect | undefined
|
|
16
17
|
|
|
17
18
|
constructor(shaclSubject: BlankNode | NamedNode, parent: ShaclNode, config: Config, valueSubject?: NamedNode | BlankNode) {
|
|
18
19
|
super()
|
|
19
|
-
this.template = new ShaclPropertyTemplate(config.
|
|
20
|
+
this.template = new ShaclPropertyTemplate(config.store.getQuads(shaclSubject, null, null, null), parent, config)
|
|
20
21
|
|
|
21
22
|
if (this.template.order !== undefined) {
|
|
22
23
|
this.style.order = `${this.template.order}`
|
|
@@ -25,63 +26,48 @@ export class ShaclProperty extends HTMLElement {
|
|
|
25
26
|
this.classList.add(this.template.cssClass)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
if (config.editMode) {
|
|
29
|
-
this.addButton =
|
|
30
|
-
this.addButton.innerText = this.template.label
|
|
31
|
-
this.addButton.title = 'Add ' + this.template.label
|
|
32
|
-
this.addButton.classList.add('control-button', 'add-button')
|
|
33
|
-
this.addButton.addEventListener('click', _ => {
|
|
34
|
-
const instance = this.addPropertyInstance()
|
|
35
|
-
instance.classList.add('fadeIn')
|
|
36
|
-
this.updateControls()
|
|
37
|
-
focusFirstInputElement(instance)
|
|
38
|
-
setTimeout(() => {
|
|
39
|
-
instance.classList.remove('fadeIn')
|
|
40
|
-
}, 200)
|
|
41
|
-
})
|
|
29
|
+
if (config.editMode && !parent.linked) {
|
|
30
|
+
this.addButton = this.createAddButton()
|
|
42
31
|
this.appendChild(this.addButton)
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
// bind existing values
|
|
46
35
|
if (this.template.path) {
|
|
47
|
-
|
|
36
|
+
let values: Quad[] = []
|
|
37
|
+
if (valueSubject) {
|
|
38
|
+
if (parent.linked) {
|
|
39
|
+
// for linked resource, get values in all graphs
|
|
40
|
+
values = config.store.getQuads(valueSubject, this.template.path, null, null)
|
|
41
|
+
} else {
|
|
42
|
+
// get values only from data graph
|
|
43
|
+
values = config.store.getQuads(valueSubject, this.template.path, null, DATA_GRAPH)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
48
46
|
let valuesContainHasValue = false
|
|
49
47
|
for (const value of values) {
|
|
50
48
|
// ignore values that do not conform to this property.
|
|
51
49
|
// this might be the case when there are multiple properties with the same sh:path in a NodeShape.
|
|
52
|
-
if (this.
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < targetClasses.length && !hasTargetClass; i++) {
|
|
57
|
-
if (config.dataGraph.getQuads(value.object, RDF_PREDICATE_TYPE, targetClasses[i], null).length > 0) {
|
|
58
|
-
hasTargetClass = true
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (!hasTargetClass) {
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
50
|
+
if (this.isValueValid(value.object)) {
|
|
51
|
+
this.addPropertyInstance(value.object)
|
|
52
|
+
if (this.template.hasValue && value.object.equals(this.template.hasValue)) {
|
|
53
|
+
valuesContainHasValue = true
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
|
-
this.addPropertyInstance(value.object)
|
|
67
|
-
if (this.template.hasValue && value.object.equals(this.template.hasValue)) {
|
|
68
|
-
valuesContainHasValue = true
|
|
69
|
-
}
|
|
70
56
|
}
|
|
71
|
-
if (config.editMode && this.template.hasValue && !valuesContainHasValue) {
|
|
57
|
+
if (config.editMode && this.template.hasValue && !valuesContainHasValue && !parent.linked) {
|
|
72
58
|
// sh:hasValue is defined in shapes graph, but does not exist in data graph, so force it
|
|
73
59
|
this.addPropertyInstance(this.template.hasValue)
|
|
74
60
|
}
|
|
75
61
|
}
|
|
76
62
|
|
|
77
|
-
if (config.editMode) {
|
|
63
|
+
if (config.editMode && !parent.linked) {
|
|
78
64
|
this.addEventListener('change', () => { this.updateControls() })
|
|
79
65
|
this.updateControls()
|
|
80
66
|
}
|
|
81
67
|
|
|
82
|
-
if (this.template.extendedShapes
|
|
68
|
+
if (this.template.extendedShapes.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
|
|
83
69
|
// in view mode, show collapsible only when we have something to show
|
|
84
|
-
if (config.editMode || this.childElementCount > 0) {
|
|
70
|
+
if ((config.editMode && !parent.linked) || this.childElementCount > 0) {
|
|
85
71
|
const collapsible = this
|
|
86
72
|
collapsible.classList.add('collapsible')
|
|
87
73
|
if (this.template.config.attributes.collapse === 'open') {
|
|
@@ -100,27 +86,43 @@ export class ShaclProperty extends HTMLElement {
|
|
|
100
86
|
|
|
101
87
|
addPropertyInstance(value?: Term): HTMLElement {
|
|
102
88
|
let instance: HTMLElement
|
|
103
|
-
if (this.template.shaclOr?.length) {
|
|
89
|
+
if (this.template.shaclOr?.length || this.template.shaclXone?.length) {
|
|
90
|
+
const options = this.template.shaclOr?.length ? this.template.shaclOr : this.template.shaclXone as Term[]
|
|
91
|
+
let resolved = false
|
|
104
92
|
if (value) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
93
|
+
const resolvedOptions = resolveShaclOrConstraintOnProperty(options, value, this.template.config)
|
|
94
|
+
if (resolvedOptions.length) {
|
|
95
|
+
instance = createPropertyInstance(this.template.clone().merge(resolvedOptions), value, true)
|
|
96
|
+
resolved = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
instance = createShaclOrConstraint(options, this, this.template.config)
|
|
108
101
|
appendRemoveButton(instance, '')
|
|
109
102
|
}
|
|
110
103
|
} else {
|
|
111
|
-
|
|
104
|
+
// check if value is part of the data graph. if not, create a linked resource
|
|
105
|
+
let linked = false
|
|
106
|
+
if (value) {
|
|
107
|
+
const clazz = this.getRdfClassToLinkOrCreate()
|
|
108
|
+
if (clazz && this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, clazz, DATA_GRAPH) === 0) {
|
|
109
|
+
// value is not in data graph, so must be a link in the shapes graph
|
|
110
|
+
linked = true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
instance = createPropertyInstance(this.template, value, undefined, linked || this.template.parent.linked)
|
|
112
114
|
}
|
|
113
|
-
if (this.
|
|
114
|
-
this.insertBefore(instance
|
|
115
|
+
if (this.addButton) {
|
|
116
|
+
this.insertBefore(instance!, this.addButton)
|
|
115
117
|
} else {
|
|
116
|
-
this.appendChild(instance)
|
|
118
|
+
this.appendChild(instance!)
|
|
117
119
|
}
|
|
118
|
-
return instance
|
|
120
|
+
return instance!
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
updateControls() {
|
|
122
124
|
let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
123
|
-
if (instanceCount === 0 && (!this.template.extendedShapes
|
|
125
|
+
if (instanceCount === 0 && (!this.template.extendedShapes.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
|
|
124
126
|
this.addPropertyInstance()
|
|
125
127
|
instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
126
128
|
}
|
|
@@ -128,7 +130,7 @@ export class ShaclProperty extends HTMLElement {
|
|
|
128
130
|
if (this.template.minCount !== undefined) {
|
|
129
131
|
mayRemove = instanceCount > this.template.minCount
|
|
130
132
|
} else {
|
|
131
|
-
mayRemove =
|
|
133
|
+
mayRemove = this.template.extendedShapes.length > 0 || instanceCount > 1
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
|
|
@@ -141,39 +143,142 @@ export class ShaclProperty extends HTMLElement {
|
|
|
141
143
|
const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
|
|
142
144
|
if (instance.firstChild instanceof ShaclNode) {
|
|
143
145
|
const shapeSubject = instance.firstChild.toRDF(graph)
|
|
144
|
-
graph.addQuad(subject, pathNode, shapeSubject, this.template.config.
|
|
146
|
+
graph.addQuad(subject, pathNode, shapeSubject, this.template.config.valuesGraphId)
|
|
145
147
|
} else {
|
|
146
148
|
for (const editor of instance.querySelectorAll<Editor>(':scope > .editor')) {
|
|
147
149
|
const value = toRDF(editor)
|
|
148
150
|
if (value) {
|
|
149
|
-
graph.addQuad(subject, pathNode, value, this.template.config.
|
|
151
|
+
graph.addQuad(subject, pathNode, value, this.template.config.valuesGraphId)
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
}
|
|
157
|
+
|
|
158
|
+
getRdfClassToLinkOrCreate() {
|
|
159
|
+
if (this.template.class && this.template.node) {
|
|
160
|
+
return this.template.class
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
for (const node of this.template.extendedShapes) {
|
|
164
|
+
// if this property has no sh:class but sh:node, then use the node shape's sh:targetClass to find protiential instances
|
|
165
|
+
const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
|
|
166
|
+
if (targetClasses.length > 0) {
|
|
167
|
+
return targetClasses[0] as NamedNode
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
isValueValid(value: Term) {
|
|
175
|
+
if (!this.template.extendedShapes.length) {
|
|
176
|
+
// property has no node shape, so value is valid
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
// property has node shape(s), so check if value conforms to any targetClass
|
|
180
|
+
for (const node of this.template.extendedShapes) {
|
|
181
|
+
const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
|
|
182
|
+
for (const targetClass of targetClasses) {
|
|
183
|
+
if (this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, targetClass, null) > 0) {
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
createAddButton() {
|
|
192
|
+
const addButton = new RokitSelect()
|
|
193
|
+
addButton.dense = true
|
|
194
|
+
addButton.label = "+ " + this.template.label
|
|
195
|
+
addButton.title = 'Add ' + this.template.label
|
|
196
|
+
addButton.classList.add('add-button')
|
|
197
|
+
|
|
198
|
+
// load potential value candidates for linking
|
|
199
|
+
let instances: InputListEntry[] = []
|
|
200
|
+
let clazz = this.getRdfClassToLinkOrCreate()
|
|
201
|
+
if (clazz) {
|
|
202
|
+
instances = findInstancesOf(clazz, this.template)
|
|
203
|
+
}
|
|
204
|
+
if (instances.length === 0) {
|
|
205
|
+
// no class instances found, so create an add button that creates a new instance
|
|
206
|
+
addButton.emptyMessage = ''
|
|
207
|
+
addButton.inputMinWidth = 0
|
|
208
|
+
addButton.addEventListener('click', _ => {
|
|
209
|
+
addButton.blur()
|
|
210
|
+
const instance = this.addPropertyInstance()
|
|
211
|
+
instance.classList.add('fadeIn')
|
|
212
|
+
this.updateControls()
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
focusFirstInputElement(instance)
|
|
215
|
+
instance.classList.remove('fadeIn')
|
|
216
|
+
}, 200)
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
// some instances found, so create an add button that can create a new instance or link existing ones
|
|
220
|
+
const ul = document.createElement('ul')
|
|
221
|
+
const newItem = document.createElement('li')
|
|
222
|
+
newItem.innerHTML = '+ Create new ' + this.template.label + '...'
|
|
223
|
+
newItem.dataset.value = 'new'
|
|
224
|
+
newItem.classList.add('large')
|
|
225
|
+
ul.appendChild(newItem)
|
|
226
|
+
const divider = document.createElement('li')
|
|
227
|
+
divider.classList.add('divider')
|
|
228
|
+
ul.appendChild(divider)
|
|
229
|
+
const header = document.createElement('li')
|
|
230
|
+
header.classList.add('header')
|
|
231
|
+
header.innerText = 'Or link existing:'
|
|
232
|
+
ul.appendChild(header)
|
|
233
|
+
for (const instance of instances) {
|
|
234
|
+
const li = document.createElement('li')
|
|
235
|
+
const itemValue = (typeof instance.value === 'string') ? instance.value : instance.value.value
|
|
236
|
+
li.innerText = instance.label ? instance.label : itemValue
|
|
237
|
+
li.dataset.value = JSON.stringify(instance.value)
|
|
238
|
+
ul.appendChild(li)
|
|
239
|
+
}
|
|
240
|
+
addButton.appendChild(ul)
|
|
241
|
+
addButton.collapsibleWidth = '250px'
|
|
242
|
+
addButton.collapsibleOrientationLeft = ''
|
|
243
|
+
addButton.addEventListener('change', () => {
|
|
244
|
+
if (addButton.value === 'new') {
|
|
245
|
+
// user wants to create a new instance
|
|
246
|
+
this.addPropertyInstance()
|
|
247
|
+
} else {
|
|
248
|
+
// user wants to link existing instance
|
|
249
|
+
const value = JSON.parse(addButton.value) as Term
|
|
250
|
+
this.insertBefore(createPropertyInstance(this.template, value, true, true), addButton)
|
|
251
|
+
}
|
|
252
|
+
addButton.value = ''
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
return addButton
|
|
256
|
+
}
|
|
155
257
|
}
|
|
156
258
|
|
|
157
|
-
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false): HTMLElement {
|
|
259
|
+
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false, linked = false): HTMLElement {
|
|
158
260
|
let instance: HTMLElement
|
|
159
|
-
if (template.extendedShapes
|
|
261
|
+
if (template.extendedShapes.length) {
|
|
160
262
|
instance = document.createElement('div')
|
|
161
263
|
instance.classList.add('property-instance')
|
|
162
264
|
for (const node of template.extendedShapes) {
|
|
163
|
-
instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label))
|
|
265
|
+
instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label, linked))
|
|
164
266
|
}
|
|
165
267
|
} else {
|
|
166
268
|
const plugin = findPlugin(template.path, template.datatype?.value)
|
|
167
269
|
if (plugin) {
|
|
168
|
-
if (template.config.editMode) {
|
|
270
|
+
if (template.config.editMode && !linked) {
|
|
169
271
|
instance = plugin.createEditor(template, value)
|
|
170
272
|
} else {
|
|
171
273
|
instance = plugin.createViewer(template, value!)
|
|
172
274
|
}
|
|
173
275
|
} else {
|
|
174
|
-
instance = fieldFactory(template, value || null)
|
|
276
|
+
instance = fieldFactory(template, value || null, template.config.editMode && !linked)
|
|
175
277
|
}
|
|
176
278
|
instance.classList.add('property-instance')
|
|
279
|
+
if (linked) {
|
|
280
|
+
instance.classList.add('linked')
|
|
281
|
+
}
|
|
177
282
|
}
|
|
178
283
|
if (template.config.editMode) {
|
|
179
284
|
appendRemoveButton(instance, template.label, forceRemovable)
|
|
@@ -183,10 +288,10 @@ export function createPropertyInstance(template: ShaclPropertyTemplate, value?:
|
|
|
183
288
|
}
|
|
184
289
|
|
|
185
290
|
function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
|
|
186
|
-
const removeButton =
|
|
187
|
-
removeButton.
|
|
188
|
-
removeButton.classList.add('control-button', 'btn', 'remove-button')
|
|
291
|
+
const removeButton = new RokitButton()
|
|
292
|
+
removeButton.classList.add('remove-button', 'clear')
|
|
189
293
|
removeButton.title = 'Remove ' + label
|
|
294
|
+
removeButton.dense = true
|
|
190
295
|
removeButton.addEventListener('click', _ => {
|
|
191
296
|
instance.classList.remove('fadeIn')
|
|
192
297
|
instance.classList.add('fadeOut')
|
package/src/serialize.ts
CHANGED
|
@@ -46,11 +46,13 @@ function serializeJsonld(quads: Quad[]): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function toRDF(editor: Editor): Literal | NamedNode | undefined {
|
|
49
|
-
let languageOrDatatype: NamedNode<string> | string | undefined = editor
|
|
49
|
+
let languageOrDatatype: NamedNode<string> | string | undefined = editor.shaclDatatype
|
|
50
50
|
let value: number | string = editor.value
|
|
51
51
|
if (value) {
|
|
52
52
|
if (editor.dataset.class || editor.dataset.nodeKind === PREFIX_SHACL + 'IRI') {
|
|
53
53
|
return DataFactory.namedNode(value)
|
|
54
|
+
} else if (editor.dataset.link) {
|
|
55
|
+
return JSON.parse(editor.dataset.link)
|
|
54
56
|
} else {
|
|
55
57
|
if (editor.dataset.lang) {
|
|
56
58
|
languageOrDatatype = editor.dataset.lang
|
|
@@ -65,6 +67,17 @@ export function toRDF(editor: Editor): Literal | NamedNode | undefined {
|
|
|
65
67
|
// if seconds in value are 0, the input field omits them which is then not a valid xsd:dateTime
|
|
66
68
|
value = new Date(value).toISOString().slice(0, 19)
|
|
67
69
|
}
|
|
70
|
+
// check if value is a typed rdf literal
|
|
71
|
+
if (!languageOrDatatype && typeof value === 'string') {
|
|
72
|
+
const tokens = value.split('^^')
|
|
73
|
+
if (tokens.length === 2 &&
|
|
74
|
+
((tokens[0].startsWith('"') && tokens[0].endsWith('"') || tokens[0].startsWith('\'') && tokens[0].endsWith('\''))) &&
|
|
75
|
+
tokens[1].split(':').length === 2
|
|
76
|
+
) {
|
|
77
|
+
value = tokens[0].substring(1, tokens[0].length - 1)
|
|
78
|
+
languageOrDatatype = DataFactory.namedNode(tokens[1])
|
|
79
|
+
}
|
|
80
|
+
}
|
|
68
81
|
return DataFactory.literal(value, languageOrDatatype)
|
|
69
82
|
}
|
|
70
83
|
} else if (editor['type'] === 'checkbox' || editor.getAttribute('type') === 'checkbox') {
|
package/src/styles.css
CHANGED
|
@@ -2,31 +2,29 @@ form { box-sizing: border-box; display:block; --label-width: 8em; --caret-size:
|
|
|
2
2
|
form.mode-edit { padding-left: 1em; }
|
|
3
3
|
form *, form ::after, form ::before { box-sizing: inherit; }
|
|
4
4
|
shacl-node, .shacl-group { display: flex; flex-direction: column; width: 100%; position: relative; }
|
|
5
|
-
shacl-node .
|
|
6
|
-
shacl-node .
|
|
7
|
-
shacl-node .
|
|
8
|
-
shacl-node .add-button {
|
|
9
|
-
shacl-node .add-button:before { content: '+'; margin-right: 0.2em; }
|
|
10
|
-
shacl-node .add-button:hover { color: inherit; }
|
|
5
|
+
shacl-node .remove-button { margin-left: 4px; margin-top: 1px; }
|
|
6
|
+
shacl-node .add-button { color: #555; background-color: transparent; margin: 4px 24px 0 0; border: 0; }
|
|
7
|
+
shacl-node .add-button:hover { color:#222; }
|
|
8
|
+
shacl-node .add-button:focus { box-shadow: none; }
|
|
11
9
|
shacl-node h1 { font-size: 1.1rem; border-bottom: 1px solid; margin-top: 4px; color: #555; }
|
|
12
10
|
shacl-property { display: flex; flex-direction: column; align-items: end; position: relative; }
|
|
13
11
|
shacl-property:not(.may-add) > .add-button { display: none; }
|
|
14
12
|
shacl-property:not(.may-remove) > .property-instance > .remove-button:not(.persistent) { visibility: hidden; }
|
|
15
13
|
shacl-property:not(.may-remove) > .shacl-or-constraint > .remove-button:not(.persistent) { visibility: hidden; }
|
|
16
|
-
.shacl-group { margin-bottom: 1em; padding-bottom: 1em; }
|
|
17
14
|
.mode-view .shacl-group:not(:has(shacl-property)) { display: none; }
|
|
18
15
|
.property-instance, .shacl-or-constraint { display: flex; align-items: flex-start; padding: 4px 0; width: 100%; position: relative; }
|
|
19
16
|
.shacl-or-constraint label { display: inline-block; word-break: break-word; width: var(--label-width); line-height: 1em; padding-top: 0.15em; padding-right: 1em; flex-shrink: 0; position: relative; }
|
|
20
17
|
.property-instance label[title] { cursor: help; text-decoration: underline dashed #AAA; }
|
|
18
|
+
.property-instance.linked label:after, label.linked:after { content: '\1F517'; font-size: 0.6em; padding-left: 6px; }
|
|
21
19
|
.mode-edit .property-instance label.required::before { color: red; content: '\2736'; font-size: 0.6rem; position: absolute; left: -1.4em; top: 0.15rem; }
|
|
22
|
-
.property-instance.valid::before { position: absolute; left: calc(var(--label-width) - 1em); top:
|
|
20
|
+
.property-instance.valid::before { position: absolute; left: calc(var(--label-width) - 1em); top: 3px; color: green; content: '\2713'; }
|
|
23
21
|
.editor:not([type='checkbox']), .shacl-or-constraint select { flex-grow: 1; }
|
|
24
22
|
.shacl-or-constraint select { border: 1px solid #DDD; padding: 2px 4px; }
|
|
25
23
|
select { overflow: hidden; text-overflow: ellipsis; }
|
|
26
24
|
textarea.editor { resize: vertical; }
|
|
27
|
-
.lang-chooser { position: absolute; top:
|
|
25
|
+
.lang-chooser { position: absolute; top: 6px; right: 26px; border: 0; background-color: #e9e9ed; padding: 2px 4px; max-width: 40px; width: 40px; box-sizing: content-box; }
|
|
28
26
|
.lang-chooser+.editor { padding-right: 55px; }
|
|
29
|
-
.validation-error { position: absolute; left: calc(var(--label-width) - 1em); top:
|
|
27
|
+
.validation-error { position: absolute; left: calc(var(--label-width) - 1em); top: 3px; color: red; cursor: help; }
|
|
30
28
|
.validation-error::before { content: '\26a0' }
|
|
31
29
|
.validation-error.node { left: -1em; }
|
|
32
30
|
.invalid > .editor { border-color: red !important; }
|
package/src/theme.ts
CHANGED
|
@@ -20,7 +20,7 @@ export abstract class Theme {
|
|
|
20
20
|
this.stylesheet.replaceSync(aggregatedStyles)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
apply(
|
|
23
|
+
apply(_: HTMLFormElement) {
|
|
24
24
|
// NOP
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -35,7 +35,7 @@ export abstract class Theme {
|
|
|
35
35
|
let name = value.value
|
|
36
36
|
let lang: HTMLElement | null = null
|
|
37
37
|
if (value instanceof NamedNode) {
|
|
38
|
-
const quads = template.config.
|
|
38
|
+
const quads = template.config.store.getQuads(name, null, null, null)
|
|
39
39
|
if (quads.length) {
|
|
40
40
|
const s = findLabel(quads, template.config.languages)
|
|
41
41
|
if (s) {
|
|
@@ -79,8 +79,8 @@ export abstract class Theme {
|
|
|
79
79
|
abstract createButton(label: string, primary: boolean): HTMLElement
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null): HTMLElement {
|
|
83
|
-
if (
|
|
82
|
+
export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null, editable: boolean): HTMLElement {
|
|
83
|
+
if (editable) {
|
|
84
84
|
const required = template.minCount !== undefined && template.minCount > 0
|
|
85
85
|
// if we have a class, find the instances and display them in a list
|
|
86
86
|
if (template.class) {
|
|
@@ -91,7 +91,7 @@ export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null
|
|
|
91
91
|
if (template.shaclIn) {
|
|
92
92
|
const list = template.config.lists[template.shaclIn]
|
|
93
93
|
if (list?.length) {
|
|
94
|
-
const listEntries = createInputListEntries(list, template.config.
|
|
94
|
+
const listEntries = createInputListEntries(list, template.config.store, template.config.languages)
|
|
95
95
|
return template.config.theme.createListEditor(template.label, value, required, listEntries, template)
|
|
96
96
|
}
|
|
97
97
|
else {
|
package/src/themes/bootstrap.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { DefaultTheme } from './default'
|
|
|
2
2
|
import { Term } from '@rdfjs/types'
|
|
3
3
|
import { ShaclPropertyTemplate } from '../property-template'
|
|
4
4
|
import { Editor } from '../theme'
|
|
5
|
-
import bootstrap from 'bootstrap/dist/css/bootstrap.min.css'
|
|
5
|
+
import bootstrap from 'bootstrap/dist/css/bootstrap.min.css?raw'
|
|
6
6
|
import css from './bootstrap.css?raw'
|
|
7
7
|
|
|
8
8
|
export class BootstrapTheme extends DefaultTheme {
|
package/src/themes/default.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
.editor:not([type='checkbox']) { border: 1px solid #DDD; padding: 2px 4px; }
|
|
2
|
-
.property-instance label { display: inline-
|
|
3
|
-
.property-instance:not(:first-child) > label { visibility: hidden; max-height: 0; }
|
|
2
|
+
.property-instance label { display: inline-flex; word-break: break-word; line-height: 1em; padding-top: 0.15em; padding-right: 1em; flex-shrink: 0; position: relative; }
|
|
3
|
+
.property-instance:not(:first-child) > label:not(.persistent) { visibility: hidden; max-height: 0; }
|
|
4
4
|
.mode-edit .property-instance label { width: var(--label-width); }
|
package/src/themes/default.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ShaclPropertyTemplate } from "../property-template"
|
|
|
3
3
|
import { Editor, InputListEntry, Theme } from "../theme"
|
|
4
4
|
import { PREFIX_XSD } from '../constants'
|
|
5
5
|
import { Literal } from 'n3'
|
|
6
|
+
import { Term as N3Term } from 'n3'
|
|
6
7
|
import css from './default.css?raw'
|
|
7
8
|
|
|
8
9
|
export class DefaultTheme extends Theme {
|
|
@@ -17,7 +18,9 @@ export class DefaultTheme extends Theme {
|
|
|
17
18
|
editor.classList.add('editor')
|
|
18
19
|
if (template?.datatype) {
|
|
19
20
|
// store datatype on editor, this is used for RDF serialization
|
|
20
|
-
editor
|
|
21
|
+
editor.shaclDatatype = template.datatype
|
|
22
|
+
} else if (value instanceof Literal) {
|
|
23
|
+
editor.shaclDatatype = value.datatype
|
|
21
24
|
}
|
|
22
25
|
if (template?.minCount !== undefined) {
|
|
23
26
|
editor.dataset.minCount = String(template.minCount)
|
|
@@ -200,7 +203,13 @@ export class DefaultTheme extends Theme {
|
|
|
200
203
|
|
|
201
204
|
for (const item of listEntries) {
|
|
202
205
|
const option = document.createElement('option')
|
|
203
|
-
|
|
206
|
+
let itemValue = ''
|
|
207
|
+
if (typeof item.value === 'string') {
|
|
208
|
+
itemValue = item.value
|
|
209
|
+
} else {
|
|
210
|
+
// this is needed for typed rdf literals
|
|
211
|
+
itemValue = (item.value as N3Term).id
|
|
212
|
+
}
|
|
204
213
|
option.innerHTML = item.label ? item.label : itemValue
|
|
205
214
|
option.value = itemValue
|
|
206
215
|
if (item.indent) {
|
|
@@ -231,7 +240,7 @@ export class DefaultTheme extends Theme {
|
|
|
231
240
|
return result
|
|
232
241
|
}
|
|
233
242
|
|
|
234
|
-
createButton(label: string,
|
|
243
|
+
createButton(label: string, _: boolean): HTMLElement {
|
|
235
244
|
const button = document.createElement('button')
|
|
236
245
|
button.type = 'button'
|
|
237
246
|
button.innerHTML = label
|
package/src/themes/material.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Button, TextField, Select, MenuItem, Checkbox } from 'mdui'
|
|
|
4
4
|
import { Theme } from '../theme'
|
|
5
5
|
import { InputListEntry, Editor } from '../theme'
|
|
6
6
|
import { Literal } from 'n3'
|
|
7
|
+
import { Term as N3Term } from 'n3'
|
|
7
8
|
import css from './material.css?raw'
|
|
8
9
|
import { PREFIX_XSD } from '../constants'
|
|
9
10
|
|
|
@@ -16,7 +17,9 @@ export class MaterialTheme extends Theme {
|
|
|
16
17
|
editor.classList.add('editor')
|
|
17
18
|
if (template?.datatype) {
|
|
18
19
|
// store datatype on editor, this is used for RDF serialization
|
|
19
|
-
editor
|
|
20
|
+
editor.shaclDatatype = template.datatype
|
|
21
|
+
} else if (value instanceof Literal) {
|
|
22
|
+
editor.shaclDatatype = value.datatype
|
|
20
23
|
}
|
|
21
24
|
if (template?.minCount !== undefined) {
|
|
22
25
|
editor.dataset.minCount = String(template.minCount)
|
|
@@ -100,7 +103,13 @@ export class MaterialTheme extends Theme {
|
|
|
100
103
|
|
|
101
104
|
for (const item of listEntries) {
|
|
102
105
|
const option = new MenuItem()
|
|
103
|
-
|
|
106
|
+
let itemValue = ''
|
|
107
|
+
if (typeof item.value === 'string') {
|
|
108
|
+
itemValue = item.value
|
|
109
|
+
} else {
|
|
110
|
+
// this is needed for typed rdf literals
|
|
111
|
+
itemValue = (item.value as N3Term).id
|
|
112
|
+
}
|
|
104
113
|
const itemLabel = item.label ? item.label : itemValue
|
|
105
114
|
option.value = itemValue
|
|
106
115
|
option.textContent = itemLabel || itemValue
|
|
@@ -142,7 +151,7 @@ export class MaterialTheme extends Theme {
|
|
|
142
151
|
return result
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
createDateEditor(
|
|
154
|
+
createDateEditor(_: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
146
155
|
const editor = new TextField()
|
|
147
156
|
editor.variant = 'outlined'
|
|
148
157
|
editor.helper = template?.description?.value || template?.label || ''
|
package/src/util.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Literal, NamedNode, Prefixes, Quad, Store } from 'n3'
|
|
2
|
-
import { OWL_OBJECT_NAMED_INDIVIDUAL, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER } from './constants'
|
|
2
|
+
import { DATA_GRAPH, OWL_OBJECT_NAMED_INDIVIDUAL, PREFIX_FOAF, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER } from './constants'
|
|
3
3
|
import { Term } from '@rdfjs/types'
|
|
4
4
|
import { InputListEntry } from './theme'
|
|
5
5
|
import { ShaclPropertyTemplate } from './property-template'
|
|
@@ -48,11 +48,9 @@ export function focusFirstInputElement(context: HTMLElement) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function findLabel(quads: Quad[], languages: string[]): string {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages)
|
|
51
|
+
return findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, languages) ||
|
|
52
|
+
findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages) ||
|
|
53
|
+
findObjectValueByPredicate(quads, 'name', PREFIX_FOAF, languages)
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
export function createInputListEntries(subjects: Term[], shapesGraph: Store, languages: string[], indent?: number): InputListEntry[] {
|
|
@@ -92,22 +90,22 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
|
|
|
92
90
|
instances = list?.length ? list : []
|
|
93
91
|
} else {
|
|
94
92
|
// find instances in the shapes graph
|
|
95
|
-
instances = template.config.
|
|
93
|
+
instances = template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH)
|
|
96
94
|
// find instances in the data graph
|
|
97
|
-
instances.push(...template.config.
|
|
95
|
+
instances.push(...template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, DATA_GRAPH))
|
|
98
96
|
// find instances in imported taxonomies
|
|
99
|
-
findClassInstancesFromOwlImports(clazz, template, template.config.
|
|
97
|
+
findClassInstancesFromOwlImports(clazz, template, template.config.store, instances)
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
const entries = createInputListEntries(instances, template.config.
|
|
100
|
+
const entries = createInputListEntries(instances, template.config.store, template.config.languages, indent)
|
|
103
101
|
// build inheritance tree only if sh:in is not defined
|
|
104
102
|
if (template.shaclIn === undefined) {
|
|
105
|
-
for (const subClass of template.config.
|
|
103
|
+
for (const subClass of template.config.store.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) {
|
|
106
104
|
entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
|
|
107
105
|
}
|
|
108
|
-
if (template.config.
|
|
109
|
-
entries.push(...createInputListEntries([ clazz ], template.config.
|
|
110
|
-
for (const subClass of template.config.
|
|
106
|
+
if (template.config.store.getQuads(clazz, RDF_PREDICATE_TYPE, OWL_OBJECT_NAMED_INDIVIDUAL, null).length > 0) {
|
|
107
|
+
entries.push(...createInputListEntries([ clazz ], template.config.store, template.config.languages, indent))
|
|
108
|
+
for (const subClass of template.config.store.getSubjects(SKOS_PREDICATE_BROADER, clazz, null)) {
|
|
111
109
|
entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
|
|
112
110
|
}
|
|
113
111
|
}
|