@ulb-darmstadt/shacl-form 1.6.1 → 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 +8 -4
- package/dist/exports.d.ts +1 -1
- package/dist/form-bootstrap.js +1 -1
- package/dist/form-default.js +1 -1
- package/dist/form-material.js +1 -359
- package/dist/form-material.js.LICENSE.txt +0 -66
- package/dist/form.d.ts +4 -3
- package/dist/loader.d.ts +1 -0
- package/dist/plugins/leaflet.js +1 -1
- package/dist/plugins/mapbox.js +1 -1
- package/package.json +4 -3
- 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
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); }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Term } from '@rdfjs/types'
|
|
2
|
+
import { ShaclPropertyTemplate } from "../property-template"
|
|
3
|
+
import { Editor, InputListEntry, Theme } from "../theme"
|
|
4
|
+
import { PREFIX_XSD } from '../constants'
|
|
5
|
+
import { Literal } from 'n3'
|
|
6
|
+
import css from './default.css?raw'
|
|
7
|
+
|
|
8
|
+
export class DefaultTheme extends Theme {
|
|
9
|
+
idCtr = 0
|
|
10
|
+
|
|
11
|
+
constructor(overiddenCss?: string) {
|
|
12
|
+
super(overiddenCss ? overiddenCss : css)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createDefaultTemplate(label: string, value: Term | null, required: boolean, editor: Editor, template?: ShaclPropertyTemplate): HTMLElement {
|
|
16
|
+
editor.id = `e${this.idCtr++}`
|
|
17
|
+
editor.classList.add('editor')
|
|
18
|
+
if (template?.datatype) {
|
|
19
|
+
// store datatype on editor, this is used for RDF serialization
|
|
20
|
+
editor['shaclDatatype'] = template.datatype
|
|
21
|
+
}
|
|
22
|
+
if (template?.minCount !== undefined) {
|
|
23
|
+
editor.dataset.minCount = String(template.minCount)
|
|
24
|
+
}
|
|
25
|
+
if (template?.class) {
|
|
26
|
+
editor.dataset.class = template.class.value
|
|
27
|
+
}
|
|
28
|
+
if (template?.nodeKind) {
|
|
29
|
+
editor.dataset.nodeKind = template.nodeKind.value
|
|
30
|
+
}
|
|
31
|
+
if (template?.hasValue) {
|
|
32
|
+
editor.disabled = true
|
|
33
|
+
}
|
|
34
|
+
editor.value = value?.value || template?.defaultValue?.value || ''
|
|
35
|
+
|
|
36
|
+
const labelElem = document.createElement('label')
|
|
37
|
+
labelElem.htmlFor = editor.id
|
|
38
|
+
labelElem.innerText = label
|
|
39
|
+
if (template?.description) {
|
|
40
|
+
labelElem.setAttribute('title', template.description.value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const placeholder = template?.description ? template.description.value : template?.pattern ? template.pattern : null
|
|
44
|
+
if (placeholder) {
|
|
45
|
+
editor.setAttribute('placeholder', placeholder)
|
|
46
|
+
}
|
|
47
|
+
if (required) {
|
|
48
|
+
editor.setAttribute('required', 'true')
|
|
49
|
+
labelElem.classList.add('required')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = document.createElement('div')
|
|
53
|
+
result.appendChild(labelElem)
|
|
54
|
+
result.appendChild(editor)
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createDateEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
59
|
+
const editor: Editor = document.createElement('input')
|
|
60
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
61
|
+
editor.type = 'datetime-local'
|
|
62
|
+
// this enables seconds in dateTime input
|
|
63
|
+
editor.setAttribute('step', '1')
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
editor.type = 'date'
|
|
67
|
+
}
|
|
68
|
+
editor.classList.add('pr-0')
|
|
69
|
+
const result = this.createDefaultTemplate(label, null, required, editor, template)
|
|
70
|
+
if (value) {
|
|
71
|
+
try {
|
|
72
|
+
let isoDate = new Date(value.value).toISOString()
|
|
73
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
74
|
+
isoDate = isoDate.slice(0, 19)
|
|
75
|
+
} else {
|
|
76
|
+
isoDate = isoDate.slice(0, 10)
|
|
77
|
+
}
|
|
78
|
+
editor.value = isoDate
|
|
79
|
+
} catch(ex) {
|
|
80
|
+
console.error(ex, value)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
createTextEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
87
|
+
let editor
|
|
88
|
+
if (template.singleLine === false) {
|
|
89
|
+
editor = document.createElement('textarea')
|
|
90
|
+
editor.rows = 5
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
editor = document.createElement('input')
|
|
94
|
+
editor.type = 'text'
|
|
95
|
+
if (template.pattern) {
|
|
96
|
+
editor.pattern = template.pattern
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (template.minLength) {
|
|
101
|
+
editor.minLength = template.minLength
|
|
102
|
+
}
|
|
103
|
+
if (template.maxLength) {
|
|
104
|
+
editor.maxLength = template.maxLength
|
|
105
|
+
}
|
|
106
|
+
return this.createDefaultTemplate(label, value, required, editor, template)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
createLangStringEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
110
|
+
const result = this.createTextEditor(label, value, required, template)
|
|
111
|
+
const editor = result.querySelector(':scope .editor') as Editor
|
|
112
|
+
let langChooser: HTMLSelectElement | HTMLInputElement
|
|
113
|
+
if (template.languageIn?.length) {
|
|
114
|
+
langChooser = document.createElement('select')
|
|
115
|
+
for (const lang of template.languageIn) {
|
|
116
|
+
const option = document.createElement('option')
|
|
117
|
+
option.innerText = lang.value
|
|
118
|
+
langChooser.appendChild(option)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
langChooser = document.createElement('input')
|
|
122
|
+
langChooser.maxLength = 5 // e.g. en-US
|
|
123
|
+
langChooser.placeholder = 'lang?'
|
|
124
|
+
}
|
|
125
|
+
langChooser.title = 'Language of the text'
|
|
126
|
+
langChooser.classList.add('lang-chooser')
|
|
127
|
+
// if lang chooser changes, fire a change event on the text input instead. this is for shacl validation handling.
|
|
128
|
+
langChooser.addEventListener('change', (ev) => {
|
|
129
|
+
ev.stopPropagation();
|
|
130
|
+
if (editor) {
|
|
131
|
+
editor.dataset.lang = langChooser.value
|
|
132
|
+
editor.dispatchEvent(new Event('change', { bubbles: true }))
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
if (value instanceof Literal) {
|
|
136
|
+
langChooser.value = value.language
|
|
137
|
+
}
|
|
138
|
+
editor.dataset.lang = langChooser.value
|
|
139
|
+
editor.after(langChooser)
|
|
140
|
+
return result
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
createBooleanEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
144
|
+
const editor = document.createElement('input')
|
|
145
|
+
editor.type = 'checkbox'
|
|
146
|
+
editor.classList.add('ml-0')
|
|
147
|
+
|
|
148
|
+
const result = this.createDefaultTemplate(label, null, required, editor, template)
|
|
149
|
+
|
|
150
|
+
// 'required' on checkboxes forces the user to tick the checkbox, which is not what we want here
|
|
151
|
+
editor.removeAttribute('required')
|
|
152
|
+
result.querySelector(':scope label')?.classList.remove('required')
|
|
153
|
+
if (value instanceof Literal) {
|
|
154
|
+
editor.checked = value.value === 'true'
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
createFileEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
160
|
+
const editor = document.createElement('input')
|
|
161
|
+
editor.type = 'file'
|
|
162
|
+
editor.addEventListener('change', (e) => {
|
|
163
|
+
if (editor.files?.length) {
|
|
164
|
+
e.stopPropagation()
|
|
165
|
+
const reader = new FileReader()
|
|
166
|
+
reader.readAsDataURL(editor.files[0])
|
|
167
|
+
reader.onload = () => {
|
|
168
|
+
(editor as Editor)['binaryData'] = btoa(reader.result as string)
|
|
169
|
+
editor.parentElement?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
(editor as Editor)['binaryData'] = undefined
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
return this.createDefaultTemplate(label, value, required, editor, template)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
createNumberEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
179
|
+
const editor = document.createElement('input')
|
|
180
|
+
editor.type = 'number'
|
|
181
|
+
editor.classList.add('pr-0')
|
|
182
|
+
const min = template.minInclusive !== undefined ? template.minInclusive : template.minExclusive !== undefined ? template.minExclusive + 1 : undefined
|
|
183
|
+
const max = template.maxInclusive !== undefined ? template.maxInclusive : template.maxExclusive !== undefined ? template.maxExclusive - 1 : undefined
|
|
184
|
+
if (min !== undefined) {
|
|
185
|
+
editor.min = String(min)
|
|
186
|
+
}
|
|
187
|
+
if (max !== undefined) {
|
|
188
|
+
editor.max = String(max)
|
|
189
|
+
}
|
|
190
|
+
if (template.datatype?.value !== PREFIX_XSD + 'integer') {
|
|
191
|
+
editor.step = '0.1'
|
|
192
|
+
}
|
|
193
|
+
return this.createDefaultTemplate(label, value, required, editor, template)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
createListEditor(label: string, value: Term | null, required: boolean, listEntries: InputListEntry[], template?: ShaclPropertyTemplate): HTMLElement {
|
|
197
|
+
const editor = document.createElement('select')
|
|
198
|
+
const result = this.createDefaultTemplate(label, null, required, editor, template)
|
|
199
|
+
let addEmptyOption = true
|
|
200
|
+
|
|
201
|
+
for (const item of listEntries) {
|
|
202
|
+
const option = document.createElement('option')
|
|
203
|
+
const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
|
|
204
|
+
option.innerHTML = item.label ? item.label : itemValue
|
|
205
|
+
option.value = itemValue
|
|
206
|
+
if (item.indent) {
|
|
207
|
+
for (let i = 0; i < item.indent; i++) {
|
|
208
|
+
option.innerHTML = '  ' + option.innerHTML
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (value && value.value === itemValue) {
|
|
212
|
+
option.selected = true
|
|
213
|
+
}
|
|
214
|
+
if (itemValue === '') {
|
|
215
|
+
addEmptyOption = false
|
|
216
|
+
}
|
|
217
|
+
editor.appendChild(option)
|
|
218
|
+
}
|
|
219
|
+
if (addEmptyOption) {
|
|
220
|
+
// add an empty element
|
|
221
|
+
const emptyOption = document.createElement('option')
|
|
222
|
+
emptyOption.value = ''
|
|
223
|
+
if (!value) {
|
|
224
|
+
emptyOption.selected = true
|
|
225
|
+
}
|
|
226
|
+
editor.prepend(emptyOption)
|
|
227
|
+
}
|
|
228
|
+
if (value) {
|
|
229
|
+
editor.value = value.value
|
|
230
|
+
}
|
|
231
|
+
return result
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
createButton(label: string, primary: boolean): HTMLElement {
|
|
235
|
+
const button = document.createElement('button')
|
|
236
|
+
button.type = 'button'
|
|
237
|
+
button.innerHTML = label
|
|
238
|
+
return button
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--mdui-color-primary-light: var(--mdui-color-primary-light);
|
|
3
|
+
--mdui-color-primary-dark: var(--mdui-color-primary-dark);
|
|
4
|
+
--mdui-color-background-light: var(--mdui-color-background-light);
|
|
5
|
+
}
|
|
6
|
+
form.mode-edit { --label-width: 0em; }
|
|
7
|
+
.property-instance { margin-bottom:14px; }
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ShaclPropertyTemplate } from '../property-template'
|
|
2
|
+
import { Term } from '@rdfjs/types'
|
|
3
|
+
import { Button, TextField, Select, MenuItem, Checkbox } from 'mdui'
|
|
4
|
+
import { Theme } from '../theme'
|
|
5
|
+
import { InputListEntry, Editor } from '../theme'
|
|
6
|
+
import { Literal } from 'n3'
|
|
7
|
+
import css from './material.css?raw'
|
|
8
|
+
import { PREFIX_XSD } from '../constants'
|
|
9
|
+
|
|
10
|
+
export class MaterialTheme extends Theme {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(css)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createDefaultTemplate(label: string, value: Term | null, required: boolean, editor: Editor, template?: ShaclPropertyTemplate): HTMLElement {
|
|
16
|
+
editor.classList.add('editor')
|
|
17
|
+
if (template?.datatype) {
|
|
18
|
+
// store datatype on editor, this is used for RDF serialization
|
|
19
|
+
editor['shaclDatatype'] = template.datatype
|
|
20
|
+
}
|
|
21
|
+
if (template?.minCount !== undefined) {
|
|
22
|
+
editor.dataset.minCount = String(template.minCount)
|
|
23
|
+
}
|
|
24
|
+
if (template?.class) {
|
|
25
|
+
editor.dataset.class = template.class.value
|
|
26
|
+
}
|
|
27
|
+
if (template?.nodeKind) {
|
|
28
|
+
editor.dataset.nodeKind = template.nodeKind.value
|
|
29
|
+
}
|
|
30
|
+
if (template?.hasValue) {
|
|
31
|
+
editor.disabled = true
|
|
32
|
+
}
|
|
33
|
+
editor.value = value?.value || template?.defaultValue?.value || ''
|
|
34
|
+
|
|
35
|
+
const placeholder = template?.description ? template.description.value : template?.pattern ? template.pattern : null
|
|
36
|
+
if (placeholder) {
|
|
37
|
+
editor.setAttribute('placeholder', placeholder)
|
|
38
|
+
}
|
|
39
|
+
if (required) {
|
|
40
|
+
editor.setAttribute('required', 'true')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = document.createElement('div')
|
|
44
|
+
if (label) {
|
|
45
|
+
const labelElem = document.createElement('label')
|
|
46
|
+
labelElem.htmlFor = editor.id
|
|
47
|
+
labelElem.innerText = label
|
|
48
|
+
result.appendChild(labelElem)
|
|
49
|
+
}
|
|
50
|
+
result.appendChild(editor)
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createTextEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
55
|
+
const editor = new TextField()
|
|
56
|
+
editor.variant = 'outlined'
|
|
57
|
+
editor.label = label
|
|
58
|
+
editor.type = 'text'
|
|
59
|
+
if (template.description) {
|
|
60
|
+
editor.helper = template.description.value
|
|
61
|
+
}
|
|
62
|
+
if (template.singleLine === false) {
|
|
63
|
+
editor.rows = 5
|
|
64
|
+
}
|
|
65
|
+
if (template.pattern) {
|
|
66
|
+
editor.pattern = template.pattern
|
|
67
|
+
}
|
|
68
|
+
return this.createDefaultTemplate('', value, required, editor, template)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
createNumberEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
72
|
+
const editor = new TextField()
|
|
73
|
+
editor.variant = 'outlined'
|
|
74
|
+
editor.type = 'number'
|
|
75
|
+
editor.label = label
|
|
76
|
+
editor.helper = template.description ?.value || ''
|
|
77
|
+
const min = template.minInclusive !== undefined ? template.minInclusive : template.minExclusive !== undefined ? template.minExclusive + 1 : undefined
|
|
78
|
+
const max = template.maxInclusive !== undefined ? template.maxInclusive : template.maxExclusive !== undefined ? template.maxExclusive - 1 : undefined
|
|
79
|
+
if (min !== undefined) {
|
|
80
|
+
editor.setAttribute('min', String(min))
|
|
81
|
+
}
|
|
82
|
+
if (max !== undefined) {
|
|
83
|
+
editor.setAttribute('max', String(max))
|
|
84
|
+
}
|
|
85
|
+
if (template.datatype?.value !== PREFIX_XSD + 'integer') {
|
|
86
|
+
editor.setAttribute('step', '0.1')
|
|
87
|
+
}
|
|
88
|
+
return this.createDefaultTemplate('', value, required, editor, template)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
createListEditor(label: string, value: Term | null, required: boolean, listEntries: InputListEntry[], template?: ShaclPropertyTemplate): HTMLElement {
|
|
92
|
+
const editor = new Select()
|
|
93
|
+
editor.variant = 'outlined'
|
|
94
|
+
editor.label = label
|
|
95
|
+
editor.helper = template?.description?.value
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const result = this.createDefaultTemplate('', null, required, editor, template)
|
|
98
|
+
let addEmptyOption = true
|
|
99
|
+
|
|
100
|
+
for (const item of listEntries) {
|
|
101
|
+
const option = new MenuItem()
|
|
102
|
+
const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
|
|
103
|
+
const itemLabel = item.label ? item.label : itemValue
|
|
104
|
+
option.value = itemValue
|
|
105
|
+
option.textContent = itemLabel || itemValue
|
|
106
|
+
// if (value && value.value === itemValue) {
|
|
107
|
+
// option.selected = true
|
|
108
|
+
// }
|
|
109
|
+
if (item.indent) {
|
|
110
|
+
for (let i = 0; i < item.indent; i++) {
|
|
111
|
+
option.innerHTML = '  ' + option.innerHTML
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (itemValue === '') {
|
|
115
|
+
addEmptyOption = false
|
|
116
|
+
option.ariaLabel = 'blank'
|
|
117
|
+
}
|
|
118
|
+
editor.appendChild(option)
|
|
119
|
+
}
|
|
120
|
+
if (addEmptyOption) {
|
|
121
|
+
// add an empty element
|
|
122
|
+
const empty = new MenuItem()
|
|
123
|
+
empty.ariaLabel = 'blank'
|
|
124
|
+
editor.prepend(empty)
|
|
125
|
+
}
|
|
126
|
+
if (value) {
|
|
127
|
+
editor.value = value.value
|
|
128
|
+
}
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
createBooleanEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
133
|
+
const editor = new Checkbox()
|
|
134
|
+
const result = this.createDefaultTemplate('', value, required, editor, template)
|
|
135
|
+
// 'required' on checkboxes forces the user to tick the checkbox, which is not what we want here
|
|
136
|
+
editor.removeAttribute('required')
|
|
137
|
+
if (value instanceof Literal) {
|
|
138
|
+
editor.checked = value.value === 'true'
|
|
139
|
+
}
|
|
140
|
+
editor.innerText = label
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
createDateEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
145
|
+
const editor = new TextField()
|
|
146
|
+
editor.variant = 'outlined'
|
|
147
|
+
editor.helper = template?.description?.value || template?.label || ''
|
|
148
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
149
|
+
editor.type = 'datetime-local'
|
|
150
|
+
// this enables seconds in dateTime input
|
|
151
|
+
editor.setAttribute('step', '1')
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
editor.type = 'date'
|
|
155
|
+
}
|
|
156
|
+
editor.classList.add('pr-0')
|
|
157
|
+
const result = this.createDefaultTemplate('', null, required, editor, template)
|
|
158
|
+
if (value) {
|
|
159
|
+
try {
|
|
160
|
+
let isoDate = new Date(value.value).toISOString()
|
|
161
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
162
|
+
isoDate = isoDate.slice(0, 19)
|
|
163
|
+
} else {
|
|
164
|
+
isoDate = isoDate.slice(0, 10)
|
|
165
|
+
}
|
|
166
|
+
editor.value = isoDate
|
|
167
|
+
} catch(ex) {
|
|
168
|
+
console.error(ex, value)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
createLangStringEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
175
|
+
const result = this.createTextEditor(label, value, required, template)
|
|
176
|
+
const editor = result.querySelector(':scope .editor') as Editor
|
|
177
|
+
let langChooser: HTMLSelectElement | HTMLInputElement
|
|
178
|
+
if (template.languageIn?.length) {
|
|
179
|
+
langChooser = document.createElement('select')
|
|
180
|
+
for (const lang of template.languageIn) {
|
|
181
|
+
const option = document.createElement('option')
|
|
182
|
+
option.innerText = lang.value
|
|
183
|
+
langChooser.appendChild(option)
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
langChooser = document.createElement('input')
|
|
187
|
+
langChooser.maxLength = 5 // e.g. en-US
|
|
188
|
+
langChooser.placeholder = 'lang?'
|
|
189
|
+
}
|
|
190
|
+
langChooser.title = 'Language of the text'
|
|
191
|
+
langChooser.classList.add('lang-chooser')
|
|
192
|
+
// if lang chooser changes, fire a change event on the text input instead. this is for shacl validation handling.
|
|
193
|
+
langChooser.addEventListener('change', (ev) => {
|
|
194
|
+
ev.stopPropagation();
|
|
195
|
+
if (editor) {
|
|
196
|
+
editor.dataset.lang = langChooser.value
|
|
197
|
+
editor.dispatchEvent(new Event('change', { bubbles: true }))
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
if (value instanceof Literal) {
|
|
201
|
+
langChooser.value = value.language
|
|
202
|
+
}
|
|
203
|
+
editor.dataset.lang = langChooser.value
|
|
204
|
+
editor.after(langChooser)
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
createFileEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
209
|
+
const editor = document.createElement('input')
|
|
210
|
+
editor.type = 'file'
|
|
211
|
+
editor.addEventListener('change', (e) => {
|
|
212
|
+
if (editor.files?.length) {
|
|
213
|
+
e.stopPropagation()
|
|
214
|
+
const reader = new FileReader()
|
|
215
|
+
reader.readAsDataURL(editor.files[0])
|
|
216
|
+
reader.onload = () => {
|
|
217
|
+
(editor as Editor)['binaryData'] = btoa(reader.result as string)
|
|
218
|
+
editor.parentElement?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
(editor as Editor)['binaryData'] = undefined
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
return this.createDefaultTemplate(label, value, required, editor, template)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
createButton(label: string, primary: boolean): HTMLElement {
|
|
228
|
+
let button
|
|
229
|
+
if (primary) {
|
|
230
|
+
button = new Button()
|
|
231
|
+
button.classList.add('primary')
|
|
232
|
+
} else {
|
|
233
|
+
button = new Button()
|
|
234
|
+
button.classList.add('secondary')
|
|
235
|
+
}
|
|
236
|
+
button.innerHTML = label
|
|
237
|
+
return button
|
|
238
|
+
}
|
|
239
|
+
}
|