@ulb-darmstadt/shacl-form 1.7.4 → 1.8.1
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 +20 -7
- package/dist/config.d.ts +4 -5
- package/dist/constants.d.ts +13 -13
- package/dist/constraints.d.ts +2 -2
- 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.d.ts +2 -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 +1 -1
- package/dist/property.d.ts +6 -2
- package/dist/theme.d.ts +3 -3
- package/dist/themes/default.d.ts +3 -3
- package/dist/themes/material.d.ts +2 -3
- package/dist/util.d.ts +2 -2
- package/package.json +26 -12
- package/src/config.ts +11 -10
- package/src/constants.ts +3 -3
- package/src/constraints.ts +15 -18
- 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.ts +40 -38
- package/src/plugins/leaflet.ts +2 -2
- package/src/plugins/mapbox.ts +4 -4
- package/src/property-template.ts +4 -5
- package/src/property.ts +154 -56
- package/src/serialize.ts +14 -1
- package/src/styles.css +8 -10
- package/src/theme.ts +6 -6
- package/src/themes/bootstrap.ts +1 -1
- package/src/themes/default.css +2 -2
- package/src/themes/default.ts +38 -30
- package/src/themes/material.ts +37 -34
- package/src/util.ts +26 -20
- 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/node.ts
CHANGED
|
@@ -14,24 +14,26 @@ export class ShaclNode extends HTMLElement {
|
|
|
14
14
|
targetClass: NamedNode | undefined
|
|
15
15
|
owlImports: NamedNode[] = []
|
|
16
16
|
config: Config
|
|
17
|
+
linked: boolean
|
|
17
18
|
|
|
18
|
-
constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string) {
|
|
19
|
+
constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string, linked?: boolean) {
|
|
19
20
|
super()
|
|
20
21
|
|
|
21
22
|
this.parent = parent
|
|
22
23
|
this.config = config
|
|
23
24
|
this.shaclSubject = shaclSubject
|
|
25
|
+
this.linked = linked || false
|
|
24
26
|
let nodeId: NamedNode | BlankNode | undefined = valueSubject
|
|
25
27
|
if (!nodeId) {
|
|
26
28
|
// if no value subject given, create new node id with a type depending on own nodeKind or given parent property nodeKind
|
|
27
29
|
if (!nodeKind) {
|
|
28
|
-
const spec = config.
|
|
30
|
+
const spec = config.store.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
|
|
29
31
|
if (spec.length) {
|
|
30
32
|
nodeKind = spec[0] as NamedNode
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
// if nodeKind is not set, but a value namespace is configured or if nodeKind is sh:IRI, then create a NamedNode
|
|
34
|
-
if ((nodeKind === undefined && config.attributes.valuesNamespace) || nodeKind?.
|
|
36
|
+
if ((nodeKind === undefined && config.attributes.valuesNamespace) || nodeKind?.value === `${PREFIX_SHACL}IRI`) {
|
|
35
37
|
// no requirements on node type, so create a NamedNode and use configured value namespace
|
|
36
38
|
nodeId = DataFactory.namedNode(config.attributes.valuesNamespace + uuidv4())
|
|
37
39
|
} else {
|
|
@@ -45,17 +47,19 @@ export class ShaclNode extends HTMLElement {
|
|
|
45
47
|
const id = JSON.stringify([shaclSubject, valueSubject])
|
|
46
48
|
if (valueSubject && config.renderedNodes.has(id)) {
|
|
47
49
|
// node/value pair is already rendered in the form, so just display a reference
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
label = label || "Link"
|
|
51
|
+
const labelElem = document.createElement('label')
|
|
52
|
+
labelElem.innerText = label
|
|
53
|
+
labelElem.classList.add('linked')
|
|
54
|
+
this.appendChild(labelElem)
|
|
55
|
+
|
|
53
56
|
const anchor = document.createElement('a')
|
|
54
|
-
|
|
57
|
+
let refId = (valueSubject.termType === 'BlankNode') ? '_:' + valueSubject.value : valueSubject.value
|
|
58
|
+
anchor.innerText = refId
|
|
55
59
|
anchor.classList.add('ref-link')
|
|
56
60
|
anchor.onclick = () => {
|
|
57
61
|
// if anchor is clicked, scroll referenced shacl node into view
|
|
58
|
-
this.config.form.querySelector(`shacl-node[data-node-id='${
|
|
62
|
+
this.config.form.querySelector(`shacl-node[data-node-id='${refId}']`)?.scrollIntoView()
|
|
59
63
|
}
|
|
60
64
|
this.appendChild(anchor)
|
|
61
65
|
this.style.flexDirection = 'row'
|
|
@@ -64,9 +68,6 @@ export class ShaclNode extends HTMLElement {
|
|
|
64
68
|
config.renderedNodes.add(id)
|
|
65
69
|
}
|
|
66
70
|
this.dataset.nodeId = this.nodeId.id
|
|
67
|
-
const quads = config.shapesGraph.getQuads(shaclSubject, null, null, null)
|
|
68
|
-
let list: Term[] | undefined
|
|
69
|
-
|
|
70
71
|
if (this.config.attributes.showNodeIds !== null) {
|
|
71
72
|
const div = document.createElement('div')
|
|
72
73
|
div.innerText = `id: ${this.nodeId.id}`
|
|
@@ -74,14 +75,19 @@ export class ShaclNode extends HTMLElement {
|
|
|
74
75
|
this.appendChild(div)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// first initialize owl:imports, this is needed before adding properties to properly resolve class instances etc.
|
|
79
|
+
for (const owlImport of config.store.getQuads(shaclSubject, OWL_PREDICATE_IMPORTS, null, null)) {
|
|
80
|
+
this.owlImports.push(owlImport.object as NamedNode)
|
|
81
|
+
}
|
|
82
|
+
// now parse other node quads
|
|
83
|
+
for (const quad of config.store.getQuads(shaclSubject, null, null, null)) {
|
|
78
84
|
switch (quad.predicate.id) {
|
|
79
85
|
case SHACL_PREDICATE_PROPERTY.id:
|
|
80
86
|
this.addPropertyInstance(quad.object, config, valueSubject)
|
|
81
87
|
break;
|
|
82
88
|
case `${PREFIX_SHACL}and`:
|
|
83
89
|
// inheritance via sh:and
|
|
84
|
-
list = config.lists[quad.object.value]
|
|
90
|
+
const list = config.lists[quad.object.value]
|
|
85
91
|
if (list?.length) {
|
|
86
92
|
for (const shape of list) {
|
|
87
93
|
this.prepend(new ShaclNode(shape as NamedNode, config, valueSubject, this))
|
|
@@ -98,9 +104,6 @@ export class ShaclNode extends HTMLElement {
|
|
|
98
104
|
case `${PREFIX_SHACL}targetClass`:
|
|
99
105
|
this.targetClass = quad.object as NamedNode
|
|
100
106
|
break;
|
|
101
|
-
case OWL_PREDICATE_IMPORTS.id:
|
|
102
|
-
this.owlImports.push(quad.object as NamedNode)
|
|
103
|
-
break;
|
|
104
107
|
case `${PREFIX_SHACL}or`:
|
|
105
108
|
this.tryResolve(quad.object, valueSubject, config)
|
|
106
109
|
break;
|
|
@@ -122,15 +125,18 @@ export class ShaclNode extends HTMLElement {
|
|
|
122
125
|
if (!subject) {
|
|
123
126
|
subject = this.nodeId
|
|
124
127
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
// output triples only if node is not a link
|
|
129
|
+
if (!this.linked) {
|
|
130
|
+
for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
|
|
131
|
+
(shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
|
|
132
|
+
}
|
|
133
|
+
if (this.targetClass) {
|
|
134
|
+
graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass, this.config.valuesGraphId)
|
|
135
|
+
}
|
|
136
|
+
// if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
|
|
137
|
+
if (this.config.attributes.generateNodeShapeReference && !this.parent) {
|
|
138
|
+
graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject, this.config.valuesGraphId)
|
|
139
|
+
}
|
|
134
140
|
}
|
|
135
141
|
return subject
|
|
136
142
|
}
|
|
@@ -138,7 +144,7 @@ export class ShaclNode extends HTMLElement {
|
|
|
138
144
|
addPropertyInstance(shaclSubject: Term, config: Config, valueSubject: NamedNode | BlankNode | undefined) {
|
|
139
145
|
let parentElement: HTMLElement = this
|
|
140
146
|
// check if property belongs to a group
|
|
141
|
-
const groupRef = config.
|
|
147
|
+
const groupRef = config.store.getQuads(shaclSubject as Term, `${PREFIX_SHACL}group`, null, null)
|
|
142
148
|
if (groupRef.length > 0) {
|
|
143
149
|
const groupSubject = groupRef[0].object.value
|
|
144
150
|
if (config.groups.indexOf(groupSubject) > -1) {
|
|
@@ -153,15 +159,11 @@ export class ShaclNode extends HTMLElement {
|
|
|
153
159
|
console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
|
-
|
|
157
|
-
// This
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (property.childElementCount > 0) {
|
|
162
|
-
parentElement.appendChild(property)
|
|
163
|
-
}
|
|
164
|
-
})
|
|
162
|
+
const property = new ShaclProperty(shaclSubject as NamedNode | BlankNode, this, config, valueSubject)
|
|
163
|
+
// do not add empty properties (i.e. properties with no instances). This can be the case e.g. in viewer mode when there is no data for the respective property.
|
|
164
|
+
if (property.childElementCount > 0) {
|
|
165
|
+
parentElement.appendChild(property)
|
|
166
|
+
}
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
tryResolve(subject: Term, valueSubject: NamedNode | BlankNode | undefined, config: Config) {
|
|
@@ -187,4 +189,4 @@ export class ShaclNode extends HTMLElement {
|
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
window.customElements.define('shacl-node', ShaclNode)
|
|
192
|
+
window.customElements.define('shacl-node', ShaclNode)
|
package/src/plugins/leaflet.ts
CHANGED
|
@@ -136,12 +136,12 @@ export class LeafletPlugin extends Plugin {
|
|
|
136
136
|
document.body.style.position = 'fixed'
|
|
137
137
|
dialog.showModal()
|
|
138
138
|
}
|
|
139
|
-
const instance = fieldFactory(template, value || null)
|
|
139
|
+
const instance = fieldFactory(template, value || null, true)
|
|
140
140
|
instance.appendChild(button)
|
|
141
141
|
return instance
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
createViewer(
|
|
144
|
+
createViewer(_: ShaclPropertyTemplate, value: Term): HTMLElement {
|
|
145
145
|
const container = document.createElement('div')
|
|
146
146
|
const geometry = wktToGeometry(value.value)
|
|
147
147
|
if (geometry?.coordinates?.length) {
|
package/src/plugins/mapbox.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { ShaclPropertyTemplate } from '../property-template'
|
|
|
4
4
|
import { Editor, fieldFactory } from '../theme'
|
|
5
5
|
import { Map, NavigationControl, FullscreenControl, LngLatBounds, LngLatLike } from 'mapbox-gl'
|
|
6
6
|
import MapboxDraw from '@mapbox/mapbox-gl-draw'
|
|
7
|
-
import mapboxGlCss from 'mapbox-gl/dist/mapbox-gl.css'
|
|
8
|
-
import mapboxGlDrawCss from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
|
|
7
|
+
import mapboxGlCss from 'mapbox-gl/dist/mapbox-gl.css?raw'
|
|
8
|
+
import mapboxGlDrawCss from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css?raw'
|
|
9
9
|
import { Geometry, geometryToWkt, wktToGeometry } from './map-util'
|
|
10
10
|
|
|
11
11
|
const css = `
|
|
@@ -106,12 +106,12 @@ export class MapboxPlugin extends Plugin {
|
|
|
106
106
|
document.body.style.position = 'fixed'
|
|
107
107
|
dialog.showModal()
|
|
108
108
|
}
|
|
109
|
-
const instance = fieldFactory(template, value || null)
|
|
109
|
+
const instance = fieldFactory(template, value || null, true)
|
|
110
110
|
instance.appendChild(button)
|
|
111
111
|
return instance
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
createViewer(
|
|
114
|
+
createViewer(_: ShaclPropertyTemplate, value: Term): HTMLElement {
|
|
115
115
|
const container = document.createElement('div')
|
|
116
116
|
const geometry = wktToGeometry(value.value)
|
|
117
117
|
if (geometry?.coordinates?.length) {
|
package/src/property-template.ts
CHANGED
|
@@ -38,7 +38,7 @@ const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => v
|
|
|
38
38
|
[SHACL_PREDICATE_CLASS.id]: (template, term) => {
|
|
39
39
|
template.class = term as NamedNode
|
|
40
40
|
// try to find node shape that has requested target class
|
|
41
|
-
const nodeShapes = template.config.
|
|
41
|
+
const nodeShapes = template.config.store.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
|
|
42
42
|
if (nodeShapes.length > 0) {
|
|
43
43
|
template.node = nodeShapes[0] as NamedNode
|
|
44
44
|
}
|
|
@@ -61,7 +61,7 @@ const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => v
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export class ShaclPropertyTemplate
|
|
64
|
+
export class ShaclPropertyTemplate {
|
|
65
65
|
parent: ShaclNode
|
|
66
66
|
label = ''
|
|
67
67
|
name: Literal | undefined
|
|
@@ -95,14 +95,14 @@ export class ShaclPropertyTemplate {
|
|
|
95
95
|
owlImports: NamedNode[] = []
|
|
96
96
|
|
|
97
97
|
config: Config
|
|
98
|
-
extendedShapes: NamedNode[]
|
|
98
|
+
extendedShapes: NamedNode[] = []
|
|
99
99
|
|
|
100
100
|
constructor(quads: Quad[], parent: ShaclNode, config: Config) {
|
|
101
101
|
this.parent = parent
|
|
102
102
|
this.config = config
|
|
103
103
|
this.merge(quads)
|
|
104
104
|
if (this.qualifiedValueShape) {
|
|
105
|
-
this.merge(config.
|
|
105
|
+
this.merge(config.store.getQuads(this.qualifiedValueShape, null, null, null))
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,7 +117,6 @@ export class ShaclPropertyTemplate {
|
|
|
117
117
|
}
|
|
118
118
|
// resolve extended shapes
|
|
119
119
|
if (this.node || this.shaclAnd) {
|
|
120
|
-
this.extendedShapes = []
|
|
121
120
|
if (this.node) {
|
|
122
121
|
this.extendedShapes.push(this.node)
|
|
123
122
|
}
|
package/src/property.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
|
|
1
|
+
import { BlankNode, DataFactory, Literal, NamedNode, Quad, Store } from 'n3'
|
|
2
2
|
import { Term } from '@rdfjs/types'
|
|
3
3
|
import { ShaclNode } from './node'
|
|
4
|
-
import { focusFirstInputElement } from './util'
|
|
5
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') {
|
|
@@ -115,10 +101,19 @@ export class ShaclProperty extends HTMLElement {
|
|
|
115
101
|
appendRemoveButton(instance, '')
|
|
116
102
|
}
|
|
117
103
|
} else {
|
|
118
|
-
|
|
104
|
+
// check if value is part of the data graph. if not, create a linked resource
|
|
105
|
+
let linked = false
|
|
106
|
+
if (value && !(value instanceof Literal)) {
|
|
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)
|
|
119
114
|
}
|
|
120
|
-
if (this.
|
|
121
|
-
this.insertBefore(instance!, this.addButton
|
|
115
|
+
if (this.addButton) {
|
|
116
|
+
this.insertBefore(instance!, this.addButton)
|
|
122
117
|
} else {
|
|
123
118
|
this.appendChild(instance!)
|
|
124
119
|
}
|
|
@@ -127,7 +122,7 @@ export class ShaclProperty extends HTMLElement {
|
|
|
127
122
|
|
|
128
123
|
updateControls() {
|
|
129
124
|
let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
130
|
-
if (instanceCount === 0 && (!this.template.extendedShapes
|
|
125
|
+
if (instanceCount === 0 && (!this.template.extendedShapes.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
|
|
131
126
|
this.addPropertyInstance()
|
|
132
127
|
instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
133
128
|
}
|
|
@@ -135,7 +130,7 @@ export class ShaclProperty extends HTMLElement {
|
|
|
135
130
|
if (this.template.minCount !== undefined) {
|
|
136
131
|
mayRemove = instanceCount > this.template.minCount
|
|
137
132
|
} else {
|
|
138
|
-
mayRemove =
|
|
133
|
+
mayRemove = this.template.extendedShapes.length > 0 || instanceCount > 1
|
|
139
134
|
}
|
|
140
135
|
|
|
141
136
|
const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
|
|
@@ -148,39 +143,142 @@ export class ShaclProperty extends HTMLElement {
|
|
|
148
143
|
const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
|
|
149
144
|
if (instance.firstChild instanceof ShaclNode) {
|
|
150
145
|
const shapeSubject = instance.firstChild.toRDF(graph)
|
|
151
|
-
graph.addQuad(subject, pathNode, shapeSubject, this.template.config.
|
|
146
|
+
graph.addQuad(subject, pathNode, shapeSubject, this.template.config.valuesGraphId)
|
|
152
147
|
} else {
|
|
153
148
|
for (const editor of instance.querySelectorAll<Editor>(':scope > .editor')) {
|
|
154
149
|
const value = toRDF(editor)
|
|
155
150
|
if (value) {
|
|
156
|
-
graph.addQuad(subject, pathNode, value, this.template.config.
|
|
151
|
+
graph.addQuad(subject, pathNode, value, this.template.config.valuesGraphId)
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
155
|
}
|
|
161
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
|
+
}
|
|
162
257
|
}
|
|
163
258
|
|
|
164
|
-
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false): HTMLElement {
|
|
259
|
+
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false, linked = false): HTMLElement {
|
|
165
260
|
let instance: HTMLElement
|
|
166
|
-
if (template.extendedShapes
|
|
261
|
+
if (template.extendedShapes.length) {
|
|
167
262
|
instance = document.createElement('div')
|
|
168
263
|
instance.classList.add('property-instance')
|
|
169
264
|
for (const node of template.extendedShapes) {
|
|
170
|
-
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))
|
|
171
266
|
}
|
|
172
267
|
} else {
|
|
173
268
|
const plugin = findPlugin(template.path, template.datatype?.value)
|
|
174
269
|
if (plugin) {
|
|
175
|
-
if (template.config.editMode) {
|
|
270
|
+
if (template.config.editMode && !linked) {
|
|
176
271
|
instance = plugin.createEditor(template, value)
|
|
177
272
|
} else {
|
|
178
273
|
instance = plugin.createViewer(template, value!)
|
|
179
274
|
}
|
|
180
275
|
} else {
|
|
181
|
-
instance = fieldFactory(template, value || null)
|
|
276
|
+
instance = fieldFactory(template, value || null, template.config.editMode && !linked)
|
|
182
277
|
}
|
|
183
278
|
instance.classList.add('property-instance')
|
|
279
|
+
if (linked) {
|
|
280
|
+
instance.classList.add('linked')
|
|
281
|
+
}
|
|
184
282
|
}
|
|
185
283
|
if (template.config.editMode) {
|
|
186
284
|
appendRemoveButton(instance, template.label, forceRemovable)
|
|
@@ -190,10 +288,10 @@ export function createPropertyInstance(template: ShaclPropertyTemplate, value?:
|
|
|
190
288
|
}
|
|
191
289
|
|
|
192
290
|
function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
|
|
193
|
-
const removeButton =
|
|
194
|
-
removeButton.
|
|
195
|
-
removeButton.classList.add('control-button', 'btn', 'remove-button')
|
|
291
|
+
const removeButton = new RokitButton()
|
|
292
|
+
removeButton.classList.add('remove-button', 'clear')
|
|
196
293
|
removeButton.title = 'Remove ' + label
|
|
294
|
+
removeButton.dense = true
|
|
197
295
|
removeButton.addEventListener('click', _ => {
|
|
198
296
|
instance.classList.remove('fadeIn')
|
|
199
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; }
|