@ulb-darmstadt/shacl-form 1.6.1 → 1.6.3

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.
@@ -0,0 +1,41 @@
1
+ import { Point, Polygon } from 'geojson'
2
+
3
+ export type Geometry = Point | Polygon
4
+
5
+ export const worldBounds: [number, number][] = [[-90, -180], [90, 180]]
6
+
7
+ export function wktToGeometry(wkt: string): Geometry | undefined {
8
+ const pointCoords = wkt.match(/^POINT\((.*)\)$/)
9
+ if (pointCoords?.length == 2) {
10
+ const xy = pointCoords[1].split(' ')
11
+ if (xy.length === 2) {
12
+ return { type: 'Point', coordinates: [parseFloat(xy[0]), parseFloat(xy[1])] }
13
+ }
14
+ }
15
+ const polygonCoords = wkt.match(/^POLYGON[(]{2}(.*)[)]{2}$/)
16
+ if (polygonCoords?.length == 2) {
17
+ const split = polygonCoords[1].split(',')
18
+ if (split.length > 2) {
19
+ const coords: number[][][] = []
20
+ const outer: number[][] = []
21
+ coords.push(outer)
22
+ for (const coord of split) {
23
+ const xy = coord.split(' ')
24
+ if (xy.length === 2) {
25
+ outer.push([parseFloat(xy[0]), parseFloat(xy[1])])
26
+ }
27
+ }
28
+ return { type: 'Polygon', coordinates: coords }
29
+ }
30
+ }
31
+ }
32
+
33
+ export function geometryToWkt(geometry: Geometry): string {
34
+ if (geometry.type === 'Point') {
35
+ return `POINT(${geometry.coordinates.join(' ')})`
36
+ } else if (geometry.type === 'Polygon') {
37
+ return `POLYGON((${geometry.coordinates[0].map(item => { return item.join(' ') }).join(',')}))`
38
+ } else {
39
+ return ''
40
+ }
41
+ }
@@ -0,0 +1,157 @@
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'
8
+ import mapboxGlDrawCss from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
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">&#x24D8; Draw a polygon or point, then close dialog</div>
24
+ <button class="closeButton" type="button" onclick="this.parentElement.close()">&#x2715;</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&#160;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)
110
+ instance.appendChild(button)
111
+ return instance
112
+ }
113
+
114
+ createViewer(template: 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
+ }
@@ -0,0 +1,132 @@
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, 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; if (!template.name || literal.language === template.config.attributes.language) { template.name = literal } },
10
+ [`${PREFIX_SHACL}description`]: (template, term) => { const literal = term as Literal; if (!template.description || literal.language === template.config.attributes.language) { 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_OA}styleClass`]: (template, term) => { template.cssClass = term.value },
27
+ [`${PREFIX_SHACL}and`]: (template, term) => { template.shaclAnd = term.value },
28
+ [`${PREFIX_SHACL}in`]: (template, term) => { template.shaclIn = term.value },
29
+ // 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'.
30
+ [`${PREFIX_SHACL}languageIn`]: (template, term) => { template.languageIn = template.config.lists[term.value]; template.datatype = DataFactory.namedNode(PREFIX_RDF + 'langString') },
31
+ [`${PREFIX_SHACL}defaultValue`]: (template, term) => { template.defaultValue = term },
32
+ [`${PREFIX_SHACL}hasValue`]: (template, term) => { template.hasValue = term },
33
+ [OWL_PREDICATE_IMPORTS.id]: (template, term) => { template.owlImports.push(term as NamedNode) },
34
+ [SHACL_PREDICATE_CLASS.id]: (template, term) => {
35
+ template.class = term as NamedNode
36
+ // try to find node shape that has requested target class
37
+ const nodeShapes = template.config.shapesGraph.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
38
+ if (nodeShapes.length > 0) {
39
+ template.node = nodeShapes[0] as NamedNode
40
+ }
41
+ },
42
+ [`${PREFIX_SHACL}or`]: (template, term) => {
43
+ const list = template.config.lists[term.value]
44
+ if (list?.length) {
45
+ template.shaclOr = list
46
+ } else {
47
+ console.error('list not found:', term.value, 'existing lists:', template.config.lists)
48
+ }
49
+ }
50
+ }
51
+
52
+ export class ShaclPropertyTemplate {
53
+ parent: ShaclNode
54
+ label = ''
55
+ name: Literal | undefined
56
+ description: Literal | undefined
57
+ path: string | undefined
58
+ node: NamedNode | undefined
59
+ class: NamedNode | undefined
60
+ minCount: number | undefined
61
+ maxCount: number | undefined
62
+ minLength: number | undefined
63
+ maxLength: number | undefined
64
+ minInclusive: number | undefined
65
+ maxInclusive: number | undefined
66
+ minExclusive: number | undefined
67
+ maxExclusive: number | undefined
68
+ singleLine: boolean | undefined
69
+ cssClass: string | undefined
70
+ defaultValue: Term | undefined
71
+ pattern: string | undefined
72
+ order: number | undefined
73
+ nodeKind: NamedNode | undefined
74
+ shaclAnd: string | undefined
75
+ shaclIn: string | undefined
76
+ shaclOr: Term[] | undefined
77
+ languageIn: Term[] | undefined
78
+ datatype: NamedNode | undefined
79
+ hasValue: Term | undefined
80
+ owlImports: NamedNode[] = []
81
+
82
+ config: Config
83
+ extendedShapes: NamedNode[] | undefined
84
+
85
+ constructor(quads: Quad[], parent: ShaclNode, config: Config) {
86
+ this.parent = parent
87
+ this.config = config
88
+ this.merge(quads)
89
+ }
90
+
91
+ merge(quads: Quad[]): ShaclPropertyTemplate {
92
+ for (const quad of quads) {
93
+ mappers[quad.predicate.id]?.call(this, this, quad.object)
94
+ }
95
+ // provide best fitting label for UI
96
+ this.label = this.name?.value || findLabel(quads, this.config.languages)
97
+ if (!this.label && !this.shaclAnd) {
98
+ this.label = this.path ? removePrefixes(this.path, this.config.prefixes) : 'unknown'
99
+ }
100
+ // resolve extended shapes
101
+ if (this.node || this.shaclAnd) {
102
+ this.extendedShapes = []
103
+ if (this.node) {
104
+ this.extendedShapes.push(this.node)
105
+ }
106
+ if (this.shaclAnd) {
107
+ const list = this.config.lists[this.shaclAnd]
108
+ if (list?.length) {
109
+ for (const node of list) {
110
+ this.extendedShapes.push(node as NamedNode)
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return this
116
+ }
117
+
118
+ clone(): ShaclPropertyTemplate {
119
+ const copy = Object.assign({}, this)
120
+ // arrays are not cloned but referenced, so create them manually
121
+ copy.owlImports = [ ...this.owlImports ]
122
+ if (this.languageIn) {
123
+ copy.languageIn = [ ...this.languageIn ]
124
+ }
125
+ if (this.shaclOr) {
126
+ copy.shaclOr = [ ...this.shaclOr ]
127
+ }
128
+ copy.merge = this.merge.bind(copy)
129
+ copy.clone = this.clone.bind(copy)
130
+ return copy
131
+ }
132
+ }
@@ -0,0 +1,188 @@
1
+ import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
2
+ import { Term } from '@rdfjs/types'
3
+ import { ShaclNode } from './node'
4
+ import { focusFirstInputElement } from './util'
5
+ import { createShaclOrConstraint, resolveShaclOrConstraint } from './constraints'
6
+ import { Config } from './config'
7
+ import { ShaclPropertyTemplate } from './property-template'
8
+ import { Editor, fieldFactory } from './theme'
9
+ import { toRDF } from './serialize'
10
+ import { findPlugin } from './plugin'
11
+
12
+ export class ShaclProperty extends HTMLElement {
13
+ template: ShaclPropertyTemplate
14
+ addButton: HTMLElement | undefined
15
+
16
+ constructor(shaclSubject: BlankNode | NamedNode, parent: ShaclNode, config: Config, valueSubject?: NamedNode | BlankNode) {
17
+ super()
18
+ this.template = new ShaclPropertyTemplate(config.shapesGraph.getQuads(shaclSubject, null, null, null), parent, config)
19
+
20
+ if (this.template.order !== undefined) {
21
+ this.style.order = `${this.template.order}`
22
+ }
23
+ if (this.template.cssClass) {
24
+ this.classList.add(this.template.cssClass)
25
+ }
26
+
27
+ if (config.editMode) {
28
+ this.addButton = document.createElement('a')
29
+ this.addButton.innerText = this.template.label
30
+ this.addButton.title = 'Add ' + this.template.label
31
+ this.addButton.classList.add('control-button', 'add-button')
32
+ this.addButton.addEventListener('click', _ => {
33
+ const instance = this.addPropertyInstance()
34
+ instance.classList.add('fadeIn')
35
+ this.updateControls()
36
+ focusFirstInputElement(instance)
37
+ setTimeout(() => {
38
+ instance.classList.remove('fadeIn')
39
+ }, 200)
40
+ })
41
+ this.appendChild(this.addButton)
42
+ }
43
+
44
+ // bind existing values
45
+ if (this.template.path) {
46
+ const values = valueSubject ? config.dataGraph.getQuads(valueSubject, this.template.path, null, null) : []
47
+ let valuesContainHasValue = false
48
+ for (const value of values) {
49
+ this.addPropertyInstance(value.object)
50
+ if (this.template.hasValue && value.object.equals(this.template.hasValue)) {
51
+ valuesContainHasValue = true
52
+ }
53
+ }
54
+ if (config.editMode && this.template.hasValue && !valuesContainHasValue) {
55
+ // sh:hasValue is defined in shapes graph, but does not exist in data graph, so force it
56
+ this.addPropertyInstance(this.template.hasValue)
57
+ }
58
+ }
59
+
60
+ if (config.editMode) {
61
+ this.addEventListener('change', () => { this.updateControls() })
62
+ this.updateControls()
63
+ }
64
+
65
+ if (this.template.extendedShapes?.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
66
+ // in view mode, show collapsible only when we have something to show
67
+ if (config.editMode || this.childElementCount > 0) {
68
+ const collapsible = this
69
+ collapsible.classList.add('collapsible')
70
+ if (this.template.config.attributes.collapse === 'open') {
71
+ collapsible.classList.add('open')
72
+ }
73
+ const activator = document.createElement('h1')
74
+ activator.classList.add('activator')
75
+ activator.innerText = this.template.label
76
+ activator.addEventListener('click', () => {
77
+ collapsible.classList.toggle('open')
78
+ })
79
+ this.prepend(activator)
80
+ }
81
+ }
82
+ }
83
+
84
+ addPropertyInstance(value?: Term): HTMLElement {
85
+ let instance: HTMLElement
86
+ if (this.template.shaclOr?.length) {
87
+ if (value) {
88
+ instance = createPropertyInstance(resolveShaclOrConstraint(this.template, value), value, true)
89
+ } else {
90
+ instance = createShaclOrConstraint(this.template.shaclOr, this, this.template.config)
91
+ appendRemoveButton(instance, '')
92
+ }
93
+ } else {
94
+ instance = createPropertyInstance(this.template, value)
95
+ }
96
+ if (this.template.config.editMode) {
97
+ this.insertBefore(instance, this.addButton!)
98
+ } else {
99
+ this.appendChild(instance)
100
+ }
101
+ return instance
102
+ }
103
+
104
+ updateControls() {
105
+ let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
106
+ if (instanceCount === 0 && (!this.template.extendedShapes?.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
107
+ this.addPropertyInstance()
108
+ instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
109
+ }
110
+ let mayRemove: boolean
111
+ if (this.template.minCount !== undefined) {
112
+ mayRemove = instanceCount > this.template.minCount
113
+ } else {
114
+ mayRemove = (this.template.extendedShapes && this.template.extendedShapes.length > 0) || instanceCount > 1
115
+ }
116
+
117
+ const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
118
+ this.classList.toggle('may-remove', mayRemove)
119
+ this.classList.toggle('may-add', mayAdd)
120
+ }
121
+
122
+ toRDF(graph: Store, subject: NamedNode | BlankNode) {
123
+ for (const instance of this.querySelectorAll(':scope > .property-instance')) {
124
+ const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
125
+ if (instance.firstChild instanceof ShaclNode) {
126
+ const quadCount = graph.size
127
+ const shapeSubject = instance.firstChild.toRDF(graph)
128
+ graph.addQuad(subject, pathNode, shapeSubject)
129
+ } else {
130
+ const editor = instance.querySelector('.editor') as Editor
131
+ const value = toRDF(editor)
132
+ if (value) {
133
+ graph.addQuad(subject, pathNode, value)
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false): HTMLElement {
141
+ let instance: HTMLElement
142
+ if (template.extendedShapes?.length) {
143
+ instance = document.createElement('div')
144
+ instance.classList.add('property-instance')
145
+ for (const node of template.extendedShapes) {
146
+ instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label))
147
+ }
148
+ } else {
149
+ const plugin = findPlugin(template.path, template.datatype?.value)
150
+ if (plugin) {
151
+ if (template.config.editMode) {
152
+ instance = plugin.createEditor(template, value)
153
+ } else {
154
+ instance = plugin.createViewer(template, value!)
155
+ }
156
+ } else {
157
+ instance = fieldFactory(template, value || null)
158
+ }
159
+ instance.classList.add('property-instance')
160
+ }
161
+ if (template.config.editMode) {
162
+ appendRemoveButton(instance, template.label, forceRemovable)
163
+ }
164
+ instance.dataset.path = template.path
165
+ return instance
166
+ }
167
+
168
+ function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
169
+ const removeButton = document.createElement('a')
170
+ removeButton.innerText = '\u00d7'
171
+ removeButton.classList.add('control-button', 'btn', 'remove-button')
172
+ removeButton.title = 'Remove ' + label
173
+ removeButton.addEventListener('click', _ => {
174
+ instance.classList.remove('fadeIn')
175
+ instance.classList.add('fadeOut')
176
+ setTimeout(() => {
177
+ const parent = instance.parentElement
178
+ instance.remove()
179
+ parent?.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))
180
+ }, 200)
181
+ })
182
+ if (forceRemovable) {
183
+ removeButton.classList.add('persistent')
184
+ }
185
+ instance.appendChild(removeButton)
186
+ }
187
+
188
+ window.customElements.define('shacl-property', ShaclProperty)
@@ -0,0 +1,76 @@
1
+ import { DataFactory, NamedNode, Writer, Quad, Literal, Prefixes } from 'n3'
2
+ import { PREFIX_XSD, RDF_PREDICATE_TYPE, PREFIX_SHACL } from './constants'
3
+ import { Editor } from './theme'
4
+ import { NodeObject } from 'jsonld'
5
+
6
+ export function serialize(quads: Quad[], format: string, prefixes?: Prefixes): string {
7
+ if (format === 'application/ld+json') {
8
+ return serializeJsonld(quads)
9
+ } else {
10
+ const writer = new Writer({ format: format, prefixes: prefixes })
11
+ writer.addQuads(quads)
12
+ let serialized = ''
13
+ writer.end((error, result) => {
14
+ if (error) {
15
+ console.error(error)
16
+ }
17
+ serialized = result
18
+ })
19
+ return serialized
20
+ }
21
+ }
22
+
23
+ function serializeJsonld(quads: Quad[]): string {
24
+ const triples: NodeObject[] = []
25
+ for (const quad of quads) {
26
+ const triple: NodeObject = { '@id': quad.subject.id }
27
+
28
+ if (quad.predicate === RDF_PREDICATE_TYPE) {
29
+ triple['@type'] = quad.object.id
30
+ } else {
31
+ let object: string | {} = quad.object.value
32
+ if (quad.object instanceof Literal) {
33
+ if (quad.object.language) {
34
+ object = { '@language': quad.object.language, '@value': quad.object.value }
35
+ } else if (quad.object.datatype && quad.object.datatype.value !== `${PREFIX_XSD}#string`) {
36
+ object = { '@type': quad.object.datatype.value, '@value': quad.object.value }
37
+ }
38
+ } else {
39
+ object = { '@id': quad.object.id }
40
+ }
41
+ triple[quad.predicate.value] = object
42
+ }
43
+ triples.push(triple)
44
+ }
45
+ return JSON.stringify(triples)
46
+ }
47
+
48
+ export function toRDF(editor: Editor): Literal | NamedNode | undefined {
49
+ let languageOrDatatype: NamedNode<string> | string | undefined = editor['shaclDatatype']
50
+ let value: number | string = editor.value
51
+ if (value) {
52
+ if (editor.dataset.class || editor.dataset.nodeKind === PREFIX_SHACL + 'IRI') {
53
+ return DataFactory.namedNode(value)
54
+ } else {
55
+ if (editor.dataset.lang) {
56
+ languageOrDatatype = editor.dataset.lang
57
+ }
58
+ else if (editor['type'] === 'number') {
59
+ value = parseFloat(value)
60
+ }
61
+ else if (editor['type'] === 'file' && editor['binaryData']) {
62
+ value = editor['binaryData']
63
+ }
64
+ else if (editor['type'] === 'datetime-local') {
65
+ // if seconds in value are 0, the input field omits them which is then not a valid xsd:dateTime
66
+ value = new Date(value).toISOString().slice(0, 19)
67
+ }
68
+ return DataFactory.literal(value, languageOrDatatype)
69
+ }
70
+ } else if (editor['type'] === 'checkbox' || editor.getAttribute('type') === 'checkbox') {
71
+ // emit boolean 'false' only when required
72
+ if (editor['checked'] || parseInt(editor.dataset.minCount || '0') > 0) {
73
+ return DataFactory.literal(editor['checked'] ? 'true' : 'false', languageOrDatatype)
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,2 @@
1
+ declare module 'shacl-engine'
2
+ declare module '@rdfjs/data-model'
package/src/styles.css ADDED
@@ -0,0 +1,59 @@
1
+ form { box-sizing: border-box; display:block; --label-width: 8em; --caret-size: 10px; }
2
+ form.mode-edit { padding-left: 1em; }
3
+ form *, form ::after, form ::before { box-sizing: inherit; }
4
+ shacl-node, .shacl-group { display: flex; flex-direction: column; width: 100%; position: relative; }
5
+ shacl-node .control-button { text-decoration: none; cursor: pointer; border: 1px solid transparent; border-radius: 4px; padding: 2px 4px; }
6
+ shacl-node .control-button:hover { border-color: inherit; }
7
+ shacl-node .remove-button { margin-left: 4px; }
8
+ shacl-node .add-button { font-size: 0.8rem; color: #555; margin: 4px 24px 0 0; }
9
+ shacl-node .add-button:before { content: '+'; margin-right: 0.2em; }
10
+ shacl-node .add-button:hover { color: inherit; }
11
+ shacl-node h1 { font-size: 1.1rem; border-bottom: 1px solid; margin-top: 4px; color: #555; }
12
+ shacl-property { display: flex; flex-direction: column; align-items: end; position: relative; }
13
+ shacl-property:not(.may-add) > .add-button { display: none; }
14
+ shacl-property:not(.may-remove) > .property-instance > .remove-button:not(.persistent) { visibility: hidden; }
15
+ shacl-property:not(.may-remove) > .shacl-or-constraint > .remove-button:not(.persistent) { visibility: hidden; }
16
+ .shacl-group { margin-bottom: 1em; padding-bottom: 1em; }
17
+ .mode-view .shacl-group:not(:has(shacl-property)) { display: none; }
18
+ .property-instance, .shacl-or-constraint { display: flex; align-items: flex-start; padding: 4px 0; width: 100%; position: relative; }
19
+ .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
+ .property-instance label[title] { cursor: help; text-decoration: underline dashed #AAA; }
21
+ .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: 6px; color: green; content: '\2713'; }
23
+ .editor:not([type='checkbox']), .shacl-or-constraint select { flex-grow: 1; }
24
+ .shacl-or-constraint select { border: 1px solid #DDD; padding: 2px 4px; }
25
+ select { overflow: hidden; text-overflow: ellipsis; }
26
+ textarea.editor { resize: vertical; }
27
+ .lang-chooser { position: absolute; top: 5px; right: 24px; border: 0; background-color: #e9e9ed; padding: 2px 4px; max-width: 40px; width: 40px; box-sizing: content-box; }
28
+ .lang-chooser+.editor { padding-right: 55px; }
29
+ .validation-error { position: absolute; left: calc(var(--label-width) - 1em); top: 6px; color: red; cursor: help; }
30
+ .validation-error::before { content: '\26a0' }
31
+ .validation-error.node { left: -1em; }
32
+ .invalid > .editor { border-color: red !important; }
33
+ .ml-0 { margin-left: 0 !important; }
34
+ .pr-0 { padding-right: 0 !important; }
35
+ .mode-view .property-instance:not(:first-child) > label { visibility: hidden; }
36
+ .mode-view .property-instance label { width: var(--label-width); }
37
+
38
+ .d-flex { display: flex; }
39
+ .lang { opacity: 0.65; font-size: 0.6em; }
40
+ a, a:visited { color: inherit; }
41
+
42
+ .fadeIn, .fadeOut { animation: fadeIn 0.2s ease-out; }
43
+ .fadeOut { animation-direction: reverse; animation-timing-function: ease-out;}
44
+ @keyframes fadeIn {
45
+ 0% { opacity: 0; transform: scaleY(0.8); }
46
+ 100% { opacity: 1; transform: scaleY(1); }
47
+ }
48
+
49
+ .collapsible > .activator { display: flex; justify-content: space-between; align-items: center; cursor: pointer; width: 100%; border: 0; padding: 8px 0; transition: 0.2s; }
50
+ .collapsible > .activator:hover, .collapsible.open > .activator { background-color: #F5F5F5; }
51
+ .collapsible > .activator::after { content:''; width: var(--caret-size); height: var(--caret-size); border-style: none solid solid none; border-width: calc(0.3 * var(--caret-size)); transform: rotate(45deg); transition: transform .15s ease-out; margin-right: calc(0.5 * var(--caret-size)); }
52
+ .collapsible.open > .activator::after { transform: rotate(225deg); }
53
+ .collapsible > *:not(.activator) { transition: all 0.2s ease-out; opacity: 1; }
54
+ .collapsible:not(.open) > *:not(.activator) { max-height: 0; padding: 0; opacity: 0; overflow: hidden; }
55
+ .collapsible > .property-instance > shacl-node > h1 { display: none; }
56
+ .collapsible.open > .property-instance:nth-child(odd) { background-color: #F5F5F5; }
57
+ .ref-link { cursor: pointer; }
58
+ .ref-link:hover { text-decoration: underline; }
59
+ .node-id-display { color: #999; font-size: 11px; }