@ulb-darmstadt/shacl-form 1.6.2 → 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.
- package/README.md +2 -2
- package/dist/form-bootstrap.js +1 -1
- package/dist/form-default.js +1 -1
- package/dist/form-material.js +1 -7
- package/dist/form-material.js.LICENSE.txt +0 -30
- package/package.json +3 -2
- package/src/config.ts +113 -0
- package/src/constants.ts +25 -0
- package/src/constraints.ts +106 -0
- package/src/exports.ts +6 -0
- package/src/form-bootstrap.ts +12 -0
- package/src/form-default.ts +12 -0
- package/src/form-material.ts +12 -0
- package/src/form.ts +290 -0
- package/src/globals.d.ts +2 -0
- package/src/group.ts +35 -0
- package/src/loader.ts +172 -0
- package/src/node.ts +167 -0
- package/src/plugin.ts +60 -0
- package/src/plugins/file-upload.ts +26 -0
- package/src/plugins/fixed-list.ts +19 -0
- package/src/plugins/leaflet.ts +196 -0
- package/src/plugins/map-util.ts +41 -0
- package/src/plugins/mapbox.ts +157 -0
- package/src/property-template.ts +132 -0
- package/src/property.ts +188 -0
- package/src/serialize.ts +76 -0
- package/src/shacl-engine.d.ts +2 -0
- package/src/styles.css +59 -0
- package/src/theme.ts +132 -0
- package/src/themes/bootstrap.css +6 -0
- package/src/themes/bootstrap.ts +44 -0
- package/src/themes/default.css +4 -0
- package/src/themes/default.ts +240 -0
- package/src/themes/material.css +7 -0
- package/src/themes/material.ts +239 -0
- package/src/util.ts +116 -0
|
@@ -19,36 +19,6 @@
|
|
|
19
19
|
|
|
20
20
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* @license
|
|
24
|
-
* Copyright 2014 Travis Webb
|
|
25
|
-
* SPDX-License-Identifier: MIT
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @license
|
|
30
|
-
* Copyright 2017 Google LLC
|
|
31
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @license
|
|
36
|
-
* Copyright 2018 Google LLC
|
|
37
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @license
|
|
42
|
-
* Copyright 2020 Google LLC
|
|
43
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @license
|
|
48
|
-
* Copyright 2021 Google LLC
|
|
49
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
22
|
/**
|
|
53
23
|
* A JavaScript implementation of the JSON-LD API.
|
|
54
24
|
*
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ulb-darmstadt/shacl-form",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "SHACL form generator",
|
|
5
5
|
"main": "dist/form-default.js",
|
|
6
6
|
"module": "dist/form-default.js",
|
|
7
7
|
"types": "dist/form-default.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
10
11
|
],
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Prefixes, Store } from 'n3'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { PREFIX_SHACL, RDF_PREDICATE_TYPE } from './constants'
|
|
4
|
+
import { ClassInstanceProvider } from './plugin'
|
|
5
|
+
import { Loader } from './loader'
|
|
6
|
+
import { Theme } from './theme'
|
|
7
|
+
|
|
8
|
+
export class ElementAttributes {
|
|
9
|
+
shapes: string | null = null
|
|
10
|
+
shapesUrl: string | null = null
|
|
11
|
+
shapeSubject: string | null = null
|
|
12
|
+
values: string | null = null
|
|
13
|
+
valuesUrl: string | null = null
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Use valuesSubject instead
|
|
16
|
+
*/
|
|
17
|
+
valueSubject: string | null = null // for backward compatibility
|
|
18
|
+
valuesSubject: string | null = null
|
|
19
|
+
valuesNamespace = ''
|
|
20
|
+
view: string | null = null
|
|
21
|
+
language: string | null = null
|
|
22
|
+
loading: string = 'Loading\u2026'
|
|
23
|
+
ignoreOwlImports: string | null = null
|
|
24
|
+
collapse: string | null = null
|
|
25
|
+
submitButton: string | null = null
|
|
26
|
+
generateNodeShapeReference: string | null = null
|
|
27
|
+
showNodeIds: string | null = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Config {
|
|
31
|
+
attributes = new ElementAttributes()
|
|
32
|
+
loader = new Loader(this)
|
|
33
|
+
classInstanceProvider: ClassInstanceProvider | undefined
|
|
34
|
+
prefixes: Prefixes = {}
|
|
35
|
+
editMode = true
|
|
36
|
+
languages: string[]
|
|
37
|
+
|
|
38
|
+
dataGraph = new Store()
|
|
39
|
+
lists: Record<string, Term[]> = {}
|
|
40
|
+
groups: Array<string> = []
|
|
41
|
+
theme: Theme
|
|
42
|
+
form: HTMLElement
|
|
43
|
+
renderedNodes = new Set<string>()
|
|
44
|
+
private _shapesGraph = new Store()
|
|
45
|
+
|
|
46
|
+
constructor(theme: Theme, form: HTMLElement) {
|
|
47
|
+
this.theme = theme
|
|
48
|
+
this.form = form
|
|
49
|
+
this.languages = [...new Set(navigator.languages.flatMap(lang => {
|
|
50
|
+
if (lang.length > 2) {
|
|
51
|
+
// for each 5 letter lang code (e.g. de-DE) append its corresponding 2 letter code (e.g. de) directly afterwards
|
|
52
|
+
return [lang, lang.substring(0, 2)]
|
|
53
|
+
}
|
|
54
|
+
return lang
|
|
55
|
+
}))]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updateAttributes(elem: HTMLElement) {
|
|
59
|
+
const atts = new ElementAttributes();
|
|
60
|
+
(Object.keys(atts) as Array<keyof ElementAttributes>).forEach(key => {
|
|
61
|
+
const value = elem.dataset[key]
|
|
62
|
+
if (value !== undefined) {
|
|
63
|
+
atts[key] = value
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
this.editMode = atts.view === null
|
|
67
|
+
this.attributes = atts
|
|
68
|
+
// for backward compatibility
|
|
69
|
+
if (this.attributes.valueSubject && !this.attributes.valuesSubject) {
|
|
70
|
+
this.attributes.valuesSubject = this.attributes.valueSubject
|
|
71
|
+
}
|
|
72
|
+
if (atts.language) {
|
|
73
|
+
const index = this.languages.indexOf(atts.language)
|
|
74
|
+
if (index > -1) {
|
|
75
|
+
// remove preferred language from the list of languages
|
|
76
|
+
this.languages.splice(index, 1)
|
|
77
|
+
}
|
|
78
|
+
// now prepend preferred language at start of the list of languages
|
|
79
|
+
this.languages.unshift(atts.language)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static dataAttributes(): Array<string> {
|
|
84
|
+
const atts = new ElementAttributes()
|
|
85
|
+
return Object.keys(atts).map(key => {
|
|
86
|
+
// convert camelcase key to kebap case
|
|
87
|
+
key = key.replace(/[A-Z]/g, m => "-" + m.toLowerCase());
|
|
88
|
+
return 'data-' + key
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get shapesGraph() {
|
|
93
|
+
return this._shapesGraph
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
set shapesGraph(graph: Store) {
|
|
97
|
+
this._shapesGraph = graph
|
|
98
|
+
this.lists = graph.extractLists()
|
|
99
|
+
this.groups = []
|
|
100
|
+
graph.forSubjects(subject => {
|
|
101
|
+
this.groups.push(subject.id)
|
|
102
|
+
}, RDF_PREDICATE_TYPE, `${PREFIX_SHACL}PropertyGroup`, null)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
registerPrefixes(prefixes: Prefixes) {
|
|
106
|
+
for (const key in prefixes) {
|
|
107
|
+
// ignore empty (default) namespace
|
|
108
|
+
if (key) {
|
|
109
|
+
this.prefixes[key] = prefixes[key]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DataFactory } from "n3"
|
|
2
|
+
|
|
3
|
+
export const PREFIX_SHACL = 'http://www.w3.org/ns/shacl#'
|
|
4
|
+
export const PREFIX_DASH = 'http://datashapes.org/dash#'
|
|
5
|
+
export const PREFIX_XSD = 'http://www.w3.org/2001/XMLSchema#'
|
|
6
|
+
export const PREFIX_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
|
|
7
|
+
export const PREFIX_RDFS = 'http://www.w3.org/2000/01/rdf-schema#'
|
|
8
|
+
export const PREFIX_SKOS = 'http://www.w3.org/2004/02/skos/core#'
|
|
9
|
+
export const PREFIX_OWL = 'http://www.w3.org/2002/07/owl#'
|
|
10
|
+
export const PREFIX_OA = 'http://www.w3.org/ns/oa#'
|
|
11
|
+
export const PREFIX_DCTERMS = 'http://purl.org/dc/terms/'
|
|
12
|
+
|
|
13
|
+
export const SHAPES_GRAPH = DataFactory.namedNode('shapes')
|
|
14
|
+
|
|
15
|
+
export const RDF_PREDICATE_TYPE = DataFactory.namedNode(PREFIX_RDF + 'type')
|
|
16
|
+
export const DCTERMS_PREDICATE_CONFORMS_TO = DataFactory.namedNode(PREFIX_DCTERMS + 'conformsTo')
|
|
17
|
+
export const RDFS_PREDICATE_SUBCLASS_OF = DataFactory.namedNode(PREFIX_RDFS + 'subClassOf')
|
|
18
|
+
export const SKOS_PREDICATE_BROADER = DataFactory.namedNode(PREFIX_SKOS + 'broader')
|
|
19
|
+
export const OWL_PREDICATE_IMPORTS = DataFactory.namedNode(PREFIX_OWL + 'imports')
|
|
20
|
+
export const OWL_OBJECT_NAMED_INDIVIDUAL = DataFactory.namedNode(PREFIX_OWL + 'NamedIndividual')
|
|
21
|
+
export const SHACL_OBJECT_NODE_SHAPE = DataFactory.namedNode(PREFIX_SHACL + 'NodeShape')
|
|
22
|
+
export const SHACL_OBJECT_IRI = DataFactory.namedNode(PREFIX_SHACL + 'IRI')
|
|
23
|
+
export const SHACL_PREDICATE_CLASS = DataFactory.namedNode(PREFIX_SHACL + 'class')
|
|
24
|
+
export const SHACL_PREDICATE_TARGET_CLASS = DataFactory.namedNode(PREFIX_SHACL + 'targetClass')
|
|
25
|
+
export const SHACL_PREDICATE_NODE_KIND = DataFactory.namedNode(PREFIX_SHACL + 'nodeKind')
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { BlankNode, Literal, NamedNode, Quad } from 'n3'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { ShaclNode } from "./node"
|
|
4
|
+
import { ShaclProperty, createPropertyInstance } from "./property"
|
|
5
|
+
import { Config } from './config'
|
|
6
|
+
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHACL_PREDICATE_TARGET_CLASS, SHACL_PREDICATE_NODE_KIND, SHACL_OBJECT_IRI } from './constants'
|
|
7
|
+
import { findLabel, removePrefixes } from './util'
|
|
8
|
+
import { ShaclPropertyTemplate } from './property-template'
|
|
9
|
+
import { Editor, InputListEntry } from './theme'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function createShaclOrConstraint(options: Term[], context: ShaclNode | ShaclProperty, config: Config): HTMLElement {
|
|
13
|
+
const constraintElement = document.createElement('div')
|
|
14
|
+
constraintElement.classList.add('shacl-or-constraint')
|
|
15
|
+
|
|
16
|
+
const optionElements: InputListEntry[] = []
|
|
17
|
+
optionElements.push({ label: '--- please choose ---', value: '' })
|
|
18
|
+
|
|
19
|
+
if (context instanceof ShaclNode) {
|
|
20
|
+
const properties: ShaclProperty[] = []
|
|
21
|
+
// expect options to be shacl properties
|
|
22
|
+
for (let i = 0; i < options.length; i++) {
|
|
23
|
+
const property = new ShaclProperty(options[i] as NamedNode | BlankNode, context, config)
|
|
24
|
+
properties.push(property)
|
|
25
|
+
optionElements.push({ label: property.template.label, value: i.toString() })
|
|
26
|
+
}
|
|
27
|
+
const editor = config.theme.createListEditor('Please choose', null, false, optionElements)
|
|
28
|
+
const select = editor.querySelector('.editor') as Editor
|
|
29
|
+
select.onchange = () => {
|
|
30
|
+
if (select.value) {
|
|
31
|
+
constraintElement.replaceWith(properties[parseInt(select.value)])
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
constraintElement.appendChild(editor)
|
|
35
|
+
} else {
|
|
36
|
+
const values: Quad[][] = []
|
|
37
|
+
for (let i = 0; i < options.length; i++) {
|
|
38
|
+
const quads = config.shapesGraph.getQuads(options[i], null, null, null)
|
|
39
|
+
if (quads.length) {
|
|
40
|
+
values.push(quads)
|
|
41
|
+
optionElements.push({ label: findLabel(quads, config.languages) || (removePrefixes(quads[0].predicate.value, config.prefixes) + ' = ' + removePrefixes(quads[0].object.value, config.prefixes)), value: i.toString() })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const editor = config.theme.createListEditor(context.template.label + '?', null, false, optionElements, context.template)
|
|
45
|
+
const select = editor.querySelector('.editor') as Editor
|
|
46
|
+
select.onchange = () => {
|
|
47
|
+
if (select.value) {
|
|
48
|
+
constraintElement.replaceWith(createPropertyInstance(context.template.clone().merge(values[parseInt(select.value)]), undefined, true))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
constraintElement.appendChild(editor)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return constraintElement
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveShaclOrConstraint(template: ShaclPropertyTemplate, value: Term): ShaclPropertyTemplate {
|
|
58
|
+
if (!template.shaclOr) {
|
|
59
|
+
console.warn('can\'t resolve sh:or because template has no options', template)
|
|
60
|
+
return template
|
|
61
|
+
}
|
|
62
|
+
if (value instanceof Literal) {
|
|
63
|
+
// value is a literal, try to resolve sh:or by matching on given value datatype
|
|
64
|
+
const valueType = value.datatype
|
|
65
|
+
for (const subject of template.shaclOr) {
|
|
66
|
+
const options = template.config.shapesGraph.getQuads(subject, null, null, null)
|
|
67
|
+
for (const quad of options) {
|
|
68
|
+
if (quad.predicate.value === `${PREFIX_SHACL}datatype` && quad.object.equals(valueType)) {
|
|
69
|
+
return template.clone().merge(options)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// value is a NamedNode or BlankNode, try to resolve sh:or by matching rdf:type of given value with sh:node or sh:class in data graph or shapes graph
|
|
75
|
+
let types = template.config.dataGraph.getObjects(value, RDF_PREDICATE_TYPE, null)
|
|
76
|
+
types.push(...template.config.shapesGraph.getObjects(value, RDF_PREDICATE_TYPE, null))
|
|
77
|
+
for (const subject of template.shaclOr) {
|
|
78
|
+
const options = template.config.shapesGraph.getQuads(subject, null, null, null)
|
|
79
|
+
for (const quad of options) {
|
|
80
|
+
if (types.length > 0) {
|
|
81
|
+
// try to find matching sh:node in sh:or values
|
|
82
|
+
if (quad.predicate.value === `${PREFIX_SHACL}node`) {
|
|
83
|
+
for (const type of types) {
|
|
84
|
+
if (template.config.shapesGraph.getQuads(quad.object, SHACL_PREDICATE_TARGET_CLASS, type, null).length > 0) {
|
|
85
|
+
return template.clone().merge(options)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// try to find matching sh:class in sh:or values
|
|
90
|
+
if (quad.predicate.equals(SHACL_PREDICATE_CLASS)) {
|
|
91
|
+
for (const type of types) {
|
|
92
|
+
if (quad.object.equals(type)) {
|
|
93
|
+
return template.clone().merge(options)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else if (quad.predicate.equals(SHACL_PREDICATE_NODE_KIND) && quad.object.equals(SHACL_OBJECT_IRI)) {
|
|
98
|
+
// if sh:nodeKind is sh:IRI, just use that
|
|
99
|
+
return template.clone().merge(options)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
console.error('couldn\'t resolve sh:or for value', value)
|
|
105
|
+
return template
|
|
106
|
+
}
|
package/src/exports.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Theme, InputListEntry, Editor } from './theme'
|
|
2
|
+
export { Loader, setSharedShapesGraph } from './loader'
|
|
3
|
+
export { Config } from './config'
|
|
4
|
+
export { Plugin, registerPlugin } from './plugin'
|
|
5
|
+
export { ShaclPropertyTemplate } from './property-template'
|
|
6
|
+
export { findLabel } from './util'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ShaclForm as FormBase } from "./form"
|
|
2
|
+
import { BootstrapTheme } from "./themes/bootstrap"
|
|
3
|
+
|
|
4
|
+
export * from './exports'
|
|
5
|
+
|
|
6
|
+
export class ShaclForm extends FormBase {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(new BootstrapTheme())
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
window.customElements.define('shacl-form', ShaclForm)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ShaclForm as FormBase } from "./form"
|
|
2
|
+
import { DefaultTheme } from "./themes/default"
|
|
3
|
+
|
|
4
|
+
export * from './exports'
|
|
5
|
+
|
|
6
|
+
export class ShaclForm extends FormBase {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(new DefaultTheme())
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
window.customElements.define('shacl-form', ShaclForm)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ShaclForm as FormBase } from "./form"
|
|
2
|
+
import { MaterialTheme } from "./themes/material"
|
|
3
|
+
|
|
4
|
+
export * from './exports'
|
|
5
|
+
|
|
6
|
+
export class ShaclForm extends FormBase {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(new MaterialTheme())
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
window.customElements.define('shacl-form', ShaclForm)
|
package/src/form.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { ShaclNode } from './node'
|
|
2
|
+
import { Config } from './config'
|
|
3
|
+
import { ClassInstanceProvider, Plugin, listPlugins, registerPlugin } from './plugin'
|
|
4
|
+
import { Store, NamedNode, DataFactory } from 'n3'
|
|
5
|
+
import { DCTERMS_PREDICATE_CONFORMS_TO, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHACL_PREDICATE_TARGET_CLASS } from './constants'
|
|
6
|
+
import { Editor, Theme } from './theme'
|
|
7
|
+
import { serialize } from './serialize'
|
|
8
|
+
import { Validator } from 'shacl-engine'
|
|
9
|
+
import { setSharedShapesGraph } from './loader'
|
|
10
|
+
|
|
11
|
+
export class ShaclForm extends HTMLElement {
|
|
12
|
+
static get observedAttributes() { return Config.dataAttributes() }
|
|
13
|
+
|
|
14
|
+
config: Config
|
|
15
|
+
shape: ShaclNode | null = null
|
|
16
|
+
form: HTMLFormElement
|
|
17
|
+
initDebounceTimeout: ReturnType<typeof setTimeout> | undefined
|
|
18
|
+
|
|
19
|
+
constructor(theme: Theme) {
|
|
20
|
+
super()
|
|
21
|
+
|
|
22
|
+
this.attachShadow({ mode: 'open' })
|
|
23
|
+
this.form = document.createElement('form')
|
|
24
|
+
this.config = new Config(theme, this.form)
|
|
25
|
+
this.form.addEventListener('change', ev => {
|
|
26
|
+
ev.stopPropagation()
|
|
27
|
+
if (this.config.editMode) {
|
|
28
|
+
this.validate(true).then(valid => {
|
|
29
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true, cancelable: false, composed: true, detail: { 'valid': valid } }))
|
|
30
|
+
}).catch(e => { console.warn(e) })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
this.shadowRoot!.prepend(this.form)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
attributeChangedCallback() {
|
|
40
|
+
this.config.updateAttributes(this)
|
|
41
|
+
this.initialize()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private initialize() {
|
|
45
|
+
clearTimeout(this.initDebounceTimeout)
|
|
46
|
+
this.initDebounceTimeout = setTimeout(async () => {
|
|
47
|
+
// remove all child elements from form and show loading indicator
|
|
48
|
+
this.form.replaceChildren(document.createTextNode(this.config.attributes.loading))
|
|
49
|
+
try {
|
|
50
|
+
await this.config.loader.loadGraphs()
|
|
51
|
+
// remove loading indicator
|
|
52
|
+
this.form.replaceChildren()
|
|
53
|
+
// reset rendered node references
|
|
54
|
+
this.config.renderedNodes.clear()
|
|
55
|
+
// find root shacl shape
|
|
56
|
+
const rootShapeShaclSubject = this.findRootShaclShapeSubject()
|
|
57
|
+
if (rootShapeShaclSubject) {
|
|
58
|
+
// remove all previous css classes to have a defined state
|
|
59
|
+
this.form.classList.forEach(value => { this.form.classList.remove(value) })
|
|
60
|
+
this.form.classList.toggle('mode-edit', this.config.editMode)
|
|
61
|
+
this.form.classList.toggle('mode-view', !this.config.editMode)
|
|
62
|
+
// let theme add classes to form element
|
|
63
|
+
this.config.theme.apply(this.form)
|
|
64
|
+
// adopt stylesheets from theme and plugins
|
|
65
|
+
const styles: CSSStyleSheet[] = [ this.config.theme.stylesheet ]
|
|
66
|
+
for (const plugin of listPlugins()) {
|
|
67
|
+
if (plugin.stylesheet) {
|
|
68
|
+
styles.push(plugin.stylesheet)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.shadowRoot!.adoptedStyleSheets = styles
|
|
72
|
+
|
|
73
|
+
this.shape = new ShaclNode(rootShapeShaclSubject, this.config, this.config.attributes.valuesSubject ? DataFactory.namedNode(this.config.attributes.valuesSubject) : undefined)
|
|
74
|
+
this.form.appendChild(this.shape)
|
|
75
|
+
|
|
76
|
+
if (this.config.editMode) {
|
|
77
|
+
// add submit button
|
|
78
|
+
if (this.config.attributes.submitButton !== null) {
|
|
79
|
+
const button = this.config.theme.createButton(this.config.attributes.submitButton || 'Submit', true)
|
|
80
|
+
button.addEventListener('click', (event) => {
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
// let browser check form validity first
|
|
83
|
+
if (this.form.reportValidity()) {
|
|
84
|
+
// now validate data graph
|
|
85
|
+
this.validate().then(valid => {
|
|
86
|
+
if (valid) {
|
|
87
|
+
// form and data graph are valid, so fire submit event
|
|
88
|
+
this.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
89
|
+
} else {
|
|
90
|
+
// focus first invalid element
|
|
91
|
+
(this.form.querySelector(':scope .invalid > .editor') as HTMLElement)?.focus()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
this.form.appendChild(button)
|
|
97
|
+
}
|
|
98
|
+
await this.validate(true)
|
|
99
|
+
}
|
|
100
|
+
} else if (this.config.shapesGraph.size > 0) {
|
|
101
|
+
// raise error only when shapes graph is not empty
|
|
102
|
+
throw new Error('shacl root node shape not found')
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(e)
|
|
106
|
+
const errorDisplay = document.createElement('div')
|
|
107
|
+
errorDisplay.innerText = String(e)
|
|
108
|
+
this.form.replaceChildren(errorDisplay)
|
|
109
|
+
}
|
|
110
|
+
}, 200)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public serialize(format = 'text/turtle', graph = this.toRDF()): string {
|
|
114
|
+
const quads = graph.getQuads(null, null, null, null)
|
|
115
|
+
return serialize(quads, format, this.config.prefixes)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public toRDF(graph = new Store()): Store {
|
|
119
|
+
this.shape?.toRDF(graph)
|
|
120
|
+
return graph
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public registerPlugin(plugin: Plugin) {
|
|
124
|
+
registerPlugin(plugin)
|
|
125
|
+
this.initialize()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public setTheme(theme: Theme) {
|
|
129
|
+
this.config.theme = theme
|
|
130
|
+
this.initialize()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public setSharedShapesGraph(graph: Store) {
|
|
134
|
+
setSharedShapesGraph(graph)
|
|
135
|
+
this.initialize()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public setClassInstanceProvider(provider: ClassInstanceProvider) {
|
|
139
|
+
this.config.classInstanceProvider = provider
|
|
140
|
+
this.initialize()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async validate(ignoreEmptyValues = false): Promise<boolean> {
|
|
144
|
+
for (const elem of this.form.querySelectorAll(':scope .validation-error')) {
|
|
145
|
+
elem.remove()
|
|
146
|
+
}
|
|
147
|
+
for (const elem of this.form.querySelectorAll(':scope .property-instance')) {
|
|
148
|
+
elem.classList.remove('invalid')
|
|
149
|
+
if (((elem.querySelector(':scope > .editor')) as Editor)?.value) {
|
|
150
|
+
elem.classList.add('valid')
|
|
151
|
+
} else {
|
|
152
|
+
elem.classList.remove('valid')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.config.shapesGraph.deleteGraph('')
|
|
157
|
+
this.shape?.toRDF(this.config.shapesGraph)
|
|
158
|
+
try {
|
|
159
|
+
const dataset = this.config.shapesGraph
|
|
160
|
+
const report = await new Validator(dataset, { details: true, factory: DataFactory }).validate({ dataset })
|
|
161
|
+
|
|
162
|
+
for (const result of report.results) {
|
|
163
|
+
if (result.focusNode?.ptrs?.length) {
|
|
164
|
+
for (const ptr of result.focusNode.ptrs) {
|
|
165
|
+
const focusNode = ptr._term
|
|
166
|
+
// result.path can be empty, e.g. if a focus node does not contain a required property node
|
|
167
|
+
if (result.path?.length) {
|
|
168
|
+
const path = result.path[0].predicates[0]
|
|
169
|
+
// try to find most specific editor elements first
|
|
170
|
+
let invalidElements = this.form.querySelectorAll(`:scope [data-node-id='${focusNode.id}'] [data-path='${path.id}'] > .editor`)
|
|
171
|
+
if (invalidElements.length === 0) {
|
|
172
|
+
// if no editors found, select respective node. this will be the case for node shape violations.
|
|
173
|
+
invalidElements = this.form.querySelectorAll(`:scope [data-node-id='${focusNode.id}'] [data-path='${path.id}']`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const invalidElement of invalidElements) {
|
|
177
|
+
if (invalidElement.classList.contains('editor')) {
|
|
178
|
+
// this is a property shape violation
|
|
179
|
+
if (!ignoreEmptyValues || (invalidElement as Editor).value) {
|
|
180
|
+
let parent: HTMLElement | null = invalidElement.parentElement!
|
|
181
|
+
parent.classList.add('invalid')
|
|
182
|
+
parent.classList.remove('valid')
|
|
183
|
+
parent.appendChild(this.createValidationErrorDisplay(result))
|
|
184
|
+
do {
|
|
185
|
+
if (parent.classList.contains('collapsible')) {
|
|
186
|
+
parent.classList.add('open')
|
|
187
|
+
}
|
|
188
|
+
parent = parent.parentElement
|
|
189
|
+
} while (parent)
|
|
190
|
+
}
|
|
191
|
+
} else if (!ignoreEmptyValues) {
|
|
192
|
+
// this is a node shape violation
|
|
193
|
+
invalidElement.classList.add('invalid')
|
|
194
|
+
invalidElement.classList.remove('valid')
|
|
195
|
+
invalidElement.appendChild(this.createValidationErrorDisplay(result, 'node'))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (!ignoreEmptyValues) {
|
|
199
|
+
this.form.querySelector(`:scope [data-node-id='${focusNode.id}']`)?.prepend(this.createValidationErrorDisplay(result, 'node'))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return report.conforms
|
|
205
|
+
} catch(e) {
|
|
206
|
+
console.error(e)
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private createValidationErrorDisplay(validatonResult?: any, clazz?: string): HTMLElement {
|
|
212
|
+
const messageElement = document.createElement('span')
|
|
213
|
+
messageElement.classList.add('validation-error')
|
|
214
|
+
if (clazz) {
|
|
215
|
+
messageElement.classList.add(clazz)
|
|
216
|
+
}
|
|
217
|
+
if (validatonResult) {
|
|
218
|
+
if (validatonResult.message?.length > 0) {
|
|
219
|
+
for (const message of validatonResult.message) {
|
|
220
|
+
messageElement.title += message.value + '\n'
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
messageElement.title = validatonResult.sourceConstraintComponent?.value
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return messageElement
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private findRootShaclShapeSubject(): NamedNode | undefined {
|
|
230
|
+
let rootShapeShaclSubject: NamedNode | null = null
|
|
231
|
+
// if data-shape-subject is set, use that
|
|
232
|
+
if (this.config.attributes.shapeSubject) {
|
|
233
|
+
rootShapeShaclSubject = DataFactory.namedNode(this.config.attributes.shapeSubject)
|
|
234
|
+
if (this.config.shapesGraph.getQuads(rootShapeShaclSubject, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length === 0) {
|
|
235
|
+
console.warn(`shapes graph does not contain requested root shape ${this.config.attributes.shapeSubject}`)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// if we have a data graph and data-values-subject is set, use shape of that
|
|
241
|
+
if (this.config.attributes.valuesSubject && this.config.dataGraph.size > 0) {
|
|
242
|
+
const rootValueSubject = DataFactory.namedNode(this.config.attributes.valuesSubject)
|
|
243
|
+
const rootValueSubjectTypes = [
|
|
244
|
+
...this.config.dataGraph.getQuads(rootValueSubject, RDF_PREDICATE_TYPE, null, null),
|
|
245
|
+
...this.config.dataGraph.getQuads(rootValueSubject, DCTERMS_PREDICATE_CONFORMS_TO, null, null)
|
|
246
|
+
]
|
|
247
|
+
if (rootValueSubjectTypes.length === 0) {
|
|
248
|
+
console.warn(`value subject '${this.config.attributes.valuesSubject}' has neither ${RDF_PREDICATE_TYPE.id} nor ${DCTERMS_PREDICATE_CONFORMS_TO.id} statement`)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
// if type/conformsTo refers to a node shape, prioritize that over targetClass resolution
|
|
252
|
+
for (const rootValueSubjectType of rootValueSubjectTypes) {
|
|
253
|
+
if (this.config.shapesGraph.getQuads(rootValueSubjectType.object as NamedNode, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length > 0) {
|
|
254
|
+
rootShapeShaclSubject = rootValueSubjectType.object as NamedNode
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!rootShapeShaclSubject) {
|
|
259
|
+
const rootShapes = this.config.shapesGraph.getQuads(null, SHACL_PREDICATE_TARGET_CLASS, rootValueSubjectTypes[0].object, null)
|
|
260
|
+
if (rootShapes.length === 0) {
|
|
261
|
+
console.error(`value subject '${this.config.attributes.valuesSubject}' has no shacl shape definition in the shapes graph`)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (rootShapes.length > 1) {
|
|
265
|
+
console.warn(`value subject '${this.config.attributes.valuesSubject}' has multiple shacl shape definitions in the shapes graph, choosing the first found (${rootShapes[0].subject})`)
|
|
266
|
+
}
|
|
267
|
+
if (this.config.shapesGraph.getQuads(rootShapes[0].subject, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null).length === 0) {
|
|
268
|
+
console.error(`value subject '${this.config.attributes.valuesSubject}' references a shape which is not a NodeShape (${rootShapes[0].subject})`)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
rootShapeShaclSubject = rootShapes[0].subject as NamedNode
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// choose first of all defined root shapes
|
|
276
|
+
const rootShapes = this.config.shapesGraph.getQuads(null, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, null)
|
|
277
|
+
if (rootShapes.length == 0) {
|
|
278
|
+
console.warn('shapes graph does not contain any root shapes')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (rootShapes.length > 1) {
|
|
282
|
+
console.warn('shapes graph contains', rootShapes.length, 'root shapes. choosing first found which is', rootShapes[0].subject.value)
|
|
283
|
+
console.info('hint: set the shape to use with attribute "data-shape-subject"')
|
|
284
|
+
}
|
|
285
|
+
rootShapeShaclSubject = rootShapes[0].subject as NamedNode
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return rootShapeShaclSubject
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/globals.d.ts
ADDED