@ulb-darmstadt/shacl-form 1.6.2 → 1.6.4
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/dist/util.d.ts +2 -1
- 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 +14 -0
- package/src/themes/material.ts +240 -0
- package/src/util.ts +134 -0
package/src/group.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PREFIX_RDFS } from './constants'
|
|
2
|
+
import { Config } from './config'
|
|
3
|
+
import { findObjectValueByPredicate } from './util'
|
|
4
|
+
|
|
5
|
+
export function createShaclGroup(groupSubject: string, config: Config): HTMLElement {
|
|
6
|
+
const group = document.createElement('div')
|
|
7
|
+
group.dataset['subject'] = groupSubject
|
|
8
|
+
group.classList.add('shacl-group')
|
|
9
|
+
let name = groupSubject
|
|
10
|
+
const quads = config.shapesGraph.getQuads(groupSubject, null, null, null)
|
|
11
|
+
const label = findObjectValueByPredicate(quads, "label", PREFIX_RDFS, config.languages)
|
|
12
|
+
if (label) {
|
|
13
|
+
name = label
|
|
14
|
+
}
|
|
15
|
+
const order = findObjectValueByPredicate(quads, "order")
|
|
16
|
+
if (order) {
|
|
17
|
+
group.style.order = order
|
|
18
|
+
}
|
|
19
|
+
const header = document.createElement('h1')
|
|
20
|
+
header.innerText = name
|
|
21
|
+
group.appendChild(header)
|
|
22
|
+
|
|
23
|
+
if (config.attributes.collapse !== null) {
|
|
24
|
+
group.classList.add('collapsible')
|
|
25
|
+
if (config.attributes.collapse === 'open') {
|
|
26
|
+
group.classList.add('open')
|
|
27
|
+
}
|
|
28
|
+
header.classList.add('activator')
|
|
29
|
+
header.addEventListener('click', () => {
|
|
30
|
+
group.classList.toggle('open')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
return group
|
|
35
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Store, Parser, Quad, Prefixes, NamedNode, DataFactory } from 'n3'
|
|
2
|
+
import { toRDF } from 'jsonld'
|
|
3
|
+
import { DCTERMS_PREDICATE_CONFORMS_TO, OWL_PREDICATE_IMPORTS, RDF_PREDICATE_TYPE, SHACL_PREDICATE_CLASS, SHAPES_GRAPH } from './constants'
|
|
4
|
+
import { Config } from './config'
|
|
5
|
+
import { isURL } from './util'
|
|
6
|
+
|
|
7
|
+
// cache external data in module scope (and not in Loader instance) to avoid requesting
|
|
8
|
+
// them multiple times, e.g. when more than one shacl-form element is on the page
|
|
9
|
+
// that import the same resources
|
|
10
|
+
const loadedURLCache: Record<string, Promise<string>> = {}
|
|
11
|
+
const loadedClassesCache: Record<string, Promise<string>> = {}
|
|
12
|
+
let sharedShapesGraph: Store | undefined
|
|
13
|
+
|
|
14
|
+
export class Loader {
|
|
15
|
+
private config: Config
|
|
16
|
+
private loadedExternalUrls: string[] = []
|
|
17
|
+
private loadedClasses: string[] = []
|
|
18
|
+
|
|
19
|
+
constructor(config: Config) {
|
|
20
|
+
this.config = config
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loadGraphs() {
|
|
24
|
+
// clear local caches
|
|
25
|
+
this.loadedExternalUrls = []
|
|
26
|
+
this.loadedClasses = []
|
|
27
|
+
|
|
28
|
+
let shapesStore = sharedShapesGraph
|
|
29
|
+
const valuesStore = new Store()
|
|
30
|
+
this.config.prefixes = {}
|
|
31
|
+
|
|
32
|
+
const promises = [ this.importRDF(this.config.attributes.values ? this.config.attributes.values : this.config.attributes.valuesUrl ? this.fetchRDF(this.config.attributes.valuesUrl) : '', valuesStore, undefined, new Parser({ blankNodePrefix: '' })) ]
|
|
33
|
+
if (!shapesStore) {
|
|
34
|
+
shapesStore = new Store()
|
|
35
|
+
promises.push(this.importRDF(this.config.attributes.shapes ? this.config.attributes.shapes : this.config.attributes.shapesUrl ? this.fetchRDF(this.config.attributes.shapesUrl) : '', shapesStore, SHAPES_GRAPH))
|
|
36
|
+
}
|
|
37
|
+
await Promise.all(promises)
|
|
38
|
+
|
|
39
|
+
// if shapes graph is empty, but we have the following triples:
|
|
40
|
+
// <valueSubject> a <uri> or <valueSubject> dcterms:conformsTo <uri>
|
|
41
|
+
// then try to load the referenced object into the shapes graph
|
|
42
|
+
if (!sharedShapesGraph && shapesStore?.size == 0 && this.config.attributes.valuesSubject) {
|
|
43
|
+
const shapeCandidates = [
|
|
44
|
+
...valuesStore.getObjects(this.config.attributes.valuesSubject, RDF_PREDICATE_TYPE, null),
|
|
45
|
+
...valuesStore.getObjects(this.config.attributes.valuesSubject, DCTERMS_PREDICATE_CONFORMS_TO, null)
|
|
46
|
+
]
|
|
47
|
+
const promises: Promise<void>[] = []
|
|
48
|
+
for (const uri of shapeCandidates) {
|
|
49
|
+
const url = this.toURL(uri.value)
|
|
50
|
+
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
51
|
+
this.loadedExternalUrls.push(url)
|
|
52
|
+
promises.push(this.importRDF(this.fetchRDF(url), shapesStore, SHAPES_GRAPH))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await Promise.allSettled(promises)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn(e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.config.shapesGraph = shapesStore
|
|
63
|
+
this.config.dataGraph = valuesStore
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async importRDF(input: string | Promise<string>, store: Store, graph?: NamedNode, parser?: Parser) {
|
|
67
|
+
const p = parser || new Parser()
|
|
68
|
+
const parse = async (text: string) => {
|
|
69
|
+
const dependencies: Promise<void>[] = []
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
p.parse(text, (error: Error, quad: Quad, prefixes: Prefixes) => {
|
|
72
|
+
if (error) {
|
|
73
|
+
return reject(error)
|
|
74
|
+
}
|
|
75
|
+
if (quad) {
|
|
76
|
+
store.add(new Quad(quad.subject, quad.predicate, quad.object, graph))
|
|
77
|
+
// check if this is an owl:imports predicate and try to load the url
|
|
78
|
+
if (this.config.attributes.ignoreOwlImports === null && OWL_PREDICATE_IMPORTS.equals(quad.predicate)) {
|
|
79
|
+
const url = this.toURL(quad.object.value)
|
|
80
|
+
// import url only once
|
|
81
|
+
if (url && this.loadedExternalUrls.indexOf(url) < 0) {
|
|
82
|
+
this.loadedExternalUrls.push(url)
|
|
83
|
+
// import into separate graph
|
|
84
|
+
dependencies.push(this.importRDF(this.fetchRDF(url), store, DataFactory.namedNode(url), parser))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// check if this is an sh:class predicate and invoke class instance provider
|
|
88
|
+
if (this.config.classInstanceProvider && SHACL_PREDICATE_CLASS.equals(quad.predicate)) {
|
|
89
|
+
const className = quad.object.value
|
|
90
|
+
// import class definitions only once
|
|
91
|
+
if (this.loadedClasses.indexOf(className) < 0) {
|
|
92
|
+
let promise: Promise<string>
|
|
93
|
+
// check if class is in module scope cache
|
|
94
|
+
if (className in loadedClassesCache) {
|
|
95
|
+
promise = loadedClassesCache[className]
|
|
96
|
+
} else {
|
|
97
|
+
promise = this.config.classInstanceProvider(className)
|
|
98
|
+
loadedClassesCache[className] = promise
|
|
99
|
+
}
|
|
100
|
+
this.loadedClasses.push(className)
|
|
101
|
+
dependencies.push(this.importRDF(promise, store, graph, parser))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (prefixes) {
|
|
107
|
+
this.config.registerPrefixes(prefixes)
|
|
108
|
+
}
|
|
109
|
+
resolve(null)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
try {
|
|
113
|
+
await Promise.allSettled(dependencies)
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.warn(e)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (input instanceof Promise) {
|
|
120
|
+
input = await input
|
|
121
|
+
}
|
|
122
|
+
if (input) {
|
|
123
|
+
try {
|
|
124
|
+
// check if input is JSON
|
|
125
|
+
// @ts-ignore, because result of toRDF is a string and not an object
|
|
126
|
+
input = await toRDF(JSON.parse(input), { format: 'application/n-quads' }) as string
|
|
127
|
+
} catch(_) {
|
|
128
|
+
// NOP, it wasn't JSON
|
|
129
|
+
}
|
|
130
|
+
await parse(input)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async fetchRDF(url: string): Promise<string> {
|
|
135
|
+
// try to load from cache first
|
|
136
|
+
if (url in loadedURLCache) {
|
|
137
|
+
return loadedURLCache[url]
|
|
138
|
+
}
|
|
139
|
+
const promise = fetch(url, {
|
|
140
|
+
headers: {
|
|
141
|
+
'Accept': 'text/turtle, application/trig, application/n-triples, application/n-quads, text/n3, application/ld+json'
|
|
142
|
+
},
|
|
143
|
+
}).then(resp => resp.text())
|
|
144
|
+
loadedURLCache[url] = promise
|
|
145
|
+
return promise
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
toURL(id: string): string | null {
|
|
149
|
+
if (isURL(id)) {
|
|
150
|
+
return id
|
|
151
|
+
}
|
|
152
|
+
if (this.config.prefixes) {
|
|
153
|
+
const splitted = id.split(':')
|
|
154
|
+
if (splitted.length === 2) {
|
|
155
|
+
const prefix = this.config.prefixes[splitted[0]]
|
|
156
|
+
if (prefix) {
|
|
157
|
+
// need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings)
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
id = id.replace(`${splitted[0]}:`, prefix)
|
|
160
|
+
if (isURL(id)) {
|
|
161
|
+
return id
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function setSharedShapesGraph(graph: Store) {
|
|
171
|
+
sharedShapesGraph = graph
|
|
172
|
+
}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, OWL_PREDICATE_IMPORTS } from './constants'
|
|
4
|
+
import { ShaclProperty } from './property'
|
|
5
|
+
import { createShaclGroup } from './group'
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
7
|
+
import { createShaclOrConstraint } from './constraints'
|
|
8
|
+
import { Config } from './config'
|
|
9
|
+
|
|
10
|
+
export class ShaclNode extends HTMLElement {
|
|
11
|
+
parent: ShaclNode | undefined
|
|
12
|
+
shaclSubject: NamedNode
|
|
13
|
+
nodeId: NamedNode | BlankNode
|
|
14
|
+
targetClass: NamedNode | undefined
|
|
15
|
+
owlImports: NamedNode[] = []
|
|
16
|
+
config: Config
|
|
17
|
+
|
|
18
|
+
constructor(shaclSubject: NamedNode, config: Config, valueSubject: NamedNode | BlankNode | undefined, parent?: ShaclNode, nodeKind?: NamedNode, label?: string) {
|
|
19
|
+
super()
|
|
20
|
+
|
|
21
|
+
this.parent = parent
|
|
22
|
+
this.config = config
|
|
23
|
+
this.shaclSubject = shaclSubject
|
|
24
|
+
let nodeId: NamedNode | BlankNode | undefined = valueSubject
|
|
25
|
+
if (!nodeId) {
|
|
26
|
+
// if no value subject given, create new node id with a type depending on own nodeKind or given parent property nodeKind
|
|
27
|
+
if (!nodeKind) {
|
|
28
|
+
const spec = config.shapesGraph.getObjects(shaclSubject, `${PREFIX_SHACL}nodeKind`, null)
|
|
29
|
+
if (spec.length) {
|
|
30
|
+
nodeKind = spec[0] as NamedNode
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 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`) {
|
|
35
|
+
// no requirements on node type, so create a NamedNode and use configured value namespace
|
|
36
|
+
nodeId = DataFactory.namedNode(config.attributes.valuesNamespace + uuidv4())
|
|
37
|
+
} else {
|
|
38
|
+
// otherwise create a BlankNode
|
|
39
|
+
nodeId = DataFactory.blankNode(uuidv4())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.nodeId = nodeId
|
|
43
|
+
|
|
44
|
+
// check if the form already contains the node/value pair to prevent recursion
|
|
45
|
+
const id = JSON.stringify([shaclSubject, valueSubject])
|
|
46
|
+
if (valueSubject && config.renderedNodes.has(id)) {
|
|
47
|
+
// 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
|
+
}
|
|
53
|
+
const anchor = document.createElement('a')
|
|
54
|
+
anchor.innerText = valueSubject.id
|
|
55
|
+
anchor.classList.add('ref-link')
|
|
56
|
+
anchor.onclick = () => {
|
|
57
|
+
// if anchor is clicked, scroll referenced shacl node into view
|
|
58
|
+
this.config.form.querySelector(`shacl-node[data-node-id='${this.nodeId.id}']`)?.scrollIntoView()
|
|
59
|
+
}
|
|
60
|
+
this.appendChild(anchor)
|
|
61
|
+
this.style.flexDirection = 'row'
|
|
62
|
+
} else {
|
|
63
|
+
if (valueSubject) {
|
|
64
|
+
config.renderedNodes.add(id)
|
|
65
|
+
}
|
|
66
|
+
this.dataset.nodeId = this.nodeId.id
|
|
67
|
+
const quads = config.shapesGraph.getQuads(shaclSubject, null, null, null)
|
|
68
|
+
let list: Term[] | undefined
|
|
69
|
+
|
|
70
|
+
if (this.config.attributes.showNodeIds !== null) {
|
|
71
|
+
const div = document.createElement('div')
|
|
72
|
+
div.innerText = `id: ${this.nodeId.id}`
|
|
73
|
+
div.classList.add('node-id-display')
|
|
74
|
+
this.appendChild(div)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const quad of quads) {
|
|
78
|
+
switch (quad.predicate.id) {
|
|
79
|
+
case `${PREFIX_SHACL}property`:
|
|
80
|
+
let parentElement: HTMLElement = this
|
|
81
|
+
// check if property belongs to a group
|
|
82
|
+
const groupRef = config.shapesGraph.getQuads(quad.object as Term, `${PREFIX_SHACL}group`, null, null)
|
|
83
|
+
if (groupRef.length > 0) {
|
|
84
|
+
const groupSubject = groupRef[0].object.value
|
|
85
|
+
if (config.groups.indexOf(groupSubject) > -1) {
|
|
86
|
+
// check if group element already exists, otherwise create it
|
|
87
|
+
let group = this.querySelector(`:scope > .shacl-group[data-subject='${groupSubject}']`) as HTMLElement
|
|
88
|
+
if (!group) {
|
|
89
|
+
group = createShaclGroup(groupSubject, config)
|
|
90
|
+
this.appendChild(group)
|
|
91
|
+
}
|
|
92
|
+
parentElement = group
|
|
93
|
+
} else {
|
|
94
|
+
console.warn('ignoring unknown group reference', groupRef[0], 'existing groups:', config.groups)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// delay creating/appending the property until we finished parsing the node.
|
|
98
|
+
// This is needed to have possible owlImports parsed before creating the property.
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const property = new ShaclProperty(quad.object as NamedNode | BlankNode, this, config, valueSubject)
|
|
101
|
+
// 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.
|
|
102
|
+
if (property.childElementCount > 0) {
|
|
103
|
+
parentElement.appendChild(property)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
break;
|
|
107
|
+
case `${PREFIX_SHACL}and`:
|
|
108
|
+
// inheritance via sh:and
|
|
109
|
+
list = config.lists[quad.object.value]
|
|
110
|
+
if (list?.length) {
|
|
111
|
+
for (const shape of list) {
|
|
112
|
+
this.prepend(new ShaclNode(shape as NamedNode, config, valueSubject, this))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case `${PREFIX_SHACL}node`:
|
|
120
|
+
// inheritance via sh:node
|
|
121
|
+
this.prepend(new ShaclNode(quad.object as NamedNode, config, valueSubject, this))
|
|
122
|
+
break;
|
|
123
|
+
case `${PREFIX_SHACL}targetClass`:
|
|
124
|
+
this.targetClass = quad.object as NamedNode
|
|
125
|
+
break;
|
|
126
|
+
case OWL_PREDICATE_IMPORTS.id:
|
|
127
|
+
this.owlImports.push(quad.object as NamedNode)
|
|
128
|
+
break;
|
|
129
|
+
case `${PREFIX_SHACL}or`:
|
|
130
|
+
list = config.lists[quad.object.value]
|
|
131
|
+
if (list?.length) {
|
|
132
|
+
this.appendChild(createShaclOrConstraint(list, this, config))
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.error('list not found:', quad.object.value, 'existing lists:', config.lists)
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (label) {
|
|
142
|
+
const header = document.createElement('h1')
|
|
143
|
+
header.innerText = label
|
|
144
|
+
this.prepend(header)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
toRDF(graph: Store, subject?: NamedNode | BlankNode): (NamedNode | BlankNode) {
|
|
150
|
+
if (!subject) {
|
|
151
|
+
subject = this.nodeId
|
|
152
|
+
}
|
|
153
|
+
for (const shape of this.querySelectorAll(':scope > shacl-node, :scope > .shacl-group > shacl-node, :scope > shacl-property, :scope > .shacl-group > shacl-property')) {
|
|
154
|
+
(shape as ShaclNode | ShaclProperty).toRDF(graph, subject)
|
|
155
|
+
}
|
|
156
|
+
if (this.targetClass) {
|
|
157
|
+
graph.addQuad(subject, RDF_PREDICATE_TYPE, this.targetClass)
|
|
158
|
+
}
|
|
159
|
+
// if this is the root shacl node, check if we should add one of the rdf:type or dcterms:conformsTo predicates
|
|
160
|
+
if (this.config.attributes.generateNodeShapeReference && !this.parent) {
|
|
161
|
+
graph.addQuad(subject, DataFactory.namedNode(this.config.attributes.generateNodeShapeReference), this.shaclSubject)
|
|
162
|
+
}
|
|
163
|
+
return subject
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
window.customElements.define('shacl-node', ShaclNode)
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ShaclPropertyTemplate } from './property-template'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
|
|
4
|
+
// store plugins in module scope so that they apply to all shacl-form elements
|
|
5
|
+
const plugins: Record<string, Plugin> = {}
|
|
6
|
+
|
|
7
|
+
export function registerPlugin(plugin: Plugin) {
|
|
8
|
+
if (plugin.predicate === undefined && plugin.datatype === undefined) {
|
|
9
|
+
console.warn('not registering plugin because it does neither define "predicate" nor "datatype"', plugin)
|
|
10
|
+
} else {
|
|
11
|
+
plugins[`${plugin.predicate}^${plugin.datatype}`] = plugin
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function listPlugins(): Plugin[] {
|
|
16
|
+
return Object.entries(plugins).map((value: [_: string, plugin: Plugin]) => { return value[1] })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function findPlugin(predicate: string | undefined, datatype: string | undefined): Plugin | undefined {
|
|
20
|
+
// first try to find plugin with matching predicate and datatype
|
|
21
|
+
let plugin = plugins[`${predicate}^${datatype}`]
|
|
22
|
+
if (plugin) {
|
|
23
|
+
return plugin
|
|
24
|
+
}
|
|
25
|
+
// now prefer predicate over datatype
|
|
26
|
+
plugin = plugins[`${predicate}^${undefined}`]
|
|
27
|
+
if (plugin) {
|
|
28
|
+
return plugin
|
|
29
|
+
}
|
|
30
|
+
// last, try to find plugin with matching datatype
|
|
31
|
+
return plugins[`${undefined}^${datatype}`]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PluginOptions = {
|
|
35
|
+
predicate?: string
|
|
36
|
+
datatype?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export abstract class Plugin {
|
|
40
|
+
predicate: string | undefined
|
|
41
|
+
datatype: string | undefined
|
|
42
|
+
stylesheet: CSSStyleSheet | undefined
|
|
43
|
+
|
|
44
|
+
constructor(options: PluginOptions, css?: string) {
|
|
45
|
+
this.predicate = options.predicate
|
|
46
|
+
this.datatype = options.datatype
|
|
47
|
+
if (css) {
|
|
48
|
+
this.stylesheet = new CSSStyleSheet()
|
|
49
|
+
this.stylesheet.replaceSync(css)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
abstract createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
|
|
54
|
+
|
|
55
|
+
createViewer(template: ShaclPropertyTemplate, value: Term): HTMLElement {
|
|
56
|
+
return template.config.theme.createViewer(template.label, value, template)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ClassInstanceProvider = (clazz: string) => Promise<string>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Plugin, PluginOptions } from '../plugin'
|
|
2
|
+
import { ShaclPropertyTemplate } from '../property-template'
|
|
3
|
+
|
|
4
|
+
export class FileUploadPlugin extends Plugin {
|
|
5
|
+
onChange: (event: Event) => void
|
|
6
|
+
fileType: string | undefined
|
|
7
|
+
|
|
8
|
+
constructor(options: PluginOptions, onChange: (event: Event) => void, fileType?: string) {
|
|
9
|
+
super(options)
|
|
10
|
+
this.onChange = onChange
|
|
11
|
+
this.fileType = fileType
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createEditor(template: ShaclPropertyTemplate): HTMLElement {
|
|
15
|
+
const required = template.minCount !== undefined && template.minCount > 0
|
|
16
|
+
const editor = template.config.theme.createFileEditor(template.label, null, required, template)
|
|
17
|
+
editor.addEventListener('change', event => {
|
|
18
|
+
event.stopPropagation()
|
|
19
|
+
this.onChange(event)
|
|
20
|
+
})
|
|
21
|
+
if (this.fileType) {
|
|
22
|
+
editor.querySelector('input[type="file"]')?.setAttribute('accept', this.fileType)
|
|
23
|
+
}
|
|
24
|
+
return editor
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Plugin, PluginOptions } from '../plugin'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
|
|
4
|
+
import { ShaclPropertyTemplate } from '../property-template'
|
|
5
|
+
import { InputListEntry } from '../theme'
|
|
6
|
+
|
|
7
|
+
export class FixedListPlugin extends Plugin {
|
|
8
|
+
entries: InputListEntry[]
|
|
9
|
+
|
|
10
|
+
constructor(options: PluginOptions, entries: InputListEntry[]) {
|
|
11
|
+
super(options)
|
|
12
|
+
this.entries = entries
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
|
|
16
|
+
const required = template.minCount !== undefined && template.minCount > 0
|
|
17
|
+
return template.config.theme.createListEditor(template.label, value || null, required, this.entries, template)
|
|
18
|
+
}
|
|
19
|
+
}
|