@ulb-darmstadt/shacl-form 1.7.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -12
  4. package/dist/constraints.d.ts +5 -4
  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 +5 -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 +2 -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 +26 -14
  25. package/src/config.ts +11 -10
  26. package/src/constants.ts +4 -1
  27. package/src/constraints.ts +75 -31
  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 +88 -63
  34. package/src/plugins/leaflet.ts +2 -2
  35. package/src/plugins/mapbox.ts +4 -4
  36. package/src/property-template.ts +19 -8
  37. package/src/property.ts +168 -63
  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/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
- import { createShaclOrConstraint, resolveShaclOrConstraint } from './constraints'
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') {
@@ -100,27 +86,43 @@ export class ShaclProperty extends HTMLElement {
100
86
 
101
87
  addPropertyInstance(value?: Term): HTMLElement {
102
88
  let instance: HTMLElement
103
- if (this.template.shaclOr?.length) {
89
+ if (this.template.shaclOr?.length || this.template.shaclXone?.length) {
90
+ const options = this.template.shaclOr?.length ? this.template.shaclOr : this.template.shaclXone as Term[]
91
+ let resolved = false
104
92
  if (value) {
105
- instance = createPropertyInstance(resolveShaclOrConstraint(this.template, value), value, true)
106
- } else {
107
- instance = createShaclOrConstraint(this.template.shaclOr, this, this.template.config)
93
+ const resolvedOptions = resolveShaclOrConstraintOnProperty(options, value, this.template.config)
94
+ if (resolvedOptions.length) {
95
+ instance = createPropertyInstance(this.template.clone().merge(resolvedOptions), value, true)
96
+ resolved = true
97
+ }
98
+ }
99
+ if (!resolved) {
100
+ instance = createShaclOrConstraint(options, this, this.template.config)
108
101
  appendRemoveButton(instance, '')
109
102
  }
110
103
  } else {
111
- 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)
112
114
  }
113
- if (this.template.config.editMode) {
114
- this.insertBefore(instance, this.addButton!)
115
+ if (this.addButton) {
116
+ this.insertBefore(instance!, this.addButton)
115
117
  } else {
116
- this.appendChild(instance)
118
+ this.appendChild(instance!)
117
119
  }
118
- return instance
120
+ return instance!
119
121
  }
120
122
 
121
123
  updateControls() {
122
124
  let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
123
- if (instanceCount === 0 && (!this.template.extendedShapes?.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))) {
124
126
  this.addPropertyInstance()
125
127
  instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
126
128
  }
@@ -128,7 +130,7 @@ export class ShaclProperty extends HTMLElement {
128
130
  if (this.template.minCount !== undefined) {
129
131
  mayRemove = instanceCount > this.template.minCount
130
132
  } else {
131
- mayRemove = (this.template.extendedShapes && this.template.extendedShapes.length > 0) || instanceCount > 1
133
+ mayRemove = this.template.extendedShapes.length > 0 || instanceCount > 1
132
134
  }
133
135
 
134
136
  const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
@@ -141,39 +143,142 @@ export class ShaclProperty extends HTMLElement {
141
143
  const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
142
144
  if (instance.firstChild instanceof ShaclNode) {
143
145
  const shapeSubject = instance.firstChild.toRDF(graph)
144
- graph.addQuad(subject, pathNode, shapeSubject, this.template.config.valuesGraph)
146
+ graph.addQuad(subject, pathNode, shapeSubject, this.template.config.valuesGraphId)
145
147
  } else {
146
148
  for (const editor of instance.querySelectorAll<Editor>(':scope > .editor')) {
147
149
  const value = toRDF(editor)
148
150
  if (value) {
149
- graph.addQuad(subject, pathNode, value, this.template.config.valuesGraph)
151
+ graph.addQuad(subject, pathNode, value, this.template.config.valuesGraphId)
150
152
  }
151
153
  }
152
154
  }
153
155
  }
154
156
  }
157
+
158
+ getRdfClassToLinkOrCreate() {
159
+ if (this.template.class && this.template.node) {
160
+ return this.template.class
161
+ }
162
+ else {
163
+ for (const node of this.template.extendedShapes) {
164
+ // if this property has no sh:class but sh:node, then use the node shape's sh:targetClass to find protiential instances
165
+ const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
166
+ if (targetClasses.length > 0) {
167
+ return targetClasses[0] as NamedNode
168
+ }
169
+ }
170
+ }
171
+ return undefined
172
+ }
173
+
174
+ isValueValid(value: Term) {
175
+ if (!this.template.extendedShapes.length) {
176
+ // property has no node shape, so value is valid
177
+ return true
178
+ }
179
+ // property has node shape(s), so check if value conforms to any targetClass
180
+ for (const node of this.template.extendedShapes) {
181
+ const targetClasses = this.template.config.store.getObjects(node, SHACL_PREDICATE_TARGET_CLASS, null)
182
+ for (const targetClass of targetClasses) {
183
+ if (this.template.config.store.countQuads(value, RDF_PREDICATE_TYPE, targetClass, null) > 0) {
184
+ return true
185
+ }
186
+ }
187
+ }
188
+ return false
189
+ }
190
+
191
+ createAddButton() {
192
+ const addButton = new RokitSelect()
193
+ addButton.dense = true
194
+ addButton.label = "+ " + this.template.label
195
+ addButton.title = 'Add ' + this.template.label
196
+ addButton.classList.add('add-button')
197
+
198
+ // load potential value candidates for linking
199
+ let instances: InputListEntry[] = []
200
+ let clazz = this.getRdfClassToLinkOrCreate()
201
+ if (clazz) {
202
+ instances = findInstancesOf(clazz, this.template)
203
+ }
204
+ if (instances.length === 0) {
205
+ // no class instances found, so create an add button that creates a new instance
206
+ addButton.emptyMessage = ''
207
+ addButton.inputMinWidth = 0
208
+ addButton.addEventListener('click', _ => {
209
+ addButton.blur()
210
+ const instance = this.addPropertyInstance()
211
+ instance.classList.add('fadeIn')
212
+ this.updateControls()
213
+ setTimeout(() => {
214
+ focusFirstInputElement(instance)
215
+ instance.classList.remove('fadeIn')
216
+ }, 200)
217
+ })
218
+ } else {
219
+ // some instances found, so create an add button that can create a new instance or link existing ones
220
+ const ul = document.createElement('ul')
221
+ const newItem = document.createElement('li')
222
+ newItem.innerHTML = '&#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
+ }
155
257
  }
156
258
 
157
- export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false): HTMLElement {
259
+ export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false, linked = false): HTMLElement {
158
260
  let instance: HTMLElement
159
- if (template.extendedShapes?.length) {
261
+ if (template.extendedShapes.length) {
160
262
  instance = document.createElement('div')
161
263
  instance.classList.add('property-instance')
162
264
  for (const node of template.extendedShapes) {
163
- instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label))
265
+ instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label, linked))
164
266
  }
