@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/property.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { BlankNode, DataFactory, NamedNode, Store } from 'n3'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { ShaclNode } from './node'
|
|
4
|
+
import { focusFirstInputElement } from './util'
|
|
5
|
+
import { createShaclOrConstraint, resolveShaclOrConstraint } from './constraints'
|
|
6
|
+
import { Config } from './config'
|
|
7
|
+
import { ShaclPropertyTemplate } from './property-template'
|
|
8
|
+
import { Editor, fieldFactory } from './theme'
|
|
9
|
+
import { toRDF } from './serialize'
|
|
10
|
+
import { findPlugin } from './plugin'
|
|
11
|
+
|
|
12
|
+
export class ShaclProperty extends HTMLElement {
|
|
13
|
+
template: ShaclPropertyTemplate
|
|
14
|
+
addButton: HTMLElement | undefined
|
|
15
|
+
|
|
16
|
+
constructor(shaclSubject: BlankNode | NamedNode, parent: ShaclNode, config: Config, valueSubject?: NamedNode | BlankNode) {
|
|
17
|
+
super()
|
|
18
|
+
this.template = new ShaclPropertyTemplate(config.shapesGraph.getQuads(shaclSubject, null, null, null), parent, config)
|
|
19
|
+
|
|
20
|
+
if (this.template.order !== undefined) {
|
|
21
|
+
this.style.order = `${this.template.order}`
|
|
22
|
+
}
|
|
23
|
+
if (this.template.cssClass) {
|
|
24
|
+
this.classList.add(this.template.cssClass)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (config.editMode) {
|
|
28
|
+
this.addButton = document.createElement('a')
|
|
29
|
+
this.addButton.innerText = this.template.label
|
|
30
|
+
this.addButton.title = 'Add ' + this.template.label
|
|
31
|
+
this.addButton.classList.add('control-button', 'add-button')
|
|
32
|
+
this.addButton.addEventListener('click', _ => {
|
|
33
|
+
const instance = this.addPropertyInstance()
|
|
34
|
+
instance.classList.add('fadeIn')
|
|
35
|
+
this.updateControls()
|
|
36
|
+
focusFirstInputElement(instance)
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
instance.classList.remove('fadeIn')
|
|
39
|
+
}, 200)
|
|
40
|
+
})
|
|
41
|
+
this.appendChild(this.addButton)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// bind existing values
|
|
45
|
+
if (this.template.path) {
|
|
46
|
+
const values = valueSubject ? config.dataGraph.getQuads(valueSubject, this.template.path, null, null) : []
|
|
47
|
+
let valuesContainHasValue = false
|
|
48
|
+
for (const value of values) {
|
|
49
|
+
this.addPropertyInstance(value.object)
|
|
50
|
+
if (this.template.hasValue && value.object.equals(this.template.hasValue)) {
|
|
51
|
+
valuesContainHasValue = true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (config.editMode && this.template.hasValue && !valuesContainHasValue) {
|
|
55
|
+
// sh:hasValue is defined in shapes graph, but does not exist in data graph, so force it
|
|
56
|
+
this.addPropertyInstance(this.template.hasValue)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.editMode) {
|
|
61
|
+
this.addEventListener('change', () => { this.updateControls() })
|
|
62
|
+
this.updateControls()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.template.extendedShapes?.length && this.template.config.attributes.collapse !== null && (!this.template.maxCount || this.template.maxCount > 1)) {
|
|
66
|
+
// in view mode, show collapsible only when we have something to show
|
|
67
|
+
if (config.editMode || this.childElementCount > 0) {
|
|
68
|
+
const collapsible = this
|
|
69
|
+
collapsible.classList.add('collapsible')
|
|
70
|
+
if (this.template.config.attributes.collapse === 'open') {
|
|
71
|
+
collapsible.classList.add('open')
|
|
72
|
+
}
|
|
73
|
+
const activator = document.createElement('h1')
|
|
74
|
+
activator.classList.add('activator')
|
|
75
|
+
activator.innerText = this.template.label
|
|
76
|
+
activator.addEventListener('click', () => {
|
|
77
|
+
collapsible.classList.toggle('open')
|
|
78
|
+
})
|
|
79
|
+
this.prepend(activator)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addPropertyInstance(value?: Term): HTMLElement {
|
|
85
|
+
let instance: HTMLElement
|
|
86
|
+
if (this.template.shaclOr?.length) {
|
|
87
|
+
if (value) {
|
|
88
|
+
instance = createPropertyInstance(resolveShaclOrConstraint(this.template, value), value, true)
|
|
89
|
+
} else {
|
|
90
|
+
instance = createShaclOrConstraint(this.template.shaclOr, this, this.template.config)
|
|
91
|
+
appendRemoveButton(instance, '')
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
instance = createPropertyInstance(this.template, value)
|
|
95
|
+
}
|
|
96
|
+
if (this.template.config.editMode) {
|
|
97
|
+
this.insertBefore(instance, this.addButton!)
|
|
98
|
+
} else {
|
|
99
|
+
this.appendChild(instance)
|
|
100
|
+
}
|
|
101
|
+
return instance
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updateControls() {
|
|
105
|
+
let instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
106
|
+
if (instanceCount === 0 && (!this.template.extendedShapes?.length || (this.template.minCount !== undefined && this.template.minCount > 0))) {
|
|
107
|
+
this.addPropertyInstance()
|
|
108
|
+
instanceCount = this.querySelectorAll(":scope > .property-instance, :scope > .shacl-or-constraint, :scope > shacl-node").length
|
|
109
|
+
}
|
|
110
|
+
let mayRemove: boolean
|
|
111
|
+
if (this.template.minCount !== undefined) {
|
|
112
|
+
mayRemove = instanceCount > this.template.minCount
|
|
113
|
+
} else {
|
|
114
|
+
mayRemove = (this.template.extendedShapes && this.template.extendedShapes.length > 0) || instanceCount > 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mayAdd = this.template.maxCount === undefined || instanceCount < this.template.maxCount
|
|
118
|
+
this.classList.toggle('may-remove', mayRemove)
|
|
119
|
+
this.classList.toggle('may-add', mayAdd)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
toRDF(graph: Store, subject: NamedNode | BlankNode) {
|
|
123
|
+
for (const instance of this.querySelectorAll(':scope > .property-instance')) {
|
|
124
|
+
const pathNode = DataFactory.namedNode((instance as HTMLElement).dataset.path!)
|
|
125
|
+
if (instance.firstChild instanceof ShaclNode) {
|
|
126
|
+
const quadCount = graph.size
|
|
127
|
+
const shapeSubject = instance.firstChild.toRDF(graph)
|
|
128
|
+
graph.addQuad(subject, pathNode, shapeSubject)
|
|
129
|
+
} else {
|
|
130
|
+
const editor = instance.querySelector('.editor') as Editor
|
|
131
|
+
const value = toRDF(editor)
|
|
132
|
+
if (value) {
|
|
133
|
+
graph.addQuad(subject, pathNode, value)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createPropertyInstance(template: ShaclPropertyTemplate, value?: Term, forceRemovable = false): HTMLElement {
|
|
141
|
+
let instance: HTMLElement
|
|
142
|
+
if (template.extendedShapes?.length) {
|
|
143
|
+
instance = document.createElement('div')
|
|
144
|
+
instance.classList.add('property-instance')
|
|
145
|
+
for (const node of template.extendedShapes) {
|
|
146
|
+
instance.appendChild(new ShaclNode(node, template.config, value as NamedNode | BlankNode | undefined, template.parent, template.nodeKind, template.label))
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
const plugin = findPlugin(template.path, template.datatype?.value)
|
|
150
|
+
if (plugin) {
|
|
151
|
+
if (template.config.editMode) {
|
|
152
|
+
instance = plugin.createEditor(template, value)
|
|
153
|
+
} else {
|
|
154
|
+
instance = plugin.createViewer(template, value!)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
instance = fieldFactory(template, value || null)
|
|
158
|
+
}
|
|
159
|
+
instance.classList.add('property-instance')
|
|
160
|
+
}
|
|
161
|
+
if (template.config.editMode) {
|
|
162
|
+
appendRemoveButton(instance, template.label, forceRemovable)
|
|
163
|
+
}
|
|
164
|
+
instance.dataset.path = template.path
|
|
165
|
+
return instance
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function appendRemoveButton(instance: HTMLElement, label: string, forceRemovable = false) {
|
|
169
|
+
const removeButton = document.createElement('a')
|
|
170
|
+
removeButton.innerText = '\u00d7'
|
|
171
|
+
removeButton.classList.add('control-button', 'btn', 'remove-button')
|
|
172
|
+
removeButton.title = 'Remove ' + label
|
|
173
|
+
removeButton.addEventListener('click', _ => {
|
|
174
|
+
instance.classList.remove('fadeIn')
|
|
175
|
+
instance.classList.add('fadeOut')
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
const parent = instance.parentElement
|
|
178
|
+
instance.remove()
|
|
179
|
+
parent?.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))
|
|
180
|
+
}, 200)
|
|
181
|
+
})
|
|
182
|
+
if (forceRemovable) {
|
|
183
|
+
removeButton.classList.add('persistent')
|
|
184
|
+
}
|
|
185
|
+
instance.appendChild(removeButton)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
window.customElements.define('shacl-property', ShaclProperty)
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DataFactory, NamedNode, Writer, Quad, Literal, Prefixes } from 'n3'
|
|
2
|
+
import { PREFIX_XSD, RDF_PREDICATE_TYPE, PREFIX_SHACL } from './constants'
|
|
3
|
+
import { Editor } from './theme'
|
|
4
|
+
import { NodeObject } from 'jsonld'
|
|
5
|
+
|
|
6
|
+
export function serialize(quads: Quad[], format: string, prefixes?: Prefixes): string {
|
|
7
|
+
if (format === 'application/ld+json') {
|
|
8
|
+
return serializeJsonld(quads)
|
|
9
|
+
} else {
|
|
10
|
+
const writer = new Writer({ format: format, prefixes: prefixes })
|
|
11
|
+
writer.addQuads(quads)
|
|
12
|
+
let serialized = ''
|
|
13
|
+
writer.end((error, result) => {
|
|
14
|
+
if (error) {
|
|
15
|
+
console.error(error)
|
|
16
|
+
}
|
|
17
|
+
serialized = result
|
|
18
|
+
})
|
|
19
|
+
return serialized
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function serializeJsonld(quads: Quad[]): string {
|
|
24
|
+
const triples: NodeObject[] = []
|
|
25
|
+
for (const quad of quads) {
|
|
26
|
+
const triple: NodeObject = { '@id': quad.subject.id }
|
|
27
|
+
|
|
28
|
+
if (quad.predicate === RDF_PREDICATE_TYPE) {
|
|
29
|
+
triple['@type'] = quad.object.id
|
|
30
|
+
} else {
|
|
31
|
+
let object: string | {} = quad.object.value
|
|
32
|
+
if (quad.object instanceof Literal) {
|
|
33
|
+
if (quad.object.language) {
|
|
34
|
+
object = { '@language': quad.object.language, '@value': quad.object.value }
|
|
35
|
+
} else if (quad.object.datatype && quad.object.datatype.value !== `${PREFIX_XSD}#string`) {
|
|
36
|
+
object = { '@type': quad.object.datatype.value, '@value': quad.object.value }
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
object = { '@id': quad.object.id }
|
|
40
|
+
}
|
|
41
|
+
triple[quad.predicate.value] = object
|
|
42
|
+
}
|
|
43
|
+
triples.push(triple)
|
|
44
|
+
}
|
|
45
|
+
return JSON.stringify(triples)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function toRDF(editor: Editor): Literal | NamedNode | undefined {
|
|
49
|
+
let languageOrDatatype: NamedNode<string> | string | undefined = editor['shaclDatatype']
|
|
50
|
+
let value: number | string = editor.value
|
|
51
|
+
if (value) {
|
|
52
|
+
if (editor.dataset.class || editor.dataset.nodeKind === PREFIX_SHACL + 'IRI') {
|
|
53
|
+
return DataFactory.namedNode(value)
|
|
54
|
+
} else {
|
|
55
|
+
if (editor.dataset.lang) {
|
|
56
|
+
languageOrDatatype = editor.dataset.lang
|
|
57
|
+
}
|
|
58
|
+
else if (editor['type'] === 'number') {
|
|
59
|
+
value = parseFloat(value)
|
|
60
|
+
}
|
|
61
|
+
else if (editor['type'] === 'file' && editor['binaryData']) {
|
|
62
|
+
value = editor['binaryData']
|
|
63
|
+
}
|
|
64
|
+
else if (editor['type'] === 'datetime-local') {
|
|
65
|
+
// if seconds in value are 0, the input field omits them which is then not a valid xsd:dateTime
|
|
66
|
+
value = new Date(value).toISOString().slice(0, 19)
|
|
67
|
+
}
|
|
68
|
+
return DataFactory.literal(value, languageOrDatatype)
|
|
69
|
+
}
|
|
70
|
+
} else if (editor['type'] === 'checkbox' || editor.getAttribute('type') === 'checkbox') {
|
|
71
|
+
// emit boolean 'false' only when required
|
|
72
|
+
if (editor['checked'] || parseInt(editor.dataset.minCount || '0') > 0) {
|
|
73
|
+
return DataFactory.literal(editor['checked'] ? 'true' : 'false', languageOrDatatype)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
form { box-sizing: border-box; display:block; --label-width: 8em; --caret-size: 10px; }
|
|
2
|
+
form.mode-edit { padding-left: 1em; }
|
|
3
|
+
form *, form ::after, form ::before { box-sizing: inherit; }
|
|
4
|
+
shacl-node, .shacl-group { display: flex; flex-direction: column; width: 100%; position: relative; }
|
|
5
|
+
shacl-node .control-button { text-decoration: none; cursor: pointer; border: 1px solid transparent; border-radius: 4px; padding: 2px 4px; }
|
|
6
|
+
shacl-node .control-button:hover { border-color: inherit; }
|
|
7
|
+
shacl-node .remove-button { margin-left: 4px; }
|
|
8
|
+
shacl-node .add-button { font-size: 0.8rem; color: #555; margin: 4px 24px 0 0; }
|
|
9
|
+
shacl-node .add-button:before { content: '+'; margin-right: 0.2em; }
|
|
10
|
+
shacl-node .add-button:hover { color: inherit; }
|
|
11
|
+
shacl-node h1 { font-size: 1.1rem; border-bottom: 1px solid; margin-top: 4px; color: #555; }
|
|
12
|
+
shacl-property { display: flex; flex-direction: column; align-items: end; position: relative; }
|
|
13
|
+
shacl-property:not(.may-add) > .add-button { display: none; }
|
|
14
|
+
shacl-property:not(.may-remove) > .property-instance > .remove-button:not(.persistent) { visibility: hidden; }
|
|
15
|
+
shacl-property:not(.may-remove) > .shacl-or-constraint > .remove-button:not(.persistent) { visibility: hidden; }
|
|
16
|
+
.shacl-group { margin-bottom: 1em; padding-bottom: 1em; }
|
|
17
|
+
.mode-view .shacl-group:not(:has(shacl-property)) { display: none; }
|
|
18
|
+
.property-instance, .shacl-or-constraint { display: flex; align-items: flex-start; padding: 4px 0; width: 100%; position: relative; }
|
|
19
|
+
.shacl-or-constraint label { display: inline-block; word-break: break-word; width: var(--label-width); line-height: 1em; padding-top: 0.15em; padding-right: 1em; flex-shrink: 0; position: relative; }
|
|
20
|
+
.property-instance label[title] { cursor: help; text-decoration: underline dashed #AAA; }
|
|
21
|
+
.mode-edit .property-instance label.required::before { color: red; content: '\2736'; font-size: 0.6rem; position: absolute; left: -1.4em; top: 0.15rem; }
|
|
22
|
+
.property-instance.valid::before { position: absolute; left: calc(var(--label-width) - 1em); top: 6px; color: green; content: '\2713'; }
|
|
23
|
+
.editor:not([type='checkbox']), .shacl-or-constraint select { flex-grow: 1; }
|
|
24
|
+
.shacl-or-constraint select { border: 1px solid #DDD; padding: 2px 4px; }
|
|
25
|
+
select { overflow: hidden; text-overflow: ellipsis; }
|
|
26
|
+
textarea.editor { resize: vertical; }
|
|
27
|
+
.lang-chooser { position: absolute; top: 5px; right: 24px; border: 0; background-color: #e9e9ed; padding: 2px 4px; max-width: 40px; width: 40px; box-sizing: content-box; }
|
|
28
|
+
.lang-chooser+.editor { padding-right: 55px; }
|
|
29
|
+
.validation-error { position: absolute; left: calc(var(--label-width) - 1em); top: 6px; color: red; cursor: help; }
|
|
30
|
+
.validation-error::before { content: '\26a0' }
|
|
31
|
+
.validation-error.node { left: -1em; }
|
|
32
|
+
.invalid > .editor { border-color: red !important; }
|
|
33
|
+
.ml-0 { margin-left: 0 !important; }
|
|
34
|
+
.pr-0 { padding-right: 0 !important; }
|
|
35
|
+
.mode-view .property-instance:not(:first-child) > label { visibility: hidden; }
|
|
36
|
+
.mode-view .property-instance label { width: var(--label-width); }
|
|
37
|
+
|
|
38
|
+
.d-flex { display: flex; }
|
|
39
|
+
.lang { opacity: 0.65; font-size: 0.6em; }
|
|
40
|
+
a, a:visited { color: inherit; }
|
|
41
|
+
|
|
42
|
+
.fadeIn, .fadeOut { animation: fadeIn 0.2s ease-out; }
|
|
43
|
+
.fadeOut { animation-direction: reverse; animation-timing-function: ease-out;}
|
|
44
|
+
@keyframes fadeIn {
|
|
45
|
+
0% { opacity: 0; transform: scaleY(0.8); }
|
|
46
|
+
100% { opacity: 1; transform: scaleY(1); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.collapsible > .activator { display: flex; justify-content: space-between; align-items: center; cursor: pointer; width: 100%; border: 0; padding: 8px 0; transition: 0.2s; }
|
|
50
|
+
.collapsible > .activator:hover, .collapsible.open > .activator { background-color: #F5F5F5; }
|
|
51
|
+
.collapsible > .activator::after { content:''; width: var(--caret-size); height: var(--caret-size); border-style: none solid solid none; border-width: calc(0.3 * var(--caret-size)); transform: rotate(45deg); transition: transform .15s ease-out; margin-right: calc(0.5 * var(--caret-size)); }
|
|
52
|
+
.collapsible.open > .activator::after { transform: rotate(225deg); }
|
|
53
|
+
.collapsible > *:not(.activator) { transition: all 0.2s ease-out; opacity: 1; }
|
|
54
|
+
.collapsible:not(.open) > *:not(.activator) { max-height: 0; padding: 0; opacity: 0; overflow: hidden; }
|
|
55
|
+
.collapsible > .property-instance > shacl-node > h1 { display: none; }
|
|
56
|
+
.collapsible.open > .property-instance:nth-child(odd) { background-color: #F5F5F5; }
|
|
57
|
+
.ref-link { cursor: pointer; }
|
|
58
|
+
.ref-link:hover { text-decoration: underline; }
|
|
59
|
+
.node-id-display { color: #999; font-size: 11px; }
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Literal, NamedNode } from 'n3'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { PREFIX_XSD, PREFIX_RDF } from './constants'
|
|
4
|
+
import { createInputListEntries, findInstancesOf, findLabel, isURL } from './util'
|
|
5
|
+
import { ShaclPropertyTemplate } from './property-template'
|
|
6
|
+
import css from './styles.css?raw'
|
|
7
|
+
|
|
8
|
+
export type Editor = HTMLElement & { value: string, type?: string, shaclDatatype?: NamedNode<string>, binaryData?: string, checked?: boolean, disabled?: boolean }
|
|
9
|
+
export type InputListEntry = { value: Term | string, label?: string, indent?: number }
|
|
10
|
+
|
|
11
|
+
export abstract class Theme {
|
|
12
|
+
stylesheet: CSSStyleSheet
|
|
13
|
+
|
|
14
|
+
constructor(styles?: string) {
|
|
15
|
+
let aggregatedStyles = css
|
|
16
|
+
if (styles) {
|
|
17
|
+
aggregatedStyles += '\n' + styles
|
|
18
|
+
}
|
|
19
|
+
this.stylesheet = new CSSStyleSheet()
|
|
20
|
+
this.stylesheet.replaceSync(aggregatedStyles)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
apply(root: HTMLFormElement) {
|
|
24
|
+
// NOP
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
createViewer(label: string, value: Term, template: ShaclPropertyTemplate): HTMLElement {
|
|
28
|
+
const viewer = document.createElement('div')
|
|
29
|
+
const labelElem = document.createElement('label')
|
|
30
|
+
labelElem.innerHTML = label + ':'
|
|
31
|
+
if (template.description) {
|
|
32
|
+
labelElem.setAttribute('title', template.description.value)
|
|
33
|
+
}
|
|
34
|
+
viewer.appendChild(labelElem)
|
|
35
|
+
let name = value.value
|
|
36
|
+
let lang: HTMLElement | null = null
|
|
37
|
+
if (value instanceof NamedNode) {
|
|
38
|
+
const quads = template.config.shapesGraph.getQuads(name, null, null, null)
|
|
39
|
+
if (quads.length) {
|
|
40
|
+
const s = findLabel(quads, template.config.languages)
|
|
41
|
+
if (s) {
|
|
42
|
+
name = s
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else if (value instanceof Literal) {
|
|
46
|
+
if (value.language) {
|
|
47
|
+
lang = document.createElement('span')
|
|
48
|
+
lang.classList.add('lang')
|
|
49
|
+
lang.innerText = `@${value.language}`
|
|
50
|
+
} else if (value.datatype.value === `${PREFIX_XSD}date`) {
|
|
51
|
+
name = new Date(Date.parse(value.value)).toDateString()
|
|
52
|
+
} else if (value.datatype.value === `${PREFIX_XSD}dateTime`) {
|
|
53
|
+
name = new Date(Date.parse(value.value)).toLocaleString()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let valueElem: HTMLElement
|
|
57
|
+
if (isURL(value.value)) {
|
|
58
|
+
valueElem = document.createElement('a')
|
|
59
|
+
valueElem.setAttribute('href', value.value)
|
|
60
|
+
} else {
|
|
61
|
+
valueElem = document.createElement('div')
|
|
62
|
+
}
|
|
63
|
+
valueElem.classList.add('d-flex')
|
|
64
|
+
valueElem.innerText = name
|
|
65
|
+
if (lang) {
|
|
66
|
+
valueElem.appendChild(lang)
|
|
67
|
+
}
|
|
68
|
+
viewer.appendChild(valueElem)
|
|
69
|
+
return viewer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
abstract createListEditor(label: string, value: Term | null, required: boolean, listEntries: InputListEntry[], template?: ShaclPropertyTemplate): HTMLElement
|
|
73
|
+
abstract createLangStringEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
74
|
+
abstract createTextEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
75
|
+
abstract createNumberEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
76
|
+
abstract createDateEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
77
|
+
abstract createBooleanEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
78
|
+
abstract createFileEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement
|
|
79
|
+
abstract createButton(label: string, primary: boolean): HTMLElement
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function fieldFactory(template: ShaclPropertyTemplate, value: Term | null): HTMLElement {
|
|
83
|
+
if (template.config.editMode) {
|
|
84
|
+
const required = template.minCount !== undefined && template.minCount > 0
|
|
85
|
+
// if we have a class, find the instances and display them in a list
|
|
86
|
+
if (template.class) {
|
|
87
|
+
return template.config.theme.createListEditor(template.label, value, required, findInstancesOf(template.class, template), template)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// check if it is a list
|
|
91
|
+
if (template.shaclIn) {
|
|
92
|
+
const list = template.config.lists[template.shaclIn]
|
|
93
|
+
if (list?.length) {
|
|
94
|
+
const listEntries = createInputListEntries(list, template.config.shapesGraph, template.config.languages)
|
|
95
|
+
return template.config.theme.createListEditor(template.label, value, required, listEntries, template)
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error('list not found:', template.shaclIn, 'existing lists:', template.config.lists)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// check if it is a langstring
|
|
103
|
+
if (template.datatype?.value === `${PREFIX_RDF}langString` || template.languageIn?.length) {
|
|
104
|
+
return template.config.theme.createLangStringEditor(template.label, value, required, template)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (template.datatype?.value.replace(PREFIX_XSD, '')) {
|
|
108
|
+
case 'integer':
|
|
109
|
+
case 'float':
|
|
110
|
+
case 'double':
|
|
111
|
+
case 'decimal':
|
|
112
|
+
return template.config.theme.createNumberEditor(template.label, value, required, template)
|
|
113
|
+
case 'date':
|
|
114
|
+
case 'dateTime':
|
|
115
|
+
return template.config.theme.createDateEditor(template.label, value, required, template)
|
|
116
|
+
case 'boolean':
|
|
117
|
+
return template.config.theme.createBooleanEditor(template.label, value, required, template)
|
|
118
|
+
case 'base64Binary':
|
|
119
|
+
return template.config.theme.createFileEditor(template.label, value, required, template)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// nothing found (or datatype is 'string'), fallback to 'text'
|
|
123
|
+
return template.config.theme.createTextEditor(template.label, value, required, template)
|
|
124
|
+
} else {
|
|
125
|
+
if (value) {
|
|
126
|
+
return template.config.theme.createViewer(template.label, value, template)
|
|
127
|
+
}
|
|
128
|
+
const fallback = document.createElement('div')
|
|
129
|
+
fallback.innerHTML = 'No value'
|
|
130
|
+
return fallback
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
form.mode-edit { --label-width: 0em; }
|
|
2
|
+
.lang-chooser { right: 24px; font-size: 0.8em; }
|
|
3
|
+
.property-instance[data-description]::after { content: attr(data-description); position: absolute; bottom: -12px; left: 13px; font-size: 12px; opacity: 0.7;}
|
|
4
|
+
.property-instance { margin-bottom:14px; }
|
|
5
|
+
.form-floating[data-description] { margin-bottom: 28px; }
|
|
6
|
+
.remove-button { padding: 6px; }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DefaultTheme } from './default'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { ShaclPropertyTemplate } from '../property-template'
|
|
4
|
+
import { Editor } from '../theme'
|
|
5
|
+
import bootstrap from 'bootstrap/dist/css/bootstrap.min.css'
|
|
6
|
+
import css from './bootstrap.css?raw'
|
|
7
|
+
|
|
8
|
+
export class BootstrapTheme extends DefaultTheme {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(bootstrap + '\n' + css)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
apply(root: HTMLFormElement): void {
|
|
14
|
+
super.apply(root)
|
|
15
|
+
root.dataset.bsTheme = 'light'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createDefaultTemplate(label: string, value: Term | null, required: boolean, editor: Editor, template?: ShaclPropertyTemplate | undefined): HTMLElement {
|
|
19
|
+
const result = super.createDefaultTemplate(label, value, required, editor, template)
|
|
20
|
+
if (editor.type !== 'checkbox') {
|
|
21
|
+
result.classList.add('form-floating')
|
|
22
|
+
if (editor.tagName === 'SELECT') {
|
|
23
|
+
editor.classList.add('form-select')
|
|
24
|
+
} else {
|
|
25
|
+
editor.classList.add('form-control')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const labelElem = result.querySelector('label')
|
|
29
|
+
labelElem?.classList.add('form-label')
|
|
30
|
+
if (labelElem?.title) {
|
|
31
|
+
result.dataset.description = labelElem.title
|
|
32
|
+
labelElem.removeAttribute('title')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
result.prepend(editor)
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createButton(label: string, primary: boolean): HTMLElement {
|
|
40
|
+
const button = super.createButton(label, primary)
|
|
41
|
+
button.classList.add('btn', primary ? 'btn-primary' : 'btn-outline-secondary')
|
|
42
|
+
return button
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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; }
|
|
4
|
+
.mode-edit .property-instance label { width: var(--label-width); }
|