@ulb-darmstadt/shacl-form 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +13 -3
  2. package/dist/config.d.ts +4 -5
  3. package/dist/constants.d.ts +15 -13
  4. package/dist/constraints.d.ts +2 -2
  5. package/dist/exports.d.ts +2 -1
  6. package/dist/form-bootstrap.d.ts +1 -1
  7. package/dist/form-bootstrap.js +361 -2
  8. package/dist/form-default.d.ts +1 -1
  9. package/dist/form-default.js +350 -2
  10. package/dist/form-material.d.ts +1 -1
  11. package/dist/form-material.js +670 -2
  12. package/dist/form.d.ts +3 -2
  13. package/dist/node-template.d.ts +17 -0
  14. package/dist/node.d.ts +2 -1
  15. package/dist/plugins/leaflet.d.ts +2 -4
  16. package/dist/plugins/leaflet.js +720 -2
  17. package/dist/plugins/mapbox.d.ts +2 -2
  18. package/dist/plugins/mapbox.js +2764 -2
  19. package/dist/property-template.d.ts +1 -1
  20. package/dist/property.d.ts +6 -2
  21. package/dist/theme.d.ts +2 -2
  22. package/dist/themes/default.d.ts +3 -3
  23. package/dist/themes/material.d.ts +2 -3
  24. package/package.json +23 -11
  25. package/src/config.ts +11 -10
  26. package/src/constants.ts +3 -1
  27. package/src/constraints.ts +15 -18
  28. package/src/exports.ts +2 -1
  29. package/src/form.ts +32 -17
  30. package/src/group.ts +1 -1
  31. package/src/loader.ts +12 -13
  32. package/src/node-template.ts +82 -0
  33. package/src/node.ts +40 -38
  34. package/src/plugins/leaflet.ts +2 -2
  35. package/src/plugins/mapbox.ts +4 -4
  36. package/src/property-template.ts +4 -5
  37. package/src/property.ts +154 -56
  38. package/src/serialize.ts +14 -1
  39. package/src/styles.css +8 -10
  40. package/src/theme.ts +5 -5
  41. package/src/themes/bootstrap.ts +1 -1
  42. package/src/themes/default.css +2 -2
  43. package/src/themes/default.ts +12 -3
  44. package/src/themes/material.ts +12 -3
  45. package/src/util.ts +12 -14
  46. package/dist/form-bootstrap.js.LICENSE.txt +0 -69
  47. package/dist/form-default.js.LICENSE.txt +0 -69
  48. package/dist/form-material.js.LICENSE.txt +0 -69
  49. package/dist/plugins/file-upload.js +0 -1
  50. package/dist/plugins/fixed-list.js +0 -1
  51. package/dist/plugins/leaflet.js.LICENSE.txt +0 -4
  52. 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.shapesGraph.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
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?.id === `${PREFIX_SHACL}IRI`) {
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
- if (label && config.attributes.collapse === null) {
49
- const labelElem = document.createElement('label')
50
- labelElem.innerText = label
51
- this.appendChild(labelElem)
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
- anchor.innerText = valueSubject.id
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='${this.nodeId.id}']`)?.scrollIntoView()
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
- for (const quad of quads) {
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
- for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
126
- (shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
127
- }
128
- if (this.targetClass) {
129
- graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass, this.config.valuesGraph)
130
- }
131
- // if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
132
- if (this.config.attributes.generateNodeShapeReference && !this.parent) {
133
- graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject, this.config.valuesGraph)
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.shapesGraph.getQuads(shaclSubject as Term, `${PREFIX_SHACL}group`, null, null)
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
- // delay creating/appending the property until we finished parsing the node.
157
- // This is needed to have possible owlImports parsed before creating the property.
158
- setTimeout(() => {
159
- const property = new ShaclProperty(shaclSubject as NamedNode | BlankNode, this, config, valueSubject)
160
- // 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.
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)
@@ -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(template: ShaclPropertyTemplate, value: Term): HTMLElement {
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) {
@@ -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(template: ShaclPropertyTemplate, value: Term): HTMLElement {
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) {
@@ -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.shapesGraph.getSubjects(SHACL_PREDICATE_TARGET_CLASS, term, null)
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[] | undefined
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.shapesGraph.getQuads(this.qualifiedValueShape, null, null, null))
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, 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: HTMLElement | undefined
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.shapesGraph.getQuads(shaclSubject, null, null, null), parent, 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 = document.createElement('a')
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
- const values = valueSubject ? config.dataGraph.getQuads(valueSubject, this.template.path, null, null) : []
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.template.node) {
53
- const targetClasses = config.shapesGraph.getObjects(this.template.node, SHACL_PREDICATE_TARGET_CLASS, null)
54
- if (targetClasses.length > 0) {
55
- let hasTargetClass = false
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?.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
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
- instance = createPropertyInstance(this.template, value)
104
+ // check if value is part of the data graph. if not, create a linked resource
105
+ let linked = false
106
+ if (value) {
107
+ const clazz = this.getRdfClassToLinkOrCreate()
108
+ if (clazz && this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, clazz, DATA_GRAPH) === 0) {
109
+ // value is not in data graph, so must be a link in the shapes graph
110
+ linked = true
111
+ }
112
+ }
113
+ instance = createPropertyInstance(this.template, value, undefined, linked || this.template.parent.linked)
119
114
  }
120
- if (this.template.config.editMode) {
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?.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
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 = (this.template.extendedShapes && this.template.extendedShapes.length > 0) || instanceCount > 1
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.valuesGraph)
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.valuesGraph)
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 = '&#xFF0B; 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?.length) {
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 = document.createElement('a')
194
- removeButton.innerText = '\u00d7'
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['shaclDatatype']
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 .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; }
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: 6px; color: green; content: '\2713'; }
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: 5px; right: 24px; border: 0; background-color: #e9e9ed; padding: 2px 4px; max-width: 40px; width: 40px; box-sizing: content-box; }
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: 6px; color: red; cursor: help; }
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; }