165
267
  } else {
166
268
  const plugin = findPlugin(template.path, template.datatype?.value)
167
269
  if (plugin) {
168
- if (template.config.editMode) {
270
+ if (template.config.editMode && !linked) {
169
271
  instance = plugin.createEditor(template, value)
170
272
  } else {
171
273
  instance = plugin.createViewer(template, value!)
172
274
  }
173
275
  } else {
174
- instance = fieldFactory(template, value || null)
276
+ instance = fieldFactory(template, value || null, template.config.editMode && !linked)
175
277
  }
176
278
  instance.classList.add('property-instance')
279
+ if (linked) {
280
+ instance.classList.add('linked')
281
+ }
177
282
  }
178
283
  if (template.config.editMode) {
179
284
  appendRemoveButton(instance, template.label, forceRemovable)
@@ -183,10 +288,10 @@ export function createPropertyInstance(template: ShaclPropertyTemplate, value?:
183
288
  }
184
289
 
185
290
  function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
186
- const removeButton = document.createElement('a')
187
- removeButton.innerText = '\u00d7'
188
- removeButton.classList.add('control-button', 'btn', 'remove-button')
291
+ const removeButton = new RokitButton()
292
+ removeButton.classList.add('remove-button', 'clear')
189
293
  removeButton.title = 'Remove ' + label
294
+ removeButton.dense = true
190
295
  removeButton.addEventListener('click', _ => {
191
296
  instance.classList.remove('fadeIn')
192
297
  instance.classList.add('fadeOut')
package/src/serialize.ts CHANGED
@@ -46,11 +46,13 @@ function serializeJsonld(quads: Quad[]): string {
46
46
  }
47
47
 
48
48
  export function toRDF(editor: Editor): Literal | NamedNode | undefined {
49
- let languageOrDatatype: NamedNode<string> | string | undefined = editor['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; }
package/src/theme.ts CHANGED
@@ -20,7 +20,7 @@ export abstract class Theme {
20
20
  this.stylesheet.replaceSync(aggregatedStyles)
21
21
  }
22
22
 
23
- apply(root: HTMLFormElement) {
23
+ apply(_: HTMLFormElement) {
24
24
  // NOP
25
25
  }
26
26
 
@@ -35,7 +35,7 @@ export abstract class Theme {
35
35
  let name = value.value
36
36
  let lang: HTMLElement | null = null
37
37
  if (value instanceof NamedNode) {
38
- const quads = template.config.shapesGraph.getQuads(name, null, null, null)
38
+ const quads = template.config.store.getQuads(name, null, null, null)
39
39
  if (quads.length) {
40
40
  const s = findLabel(quads, template.config.languages)
41
41
  if (s) {
@@ -79,8 +79,8 @@ export abstract class Theme {
79
79
  abstract createButton(label: string, primary: boolean): HTMLElement
80
80
  }
81
81
 
82
- export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null): HTMLElement {
83
- if (template.config.editMode) {
82
+ export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null, editable: boolean): HTMLElement {
83
+ if (editable) {
84
84
  const required = template.minCount !== undefined && template.minCount > 0
85
85
  // if we have a class, find the instances and display them in a list
86
86
  if (template.class) {
@@ -91,7 +91,7 @@ export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null
91
91
  if (template.shaclIn) {
92
92
  const list = template.config.lists[template.shaclIn]
93
93
  if (list?.length) {
94
- const listEntries = createInputListEntries(list, template.config.shapesGraph, template.config.languages)
94
+ const listEntries = createInputListEntries(list, template.config.store, template.config.languages)
95
95
  return template.config.theme.createListEditor(template.label, value, required, listEntries, template)
96
96
  }
97
97
  else {
@@ -2,7 +2,7 @@ import { DefaultTheme } from './default'
2
2
  import { Term } from '@rdfjs/types'
3
3
  import { ShaclPropertyTemplate } from '../property-template'
4
4
  import { Editor } from '../theme'
5
- import bootstrap from 'bootstrap/dist/css/bootstrap.min.css'
5
+ import bootstrap from 'bootstrap/dist/css/bootstrap.min.css?raw'
6
6
  import css from './bootstrap.css?raw'
7
7
 
8
8
  export class BootstrapTheme extends DefaultTheme {
@@ -1,4 +1,4 @@
1
1
  .editor:not([type='checkbox']) { border: 1px solid #DDD; padding: 2px 4px; }
2
- .property-instance label { display: inline-block; word-break: break-word; line-height: 1em; padding-top: 0.15em; padding-right: 1em; flex-shrink: 0; position: relative; }
3
- .property-instance:not(:first-child) > label { visibility: hidden; max-height: 0; }
2
+ .property-instance label { display: inline-flex; word-break: break-word; line-height: 1em; padding-top: 0.15em; padding-right: 1em; flex-shrink: 0; position: relative; }
3
+ .property-instance:not(:first-child) > label:not(.persistent) { visibility: hidden; max-height: 0; }
4
4
  .mode-edit .property-instance label { width: var(--label-width); }
@@ -3,6 +3,7 @@ import { ShaclPropertyTemplate } from "../property-template"
3
3
  import { Editor, InputListEntry, Theme } from "../theme"
4
4
  import { PREFIX_XSD } from '../constants'
5
5
  import { Literal } from 'n3'
6
+ import { Term as N3Term } from 'n3'
6
7
  import css from './default.css?raw'
7
8
 
8
9
  export class DefaultTheme extends Theme {
@@ -17,7 +18,9 @@ export class DefaultTheme extends Theme {
17
18
  editor.classList.add('editor')
18
19
  if (template?.datatype) {
19
20
  // store datatype on editor, this is used for RDF serialization
20
- editor['shaclDatatype'] = template.datatype
21
+ editor.shaclDatatype = template.datatype
22
+ } else if (value instanceof Literal) {
23
+ editor.shaclDatatype = value.datatype
21
24
  }
22
25
  if (template?.minCount !== undefined) {
23
26
  editor.dataset.minCount = String(template.minCount)
@@ -200,7 +203,13 @@ export class DefaultTheme extends Theme {
200
203
 
201
204
  for (const item of listEntries) {
202
205
  const option = document.createElement('option')
203
- const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
206
+ let itemValue = ''
207
+ if (typeof item.value === 'string') {
208
+ itemValue = item.value
209
+ } else {
210
+ // this is needed for typed rdf literals
211
+ itemValue = (item.value as N3Term).id
212
+ }
204
213
  option.innerHTML = item.label ? item.label : itemValue
205
214
  option.value = itemValue
206
215
  if (item.indent) {
@@ -231,7 +240,7 @@ export class DefaultTheme extends Theme {
231
240
  return result
232
241
  }
233
242
 
234
- createButton(label: string, primary: boolean): HTMLElement {
243
+ createButton(label: string, _: boolean): HTMLElement {
235
244
  const button = document.createElement('button')
236
245
  button.type = 'button'
237
246
  button.innerHTML = label
@@ -4,6 +4,7 @@ import { Button, TextField, Select, MenuItem, Checkbox } from 'mdui'
4
4
  import { Theme } from '../theme'
5
5
  import { InputListEntry, Editor } from '../theme'
6
6
  import { Literal } from 'n3'
7
+ import { Term as N3Term } from 'n3'
7
8
  import css from './material.css?raw'
8
9
  import { PREFIX_XSD } from '../constants'
9
10
 
@@ -16,7 +17,9 @@ export class MaterialTheme extends Theme {
16
17
  editor.classList.add('editor')
17
18
  if (template?.datatype) {
18
19
  // store datatype on editor, this is used for RDF serialization
19
- editor['shaclDatatype'] = template.datatype
20
+ editor.shaclDatatype = template.datatype
21
+ } else if (value instanceof Literal) {
22
+ editor.shaclDatatype = value.datatype
20
23
  }
21
24
  if (template?.minCount !== undefined) {
22
25
  editor.dataset.minCount = String(template.minCount)
@@ -100,7 +103,13 @@ export class MaterialTheme extends Theme {
100
103
 
101
104
  for (const item of listEntries) {
102
105
  const option = new MenuItem()
103
- const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
106
+ let itemValue = ''
107
+ if (typeof item.value === 'string') {
108
+ itemValue = item.value
109
+ } else {
110
+ // this is needed for typed rdf literals
111
+ itemValue = (item.value as N3Term).id
112
+ }
104
113
  const itemLabel = item.label ? item.label : itemValue
105
114
  option.value = itemValue
106
115
  option.textContent = itemLabel || itemValue
@@ -142,7 +151,7 @@ export class MaterialTheme extends Theme {
142
151
  return result
143
152
  }
144
153
 
145
- createDateEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
154
+ createDateEditor(_: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
146
155
  const editor = new TextField()
147
156
  editor.variant = 'outlined'
148
157
  editor.helper = template?.description?.value || template?.label || ''
package/src/util.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Literal, NamedNode, Prefixes, Quad, Store } from 'n3'
2
- import { OWL_OBJECT_NAMED_INDIVIDUAL, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER } from './constants'
2
+ import { DATA_GRAPH, OWL_OBJECT_NAMED_INDIVIDUAL, PREFIX_FOAF, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER } from './constants'
3
3
  import { Term } from '@rdfjs/types'
4
4
  import { InputListEntry } from './theme'
5
5
  import { ShaclPropertyTemplate } from './property-template'
@@ -48,11 +48,9 @@ export function focusFirstInputElement(context: HTMLElement) {
48
48
  }
49
49
 
50
50
  export function findLabel(quads: Quad[], languages: string[]): string {
51
- let label = findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, languages)
52
- if (label) {
53
- return label
54
- }
55
- return findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages)
51
+ return findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, languages) ||
52
+ findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages) ||
53
+ findObjectValueByPredicate(quads, 'name', PREFIX_FOAF, languages)
56
54
  }
57
55
 
58
56
  export function createInputListEntries(subjects: Term[], shapesGraph: Store, languages: string[], indent?: number): InputListEntry[] {
@@ -92,22 +90,22 @@ export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplat
92
90
  instances = list?.length ? list : []
93
91
  } else {
94
92
  // find instances in the shapes graph
95
- instances = template.config.shapesGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH)
93
+ instances = template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH)
96
94
  // find instances in the data graph
97
- instances.push(...template.config.dataGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, null))
95
+ instances.push(...template.config.store.getSubjects(RDF_PREDICATE_TYPE, clazz, DATA_GRAPH))
98
96
  // find instances in imported taxonomies
99
- findClassInstancesFromOwlImports(clazz, template, template.config.shapesGraph, instances)
97
+ findClassInstancesFromOwlImports(clazz, template, template.config.store, instances)
100
98
  }
101
99
 
102
- const entries = createInputListEntries(instances, template.config.shapesGraph, template.config.languages, indent)
100
+ const entries = createInputListEntries(instances, template.config.store, template.config.languages, indent)
103
101
  // build inheritance tree only if sh:in is not defined
104
102
  if (template.shaclIn === undefined) {
105
- for (const subClass of template.config.shapesGraph.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) {
103
+ for (const subClass of template.config.store.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) {
106
104
  entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
107
105
  }
108
- if (template.config.shapesGraph.getQuads(clazz, RDF_PREDICATE_TYPE, OWL_OBJECT_NAMED_INDIVIDUAL, null).length > 0) {
109
- entries.push(...createInputListEntries([ clazz ], template.config.shapesGraph, template.config.languages, indent))
110
- for (const subClass of template.config.shapesGraph.getSubjects(SKOS_PREDICATE_BROADER, clazz, null)) {
106
+ if (template.config.store.getQuads(clazz, RDF_PREDICATE_TYPE, OWL_OBJECT_NAMED_INDIVIDUAL, null).length > 0) {
107
+ entries.push(...createInputListEntries([ clazz ], template.config.store, template.config.languages, indent))
108
+ for (const subClass of template.config.store.getSubjects(SKOS_PREDICATE_BROADER, clazz, null)) {
111
109
  entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
112
110
  }
113
111
  }