@ulb-darmstadt/shacl-form 1.10.3 → 2.0.0-rc1
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 +1 -1
- package/dist/bundle.js +412 -0
- package/dist/constants.d.ts +16 -16
- package/dist/constraints.d.ts +2 -2
- package/dist/form.d.ts +4 -3
- package/dist/index.js +62 -0
- package/dist/plugins/assets/plugin-VN3CfgGe.js +1 -0
- package/dist/plugins/file-upload.js +1 -0
- package/dist/plugins/leaflet.d.ts +3 -1
- package/dist/plugins/leaflet.js +5 -708
- package/dist/plugins/map-util.js +1 -0
- package/dist/{themes/default.d.ts → theme.default.d.ts} +2 -2
- package/package.json +28 -33
- package/dist/form-bootstrap.d.ts +0 -5
- package/dist/form-bootstrap.js +0 -413
- package/dist/form-default.d.ts +0 -5
- package/dist/form-default.js +0 -402
- package/dist/form-material.d.ts +0 -5
- package/dist/form-material.js +0 -722
- package/dist/plugins/fixed-list.d.ts +0 -9
- package/dist/plugins/mapbox.d.ts +0 -19
- package/dist/plugins/mapbox.js +0 -2870
- package/dist/themes/bootstrap.d.ts +0 -10
- package/dist/themes/material.d.ts +0 -15
- package/src/config.ts +0 -110
- package/src/constants.ts +0 -30
- package/src/constraints.ts +0 -149
- package/src/exports.ts +0 -7
- package/src/form-bootstrap.ts +0 -12
- package/src/form-default.ts +0 -12
- package/src/form-material.ts +0 -12
- package/src/form.ts +0 -319
- package/src/globals.d.ts +0 -2
- package/src/group.ts +0 -34
- package/src/loader.ts +0 -187
- package/src/node.ts +0 -192
- package/src/plugin.ts +0 -60
- package/src/plugins/file-upload.ts +0 -26
- package/src/plugins/fixed-list.ts +0 -19
- package/src/plugins/leaflet.ts +0 -196
- package/src/plugins/map-util.ts +0 -41
- package/src/plugins/mapbox.ts +0 -157
- package/src/property-template.ts +0 -151
- package/src/property.ts +0 -309
- package/src/serialize.ts +0 -96
- package/src/shacl-engine.d.ts +0 -2
- package/src/styles.css +0 -49
- package/src/theme.ts +0 -132
- package/src/themes/bootstrap.css +0 -6
- package/src/themes/bootstrap.ts +0 -44
- package/src/themes/default.css +0 -4
- package/src/themes/default.ts +0 -255
- package/src/themes/material.css +0 -14
- package/src/themes/material.ts +0 -250
- package/src/util.ts +0 -275
package/src/plugins/mapbox.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { Term } from '@rdfjs/types'
|
|
2
|
-
import { Plugin, PluginOptions } from '../plugin'
|
|
3
|
-
import { ShaclPropertyTemplate } from '../property-template'
|
|
4
|
-
import { Editor, fieldFactory } from '../theme'
|
|
5
|
-
import { Map, NavigationControl, FullscreenControl, LngLatBounds, LngLatLike } from 'mapbox-gl'
|
|
6
|
-
import MapboxDraw from '@mapbox/mapbox-gl-draw'
|
|
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
|
-
import { Geometry, geometryToWkt, wktToGeometry } from './map-util'
|
|
10
|
-
|
|
11
|
-
const css = `
|
|
12
|
-
#shaclMapDialog .closeButton { position: absolute; right: 0; top: 0; z-index: 1; padding: 6px 8px; cursor: pointer; border: 0; background-color: #FFFA; font-size: 24px; }
|
|
13
|
-
#shaclMapDialog { padding: 0; width:90vw; height: 90vh; margin: auto; }
|
|
14
|
-
#shaclMapDialog::backdrop { background-color: #0007; }
|
|
15
|
-
#shaclMapDialog .closeButton:hover { background-color: #FFF }
|
|
16
|
-
#shaclMapDialog .hint { position: absolute; right: 60px; top: 3px; z-index: 1; padding: 4px 6px; background-color: #FFFA; border-radius: 4px; }
|
|
17
|
-
.mapboxgl-map { min-height: 300px; }
|
|
18
|
-
#shaclMapDialogContainer { width:100%; height: 100% }
|
|
19
|
-
`
|
|
20
|
-
const dialogTemplate = `
|
|
21
|
-
<dialog id="shaclMapDialog" onclick="event.target==this && this.close()">
|
|
22
|
-
<div id="shaclMapDialogContainer"></div>
|
|
23
|
-
<div class="hint">ⓘ Draw a polygon or point, then close dialog</div>
|
|
24
|
-
<button class="closeButton" type="button" onclick="this.parentElement.close()">✕</button>
|
|
25
|
-
</dialog>`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export class MapboxPlugin extends Plugin {
|
|
29
|
-
map: Map | undefined
|
|
30
|
-
draw: MapboxDraw | undefined
|
|
31
|
-
currentEditor: Editor | undefined
|
|
32
|
-
apiKey: string
|
|
33
|
-
|
|
34
|
-
constructor(options: PluginOptions, apiKey: string) {
|
|
35
|
-
super(options, mapboxGlCss + '\n' + mapboxGlDrawCss + '\n' + css)
|
|
36
|
-
this.apiKey = apiKey
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
initEditMode(form: HTMLElement): HTMLDialogElement {
|
|
40
|
-
form.insertAdjacentHTML('beforeend', dialogTemplate)
|
|
41
|
-
const container = form.querySelector('#shaclMapDialogContainer') as HTMLElement
|
|
42
|
-
this.map = new Map({
|
|
43
|
-
container: container,
|
|
44
|
-
style: 'mapbox://styles/mapbox/satellite-streets-v11',
|
|
45
|
-
zoom: 5,
|
|
46
|
-
center: { lng: 8.657238961696038, lat: 49.87627570549512 },
|
|
47
|
-
attributionControl: false,
|
|
48
|
-
accessToken: this.apiKey
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
this.draw = new MapboxDraw({
|
|
52
|
-
displayControlsDefault: false,
|
|
53
|
-
controls: { point: true, polygon: true }
|
|
54
|
-
})
|
|
55
|
-
this.map.addControl(new NavigationControl(), 'top-left')
|
|
56
|
-
this.map.addControl(this.draw, 'top-left')
|
|
57
|
-
|
|
58
|
-
this.map.on('idle', () => {
|
|
59
|
-
// this fixes wrong size of canvas
|
|
60
|
-
this.map!.resize()
|
|
61
|
-
})
|
|
62
|
-
// @ts-ignore
|
|
63
|
-
this.map.on('draw.create', () => this.deleteAllButLastDrawing())
|
|
64
|
-
|
|
65
|
-
const dialog = form.querySelector('#shaclMapDialog') as HTMLDialogElement
|
|
66
|
-
dialog.addEventListener('close', () => {
|
|
67
|
-
const scrollY = document.body.style.top
|
|
68
|
-
document.body.style.position = ''
|
|
69
|
-
document.body.style.top = ''
|
|
70
|
-
window.scrollTo(0, parseInt(scrollY || '0') * -1)
|
|
71
|
-
// set wkt in editor
|
|
72
|
-
const data = this.draw!.getAll()
|
|
73
|
-
if (data && data.features.length && this.currentEditor) {
|
|
74
|
-
const geometry = data.features[0].geometry as Geometry
|
|
75
|
-
if (geometry.coordinates?.length) {
|
|
76
|
-
const wkt = geometryToWkt(geometry)
|
|
77
|
-
this.currentEditor.value = wkt
|
|
78
|
-
this.currentEditor.dispatchEvent(new Event('change', { bubbles: true }))
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
return dialog
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
|
|
86
|
-
let dialog = template.config.form.querySelector('#shaclMapDialog') as HTMLDialogElement
|
|
87
|
-
if (!dialog) {
|
|
88
|
-
dialog = this.initEditMode(template.config.form)
|
|
89
|
-
}
|
|
90
|
-
const button = template.config.theme.createButton('Open map...', false)
|
|
91
|
-
button.style.marginLeft = '5px'
|
|
92
|
-
button.classList.add('open-map-button')
|
|
93
|
-
button.onclick = () => {
|
|
94
|
-
this.currentEditor = instance.querySelector('.editor') as Editor
|
|
95
|
-
this.draw?.deleteAll()
|
|
96
|
-
|
|
97
|
-
const wkt = this.currentEditor.value || ''
|
|
98
|
-
const geometry = wktToGeometry(wkt)
|
|
99
|
-
if (geometry && geometry.coordinates?.length) {
|
|
100
|
-
this.draw?.add(geometry)
|
|
101
|
-
this.fitToGeometry(this.map!, geometry)
|
|
102
|
-
} else {
|
|
103
|
-
this.map?.setZoom(5)
|
|
104
|
-
}
|
|
105
|
-
document.body.style.top = `-${window.scrollY}px`
|
|
106
|
-
document.body.style.position = 'fixed'
|
|
107
|
-
dialog.showModal()
|
|
108
|
-
}
|
|
109
|
-
const instance = fieldFactory(template, value || null, true)
|
|
110
|
-
instance.appendChild(button)
|
|
111
|
-
return instance
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
createViewer(_: ShaclPropertyTemplate, value: Term): HTMLElement {
|
|
115
|
-
const container = document.createElement('div')
|
|
116
|
-
const geometry = wktToGeometry(value.value)
|
|
117
|
-
if (geometry?.coordinates?.length) {
|
|
118
|
-
// wait for container to be available in DOM
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
const draw = new MapboxDraw({ displayControlsDefault: false })
|
|
121
|
-
const map = new Map({
|
|
122
|
-
container: container,
|
|
123
|
-
style: 'mapbox://styles/mapbox/satellite-streets-v11',
|
|
124
|
-
zoom: 5,
|
|
125
|
-
attributionControl: false,
|
|
126
|
-
accessToken: this.apiKey
|
|
127
|
-
})
|
|
128
|
-
map.addControl(draw)
|
|
129
|
-
map.addControl(new FullscreenControl())
|
|
130
|
-
draw.add(geometry)
|
|
131
|
-
this.fitToGeometry(map, geometry)
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
return container
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fitToGeometry(map: Map, geometry: Geometry) {
|
|
138
|
-
if (typeof geometry.coordinates[0] === 'number') {
|
|
139
|
-
// e.g. Point
|
|
140
|
-
map.setCenter(geometry.coordinates as LngLatLike)
|
|
141
|
-
map.setZoom(15)
|
|
142
|
-
} else {
|
|
143
|
-
// e.g. Polygon
|
|
144
|
-
const bounds = geometry.coordinates[0].reduce((bounds, coord) => {
|
|
145
|
-
return bounds.extend(coord as mapboxgl.LngLatLike)
|
|
146
|
-
}, new LngLatBounds(geometry.coordinates[0][0] as mapboxgl.LngLatLike, geometry.coordinates[0][0] as mapboxgl.LngLatLike))
|
|
147
|
-
map.fitBounds(bounds, { padding: 20, animate: false })
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
deleteAllButLastDrawing() {
|
|
152
|
-
const data = this.draw!.getAll()
|
|
153
|
-
for (let i = 0; i < data.features.length - 1; i++) {
|
|
154
|
-
this.draw!.delete(data.features[i].id as string)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
package/src/property-template.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { Literal, NamedNode, Quad, DataFactory } from 'n3'
|
|
2
|
-
import { Term } from '@rdfjs/types'
|
|
3
|
-
import { OWL_PREDICATE_IMPORTS, PREFIX_DASH, PREFIX_OA, PREFIX_RDF, PREFIX_SHACL, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS } from './constants'
|
|
4
|
-
import { Config } from './config'
|
|
5
|
-
import { findLabel, prioritizeByLanguage, removePrefixes } from './util'
|
|
6
|
-
import { ShaclNode } from './node'
|
|
7
|
-
|
|
8
|
-
const mappers: Record<string, (template: ShaclPropertyTemplate, term: Term) => void> = {
|
|
9
|
-
[`${PREFIX_SHACL}name`]: (template, term) => { const literal = term as Literal; template.name = prioritizeByLanguage(template.config.languages, template.name, literal) },
|
|
10
|
-
[`${PREFIX_SHACL}description`]: (template, term) => { const literal = term as Literal; template.description = prioritizeByLanguage(template.config.languages, template.description, literal) },
|
|
11
|
-
[`${PREFIX_SHACL}path`]: (template, term) => { template.path = term.value },
|
|
12
|
-
[`${PREFIX_SHACL}node`]: (template, term) => { template.node = term as NamedNode },
|
|
13
|
-
[`${PREFIX_SHACL}datatype`]: (template, term) => { template.datatype = term as NamedNode },
|
|
14
|
-
[`${PREFIX_SHACL}nodeKind`]: (template, term) => { template.nodeKind = term as NamedNode },
|
|
15
|
-
[`${PREFIX_SHACL}minCount`]: (template, term) => { template.minCount = parseInt(term.value) },
|
|
16
|
-
[`${PREFIX_SHACL}maxCount`]: (template, term) => { template.maxCount = parseInt(term.value) },
|
|
17
|
-
[`${PREFIX_SHACL}minLength`]: (template, term) => { template.minLength = parseInt(term.value) },
|
|
18
|
-
[`${PREFIX_SHACL}maxLength`]: (template, term) => { template.maxLength = parseInt(term.value) },
|
|
19
|
-
[`${PREFIX_SHACL}minInclusive`]: (template, term) => { template.minInclusive = parseInt(term.value) },
|
|
20
|
-
[`${PREFIX_SHACL}maxInclusive`]: (template, term) => { template.maxInclusive = parseInt(term.value) },
|
|
21
|
-
[`${PREFIX_SHACL}minExclusive`]: (template, term) => { template.minExclusive = parseInt(term.value) },
|
|
22
|
-
[`${PREFIX_SHACL}maxExclusive`]: (template, term) => { template.maxExclusive = parseInt(term.value) },
|
|
23
|
-
[`${PREFIX_SHACL}pattern`]: (template, term) => { template.pattern = term.value },
|
|
24
|
-
[`${PREFIX_SHACL}order`]: (template, term) => { template.order = parseInt(term.value) },
|
|
25
|
-
[`${PREFIX_DASH}singleLine`]: (template, term) => { template.singleLine = term.value === 'true' },
|
|
26
|
-
[`${PREFIX_DASH}readonly`]: (template, term) => { template.readonly = term.value === 'true' },
|
|
27
|
-
[`${PREFIX_OA}styleClass`]: (template, term) => { template.cssClass = term.value },
|
|
28
|
-
[`${PREFIX_SHACL}and`]: (template, term) => { template.shaclAnd = term.value },
|
|
29
|
-
[`${PREFIX_SHACL}in`]: (template, term) => { template.shaclIn = term.value },
|
|
30
|
-
// sh:datatype might be undefined, but sh:languageIn defined. this is undesired. the spec says, that strings without a lang tag are not valid if sh:languageIn is set. but the shacl validator accepts these as valid. to prevent this, we just set the datatype here to 'langString'.
|
|
31
|
-
[`${PREFIX_SHACL}languageIn`]: (template, term) => { template.languageIn = template.config.lists[term.value]; template.datatype = DataFactory.namedNode(PREFIX_RDF + 'langString') },
|
|
32
|
-
[`${PREFIX_SHACL}defaultValue`]: (template, term) => { template.defaultValue = term },
|
|
33
|
-
[`${PREFIX_SHACL}hasValue`]: (template, term) => { template.hasValue = term },
|
|
34
|
-
[`${PREFIX_SHACL}qualifiedValueShape`]: (template, term) => { template.qualifiedValueShape = term },
|
|
35
|
-
[`${PREFIX_SHACL}qualifiedMinCount`]: (template, term) => { template.minCount = parseInt(term.value) },
|
|
36
|
-
[`${PREFIX_SHACL}qualifiedMaxCount`]: (template, term) => { template.maxCount = parseInt(term.value) },
|
|
37
|
-
[OWL_PREDICATE_IMPORTS.id]: (template, term) => { template.owlImports.push(term as NamedNode) },
|
|
38
|
-
[SHACL_PREDICATE_CLASS.id]: (template, term) => {
|
|
39
|
-
template.class = term as NamedNode
|
|
40
|
-
// try to find node shape that has requested target class
|
|
41
|
-
const nodeShapes = template.config.store.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
|
|
42
|
-
if (nodeShapes.length > 0) {
|
|
43
|
-
template.node = nodeShapes[0] as NamedNode
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
[`${PREFIX_SHACL}or`]: (template, term) => {
|
|
47
|
-
const list = template.config.lists[term.value]
|
|
48
|
-
if (list?.length) {
|
|
49
|
-
template.shaclOr = list
|
|
50
|
-
} else {
|
|
51
|
-
console.error('list for sh:or not found:', term.value, 'existing lists:', template.config.lists)
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
[`${PREFIX_SHACL}xone`]: (template, term) => {
|
|
55
|
-
const list = template.config.lists[term.value]
|
|
56
|
-
if (list?.length) {
|
|
57
|
-
template.shaclXone = list
|
|
58
|
-
} else {
|
|
59
|
-
console.error('list for sh:xone not found:', term.value, 'existing lists:', template.config.lists)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class ShaclPropertyTemplate {
|
|
65
|
-
parent: ShaclNode
|
|
66
|
-
label = ''
|
|
67
|
-
name: Literal | undefined
|
|
68
|
-
description: Literal | undefined
|
|
69
|
-
path: string | undefined
|
|
70
|
-
node: NamedNode | undefined
|
|
71
|
-
class: NamedNode | undefined
|
|
72
|
-
minCount: number | undefined
|
|
73
|
-
maxCount: number | undefined
|
|
74
|
-
minLength: number | undefined
|
|
75
|
-
maxLength: number | undefined
|
|
76
|
-
minInclusive: number | undefined
|
|
77
|
-
maxInclusive: number | undefined
|
|
78
|
-
minExclusive: number | undefined
|
|
79
|
-
maxExclusive: number | undefined
|
|
80
|
-
singleLine: boolean | undefined
|
|
81
|
-
readonly: boolean | undefined
|
|
82
|
-
cssClass: string | undefined
|
|
83
|
-
defaultValue: Term | undefined
|
|
84
|
-
pattern: string | undefined
|
|
85
|
-
order: number | undefined
|
|
86
|
-
nodeKind: NamedNode | undefined
|
|
87
|
-
shaclAnd: string | undefined
|
|
88
|
-
shaclIn: string | undefined
|
|
89
|
-
shaclOr: Term[] | undefined
|
|
90
|
-
shaclXone: Term[] | undefined
|
|
91
|
-
languageIn: Term[] | undefined
|
|
92
|
-
datatype: NamedNode | undefined
|
|
93
|
-
hasValue: Term | undefined
|
|
94
|
-
qualifiedValueShape: Term | undefined
|
|
95
|
-
owlImports: NamedNode[] = []
|
|
96
|
-
|
|
97
|
-
config: Config
|
|
98
|
-
extendedShapes: NamedNode[] = []
|
|
99
|
-
|
|
100
|
-
constructor(quads: Quad[], parent: ShaclNode, config: Config) {
|
|
101
|
-
this.parent = parent
|
|
102
|
-
this.config = config
|
|
103
|
-
this.merge(quads)
|
|
104
|
-
if (this.qualifiedValueShape) {
|
|
105
|
-
this.merge(config.store.getQuads(this.qualifiedValueShape, null, null, null))
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
merge(quads: Quad[]): ShaclPropertyTemplate {
|
|
110
|
-
for (const quad of quads) {
|
|
111
|
-
mappers[quad.predicate.id]?.call(this, this, quad.object)
|
|
112
|
-
}
|
|
113
|
-
// provide best fitting label for UI
|
|
114
|
-
this.label = this.name?.value || findLabel(quads, this.config.languages)
|
|
115
|
-
if (!this.label && !this.shaclAnd) {
|
|
116
|
-
this.label = this.path ? removePrefixes(this.path, this.config.prefixes) : 'unknown'
|
|
117
|
-
}
|
|
118
|
-
// register extended shapes
|
|
119
|
-
if (this.node) {
|
|
120
|
-
this.extendedShapes.push(this.node)
|
|
121
|
-
}
|
|
122
|
-
if (this.shaclAnd) {
|
|
123
|
-
const list = this.config.lists[this.shaclAnd]
|
|
124
|
-
if (list?.length) {
|
|
125
|
-
for (const node of list) {
|
|
126
|
-
this.extendedShapes.push(node as NamedNode)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return this
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
clone(): ShaclPropertyTemplate {
|
|
134
|
-
const copy = Object.assign({}, this)
|
|
135
|
-
// arrays are not cloned but referenced, so create them manually
|
|
136
|
-
copy.extendedShapes = [ ...this.extendedShapes ]
|
|
137
|
-
copy.owlImports = [ ...this.owlImports ]
|
|
138
|
-
if (this.languageIn) {
|
|
139
|
-
copy.languageIn = [ ...this.languageIn ]
|
|
140
|
-
}
|
|
141
|
-
if (this.shaclOr) {
|
|
142
|
-
copy.shaclOr = [ ...this.shaclOr ]
|
|
143
|
-
}
|
|
144
|
-
if (this.shaclXone) {
|
|
145
|
-
copy.shaclXone = [ ...this.shaclXone ]
|
|
146
|
-
}
|
|
147
|
-
copy.merge = this.merge.bind(copy)
|
|
148
|
-
copy.clone = this.clone.bind(copy)
|
|
149
|
-
return copy
|
|
150
|
-
}
|
|
151
|
-
}
|
package/src/property.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { BlankNode, DataFactory, Literal, NamedNode, Quad, Store } from 'n3'
|
|
2
|
-
import { Term } from '@rdfjs/types'
|
|
3
|
-
import { ShaclNode } from './node'
|
|
4
|
-
import { createShaclOrConstraint, resolveShaclOrConstraintOnProperty } from './constraints'
|
|
5
|
-
import { findInstancesOf, focusFirstInputElement } from './util'
|
|
6
|
-
import { Config } from './config'
|
|
7
|
-
import { ShaclPropertyTemplate } from './property-template'
|
|
8
|
-
import { Editor, fieldFactory, InputListEntry } from './theme'
|
|
9
|
-
import { toRDF } from './serialize'
|
|
10
|
-
import { findPlugin } from './plugin'
|
|
11
|
-
import { DATA_GRAPH, RDF_PREDICATE_TYPE, SHACL_PREDICATE_TARGET_CLASS } from './constants'
|
|
12
|
-
import { RokitButton, RokitCollapsible, RokitSelect } from '@ro-kit/ui-widgets'
|
|
13
|
-
|
|
14
|
-
export class ShaclProperty extends HTMLElement {
|
|
15
|
-
template: ShaclPropertyTemplate
|
|
16
|
-
addButton: RokitSelect | undefined
|
|
17
|
-
container: HTMLElement
|
|
18
|
-
|
|
19
|
-
constructor(shaclSubject: BlankNode | NamedNode, parent: ShaclNode, config: Config, valueSubject?: NamedNode | BlankNode) {
|
|
20
|
-
super()
|
|
21
|
-
this.template = new ShaclPropertyTemplate(config.store.getQuads(shaclSubject, null, null, null), parent, config)
|
|
22
|
-
this.container = this
|
|
23
|
-
if (this.template.extendedShapes.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
|
|
24
|
-
const collapsible = new RokitCollapsible()
|
|
25
|
-
collapsible.classList.add('collapsible', 'shacl-group');
|
|
26
|
-
collapsible.open = config.attributes.collapse === 'open';
|
|
27
|
-
collapsible.label = this.template.label;
|
|
28
|
-
this.container = collapsible
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (this.template.order !== undefined) {
|
|
32
|
-
this.style.order = `${this.template.order}`
|
|
33
|
-
}
|
|
34
|
-
if (this.template.cssClass) {
|
|
35
|
-
this.classList.add(this.template.cssClass)
|
|
36
|
-
}
|
|
37
|
-
if (config.editMode && !parent.linked) {
|
|
38
|
-
this.addButton = this.createAddButton()
|
|
39
|
-
this.container.appendChild(this.addButton)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// bind existing values
|
|
43
|
-
if (this.template.path) {
|
|
44
|
-
let values: Quad[] = []
|
|
45
|
-
if (valueSubject) {
|
|
46
|
-
if (parent.linked) {
|
|
47
|
-
// for linked resource, get values in all graphs
|
|
48
|
-
values = config.store.getQuads(valueSubject, this.template.path, null, null)
|
|
49
|
-
} else {
|
|
50
|
-
// get values only from data graph
|
|
51
|
-
values = config.store.getQuads(valueSubject, this.template.path, null, DATA_GRAPH)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
let valuesContainHasValue = false
|
|
55
|
-
for (const value of values) {
|
|
56
|
-
// ignore values that do not conform to this property.
|
|
57
|
-
// this might be the case when there are multiple properties with the same sh:path in a NodeShape.
|
|
58
|
-
if (this.isValueValid(value.object)) {
|
|
59
|
-
this.addPropertyInstance(value.object)
|
|
60
|
-
if (this.template.hasValue && value.object.equals(this.template.hasValue)) {
|
|
61
|
-
valuesContainHasValue = true
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (config.editMode && this.template.hasValue && !valuesContainHasValue && !parent.linked) {
|
|
66
|
-
// sh:hasValue is defined in shapes graph, but does not exist in data graph, so force it
|
|
67
|
-
this.addPropertyInstance(this.template.hasValue)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (config.editMode && !parent.linked) {
|
|
72
|
-
this.addEventListener('change', () => { this.updateControls() })
|
|
73
|
-
this.updateControls()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (this.container instanceof RokitCollapsible) {
|
|
77
|
-
// in view mode, show collapsible only when we have something to show
|
|
78
|
-
if ((config.editMode && !parent.linked) || this.container.childElementCount > 0) {
|
|
79
|
-
this.appendChild(this.container)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
addPropertyInstance(value?: Term): HTMLElement {
|
|
85
|
-
let instance: HTMLElement
|
|
86
|
-
if (this.template.shaclOr?.length || this.template.shaclXone?.length) {
|
|
87
|
-
const options = this.template.shaclOr?.length ? this.template.shaclOr : this.template.shaclXone as Term[]
|
|
88
|
-
let resolved = false
|
|
89
|
-
if (value) {
|
|
90
|
-
const resolvedOptions = resolveShaclOrConstraintOnProperty(options, value, this.template.config)
|
|
91
|
-
if (resolvedOptions.length) {
|
|
92
|
-
instance = createPropertyInstance(this.template.clone().merge(resolvedOptions), value, true)
|
|
93
|
-
resolved = true
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (!resolved) {
|
|
97
|
-
instance = createShaclOrConstraint(options, this, this.template.config)
|
|
98
|
-
appendRemoveButton(instance, '')
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
// check if value is part of the data graph. if not, create a linked resource
|
|
102
|
-
let linked = false
|
|
103
|
-
if (value && !(value instanceof Literal)) {
|
|
104
|
-
const clazz = this.getRdfClassToLinkOrCreate()
|
|
105
|
-
if (clazz && this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, clazz, DATA_GRAPH) === 0) {
|
|
106
|
-
// value is not in data graph, so must be a link in the shapes graph
|
|
107
|
-
linked = true
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
instance = createPropertyInstance(this.template, value, undefined, linked || this.template.parent.linked)
|
|
111
|
-
}
|
|
112
|
-
if (this.addButton) {
|
|
113
|
-
this.container.insertBefore(instance!, this.addButton)
|
|
114
|
-
} else {
|
|
115
|
-
this.container.appendChild(instance!)
|
|
116
|
-
}
|
|
117
|
-
return instance!
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
updateControls() {
|
|
121
|
-
let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
122
|
-
if (instanceCount === 0 && (!this.template.extendedShapes.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
|
|
123
|
-
this.addPropertyInstance()
|
|
124
|
-
instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
125
|
-
}
|
|
126
|
-
let mayRemove: boolean
|
|
127
|
-
if (this.template.minCount !== undefined) {
|
|
128
|
-
mayRemove = instanceCount > this.template.minCount
|
|
129
|
-
} else {
|
|
130
|
-
mayRemove = this.template.extendedShapes.length > 0 || instanceCount > 1
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
|
|
134
|
-
this.classList.toggle('may-remove', mayRemove)
|
|
135
|
-
this.classList.toggle('may-add', mayAdd)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
toRDF(graph: Store, subject: NamedNode | BlankNode) {
|
|
139
|
-
for (const instance of this.querySelectorAll(':scope > .property-instance, :scope > .collapsible > .property-instance')) {
|
|
140
|
-
const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
|
|
141
|
-
if (instance.firstChild instanceof ShaclNode) {
|
|
142
|
-
const shapeSubject = instance.firstChild.toRDF(graph)
|
|
143
|
-
graph.addQuad(subject, pathNode, shapeSubject, this.template.config.valuesGraphId)
|
|
144
|
-
} else {
|
|
145
|
-
for (const editor of instance.querySelectorAll<Editor>(':scope > .editor')) {
|
|
146
|
-
const value = toRDF(editor)
|
|
147
|
-
if (value) {
|
|
148
|
-
graph.addQuad(subject, pathNode, value, this.template.config.valuesGraphId)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getRdfClassToLinkOrCreate() {
|
|
156
|
-
if (this.template.class && this.template.node) {
|
|
157
|
-
return this.template.class
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
for (const node of this.template.extendedShapes) {
|
|
161
|
-
// if this property has no sh:class but sh:node, then use the node shape's sh:targetClass to find protiential instances
|
|
162
|
-
const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
|
|
163
|
-
if (targetClasses.length > 0) {
|
|
164
|
-
return targetClasses[0] as NamedNode
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return undefined
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
isValueValid(value: Term) {
|
|
172
|
-
if (!this.template.extendedShapes.length) {
|
|
173
|
-
// property has no node shape, so value is valid
|
|
174
|
-
return true
|
|
175
|
-
}
|
|
176
|
-
// property has node shape(s), so check if value conforms to any targetClass
|
|
177
|
-
for (const node of this.template.extendedShapes) {
|
|
178
|
-
const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
|
|
179
|
-
for (const targetClass of targetClasses) {
|
|
180
|
-
if (this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, targetClass, null) > 0) {
|
|
181
|
-
return true
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return false
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
createAddButton() {
|
|
189
|
-
const addButton = new RokitSelect()
|
|
190
|
-
addButton.dense = true
|
|
191
|
-
addButton.label = "+ " + this.template.label
|
|
192
|
-
addButton.title = 'Add ' + this.template.label
|
|
193
|
-
addButton.autoGrowLabelWidth = true
|
|
194
|
-
addButton.classList.add('add-button')
|
|
195
|
-
|
|
196
|
-
// load potential value candidates for linking
|
|
197
|
-
let instances: InputListEntry[] = []
|
|
198
|
-
let clazz = this.getRdfClassToLinkOrCreate()
|
|
199
|
-
if (clazz) {
|
|
200
|
-
instances = findInstancesOf(clazz, this.template)
|
|
201
|
-
}
|
|
202
|
-
if (instances.length === 0) {
|
|
203
|
-
// no class instances found, so create an add button that creates a new instance
|
|
204
|
-
addButton.emptyMessage = ''
|
|
205
|
-
addButton.inputMinWidth = 0
|
|
206
|
-
addButton.addEventListener('click', _ => {
|
|
207
|
-
addButton.blur()
|
|
208
|
-
const instance = this.addPropertyInstance()
|
|
209
|
-
instance.classList.add('fadeIn')
|
|
210
|
-
this.updateControls()
|
|
211
|
-
setTimeout(() => {
|
|
212
|
-
focusFirstInputElement(instance)
|
|
213
|
-
instance.classList.remove('fadeIn')
|
|
214
|
-
}, 200)
|
|
215
|
-
})
|
|
216
|
-
} else {
|
|
217
|
-
// some instances found, so create an add button that can create a new instance or link existing ones
|
|
218
|
-
const ul = document.createElement('ul')
|
|
219
|
-
const newItem = document.createElement('li')
|
|
220
|
-
newItem.innerHTML = '+ Create new ' + this.template.label + '...'
|
|
221
|
-
newItem.dataset.value = 'new'
|
|
222
|
-
newItem.classList.add('large')
|
|
223
|
-
ul.appendChild(newItem)
|
|
224
|
-
const divider = document.createElement('li')
|
|
225
|
-
divider.classList.add('divider')
|
|
226
|
-
ul.appendChild(divider)
|
|
227
|
-
const header = document.createElement('li')
|
|
228
|
-
header.classList.add('header')
|
|
229
|
-
header.innerText = 'Or link existing:'
|
|
230
|
-
ul.appendChild(header)
|
|
231
|
-
for (const instance of instances) {
|
|
232
|
-
const li = document.createElement('li')
|
|
233
|
-
const itemValue = (typeof instance.value === 'string') ? instance.value : instance.value.value
|
|
234
|
-
li.innerText = instance.label ? instance.label : itemValue
|
|
235
|
-
li.dataset.value = JSON.stringify(instance.value)
|
|
236
|
-
ul.appendChild(li)
|
|
237
|
-
}
|
|
238
|
-
addButton.appendChild(ul)
|
|
239
|
-
addButton.collapsibleWidth = '250px'
|
|
240
|
-
addButton.collapsibleOrientationLeft = ''
|
|
241
|
-
addButton.addEventListener('change', () => {
|
|
242
|
-
if (addButton.value === 'new') {
|
|
243
|
-
// user wants to create a new instance
|
|
244
|
-
this.addPropertyInstance()
|
|
245
|
-
} else {
|
|
246
|
-
// user wants to link existing instance
|
|
247
|
-
const value = JSON.parse(addButton.value) as Term
|
|
248
|
-
this.container.insertBefore(createPropertyInstance(this.template, value, true, true), addButton)
|
|
249
|
-
}
|
|
250
|
-
addButton.value = ''
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
return addButton
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false, linked = false): HTMLElement {
|
|
258
|
-
let instance: HTMLElement
|
|
259
|
-
if (template.extendedShapes.length) {
|
|
260
|
-
instance = document.createElement('div')
|
|
261
|
-
instance.classList.add('property-instance')
|
|
262
|
-
for (const node of template.extendedShapes) {
|
|
263
|
-
instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label, linked))
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
const plugin = findPlugin(template.path, template.datatype?.value)
|
|
267
|
-
if (plugin) {
|
|
268
|
-
if (template.config.editMode && !linked) {
|
|
269
|
-
instance = plugin.createEditor(template, value)
|
|
270
|
-
} else {
|
|
271
|
-
instance = plugin.createViewer(template, value!)
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
instance = fieldFactory(template, value || null, template.config.editMode && !linked)
|
|
275
|
-
}
|
|
276
|
-
instance.classList.add('property-instance')
|
|
277
|
-
if (linked) {
|
|
278
|
-
instance.classList.add('linked')
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (template.config.editMode) {
|
|
282
|
-
appendRemoveButton(instance, template.label, forceRemovable)
|
|
283
|
-
}
|
|
284
|
-
instance.dataset.path = template.path
|
|
285
|
-
return instance
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
|
|
289
|
-
const removeButton = new RokitButton()
|
|
290
|
-
removeButton.classList.add('remove-button', 'clear')
|
|
291
|
-
removeButton.title = 'Remove ' + label
|
|
292
|
-
removeButton.dense = true
|
|
293
|
-
removeButton.icon = true
|
|
294
|
-
removeButton.addEventListener('click', _ => {
|
|
295
|
-
instance.classList.remove('fadeIn')
|
|
296
|
-
instance.classList.add('fadeOut')
|
|
297
|
-
setTimeout(() => {
|
|
298
|
-
const parent = instance.parentElement
|
|
299
|
-
instance.remove()
|
|
300
|
-
parent?.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))
|
|
301
|
-
}, 200)
|
|
302
|
-
})
|
|
303
|
-
if (forceRemovable) {
|
|
304
|
-
removeButton.classList.add('persistent')
|
|
305
|
-
}
|
|
306
|
-
instance.appendChild(removeButton)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
window.customElements.define('shacl-property', ShaclProperty)
|