@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
|
@@ -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,14 @@
|
|
|
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
|
+
/* this prevents hiding the date picker icon and number controls */
|
|
7
|
+
mdui-text-field::part(input) {
|
|
8
|
+
clip-path: none;
|
|
9
|
+
appearance: inherit;
|
|
10
|
+
}
|
|
11
|
+
form.mode-edit { --label-width: 0em; }
|
|
12
|
+
.property-instance { margin-bottom:14px; }
|
|
13
|
+
.mode-edit .property-instance *[required]::part(icon) { align-self: flex-start; padding-top: 0.7em; }
|
|
14
|
+
.mode-edit .property-instance *[required]::part(icon)::before { color: red; content: '\2736'; font-size: 0.6em; }
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
editor.clearable = true
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
const result = this.createDefaultTemplate('', null, required, editor, template)
|
|
99
|
+
let addEmptyOption = true
|
|
100
|
+
|
|
101
|
+
for (const item of listEntries) {
|
|
102
|
+
const option = new MenuItem()
|
|
103
|
+
const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
|
|
104
|
+
const itemLabel = item.label ? item.label : itemValue
|
|
105
|
+
option.value = itemValue
|
|
106
|
+
option.textContent = itemLabel || itemValue
|
|
107
|
+
// if (value && value.value === itemValue) {
|
|
108
|
+
// option.selected = true
|
|
109
|
+
// }
|
|
110
|
+
if (item.indent) {
|
|
111
|
+
for (let i = 0; i < item.indent; i++) {
|
|
112
|
+
option.innerHTML = '  ' + option.innerHTML
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (itemValue === '') {
|
|
116
|
+
addEmptyOption = false
|
|
117
|
+
option.ariaLabel = 'blank'
|
|
118
|
+
}
|
|
119
|
+
editor.appendChild(option)
|
|
120
|
+
}
|
|
121
|
+
if (addEmptyOption) {
|
|
122
|
+
// add an empty element
|
|
123
|
+
const empty = new MenuItem()
|
|
124
|
+
empty.ariaLabel = 'blank'
|
|
125
|
+
editor.prepend(empty)
|
|
126
|
+
}
|
|
127
|
+
if (value) {
|
|
128
|
+
editor.value = value.value
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
createBooleanEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
134
|
+
const editor = new Checkbox()
|
|
135
|
+
const result = this.createDefaultTemplate('', value, required, editor, template)
|
|
136
|
+
// 'required' on checkboxes forces the user to tick the checkbox, which is not what we want here
|
|
137
|
+
editor.removeAttribute('required')
|
|
138
|
+
if (value instanceof Literal) {
|
|
139
|
+
editor.checked = value.value === 'true'
|
|
140
|
+
}
|
|
141
|
+
editor.innerText = label
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
createDateEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
146
|
+
const editor = new TextField()
|
|
147
|
+
editor.variant = 'outlined'
|
|
148
|
+
editor.helper = template?.description?.value || template?.label || ''
|
|
149
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
150
|
+
editor.type = 'datetime-local'
|
|
151
|
+
// this enables seconds in dateTime input
|
|
152
|
+
editor.setAttribute('step', '1')
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
editor.type = 'date'
|
|
156
|
+
}
|
|
157
|
+
editor.classList.add('pr-0')
|
|
158
|
+
const result = this.createDefaultTemplate('', null, required, editor, template)
|
|
159
|
+
if (value) {
|
|
160
|
+
try {
|
|
161
|
+
let isoDate = new Date(value.value).toISOString()
|
|
162
|
+
if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
|
|
163
|
+
isoDate = isoDate.slice(0, 19)
|
|
164
|
+
} else {
|
|
165
|
+
isoDate = isoDate.slice(0, 10)
|
|
166
|
+
}
|
|
167
|
+
editor.value = isoDate
|
|
168
|
+
} catch(ex) {
|
|
169
|
+
console.error(ex, value)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
createLangStringEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
176
|
+
const result = this.createTextEditor(label, value, required, template)
|
|
177
|
+
const editor = result.querySelector(':scope .editor') as Editor
|
|
178
|
+
let langChooser: HTMLSelectElement | HTMLInputElement
|
|
179
|
+
if (template.languageIn?.length) {
|
|
180
|
+
langChooser = document.createElement('select')
|
|
181
|
+
for (const lang of template.languageIn) {
|
|
182
|
+
const option = document.createElement('option')
|
|
183
|
+
option.innerText = lang.value
|
|
184
|
+
langChooser.appendChild(option)
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
langChooser = document.createElement('input')
|
|
188
|
+
langChooser.maxLength = 5 // e.g. en-US
|
|
189
|
+
langChooser.placeholder = 'lang?'
|
|
190
|
+
}
|
|
191
|
+
langChooser.title = 'Language of the text'
|
|
192
|
+
langChooser.classList.add('lang-chooser')
|
|
193
|
+
// if lang chooser changes, fire a change event on the text input instead. this is for shacl validation handling.
|
|
194
|
+
langChooser.addEventListener('change', (ev) => {
|
|
195
|
+
ev.stopPropagation();
|
|
196
|
+
if (editor) {
|
|
197
|
+
editor.dataset.lang = langChooser.value
|
|
198
|
+
editor.dispatchEvent(new Event('change', { bubbles: true }))
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
if (value instanceof Literal) {
|
|
202
|
+
langChooser.value = value.language
|
|
203
|
+
}
|
|
204
|
+
editor.dataset.lang = langChooser.value
|
|
205
|
+
editor.after(langChooser)
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
createFileEditor(label: string, value: Term | null, required: boolean, template: ShaclPropertyTemplate): HTMLElement {
|
|
210
|
+
const editor = document.createElement('input')
|
|
211
|
+
editor.type = 'file'
|
|
212
|
+
editor.addEventListener('change', (e) => {
|
|
213
|
+
if (editor.files?.length) {
|
|
214
|
+
e.stopPropagation()
|
|
215
|
+
const reader = new FileReader()
|
|
216
|
+
reader.readAsDataURL(editor.files[0])
|
|
217
|
+
reader.onload = () => {
|
|
218
|
+
(editor as Editor)['binaryData'] = btoa(reader.result as string)
|
|
219
|
+
editor.parentElement?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
(editor as Editor)['binaryData'] = undefined
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
return this.createDefaultTemplate(label, value, required, editor, template)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
createButton(label: string, primary: boolean): HTMLElement {
|
|
229
|
+
let button
|
|
230
|
+
if (primary) {
|
|
231
|
+
button = new Button()
|
|
232
|
+
button.classList.add('primary')
|
|
233
|
+
} else {
|
|
234
|
+
button = new Button()
|
|
235
|
+
button.classList.add('secondary')
|
|
236
|
+
}
|
|
237
|
+
button.innerHTML = label
|
|
238
|
+
return button
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Literal, NamedNode, Prefixes, Quad, Store } from 'n3'
|
|
2
|
+
import { OWL_OBJECT_NAMED_INDIVIDUAL, PREFIX_RDFS, PREFIX_SHACL, PREFIX_SKOS, RDFS_PREDICATE_SUBCLASS_OF, RDF_PREDICATE_TYPE, SHAPES_GRAPH, SKOS_PREDICATE_BROADER } from './constants'
|
|
3
|
+
import { Term } from '@rdfjs/types'
|
|
4
|
+
import { InputListEntry } from './theme'
|
|
5
|
+
import { ShaclPropertyTemplate } from './property-template'
|
|
6
|
+
import { ShaclNode } from './node'
|
|
7
|
+
|
|
8
|
+
export function findObjectValueByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, languages?: string[]): string {
|
|
9
|
+
let result = ''
|
|
10
|
+
const object = findObjectByPredicate(quads, predicate, prefix, languages)
|
|
11
|
+
if (object) {
|
|
12
|
+
result = object.value
|
|
13
|
+
}
|
|
14
|
+
return result
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function findObjectByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, languages?: string[]): Term | undefined {
|
|
18
|
+
let candidate: Term | undefined
|
|
19
|
+
const prefixedPredicate = prefix + predicate
|
|
20
|
+
|
|
21
|
+
if (languages?.length) {
|
|
22
|
+
for (const language of languages) {
|
|
23
|
+
for (const quad of quads) {
|
|
24
|
+
if (quad.predicate.value === prefixedPredicate) {
|
|
25
|
+
if (quad.object.id.endsWith(`@${language}`)) {
|
|
26
|
+
return quad.object
|
|
27
|
+
}
|
|
28
|
+
else if (quad.object.id.indexOf('@') < 0) {
|
|
29
|
+
candidate = quad.object
|
|
30
|
+
} else if (!candidate) {
|
|
31
|
+
candidate = quad.object
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
for (const quad of quads) {
|
|
38
|
+
if (quad.predicate.value === prefixedPredicate) {
|
|
39
|
+
return quad.object
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return candidate
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function focusFirstInputElement(context: HTMLElement) {
|
|
47
|
+
(context.querySelector('input,select,textarea') as HTMLElement)?.focus()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function findLabel(quads: Quad[], languages: string[]): string {
|
|
51
|
+
let label = findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, languages)
|
|
52
|
+
if (label) {
|
|
53
|
+
return label
|
|
54
|
+
}
|
|
55
|
+
return findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, languages)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createInputListEntries(subjects: Term[], shapesGraph: Store, languages: string[], indent?: number): InputListEntry[] {
|
|
59
|
+
const entries: InputListEntry[] = []
|
|
60
|
+
for (const subject of subjects) {
|
|
61
|
+
entries.push({ value: subject, label: findLabel(shapesGraph.getQuads(subject, null, null, null), languages), indent: indent })
|
|
62
|
+
}
|
|
63
|
+
return entries
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function removePrefixes(id: string, prefixes: Prefixes): string {
|
|
67
|
+
for (const key in prefixes) {
|
|
68
|
+
// need to ignore type check. 'prefix' is a string and not a NamedNode<string> (seems to be a bug in n3 typings)
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
id = id.replace(prefixes[key], '')
|
|
71
|
+
}
|
|
72
|
+
return id
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findClassInstancesFromOwlImports(clazz: NamedNode, context: ShaclNode | ShaclPropertyTemplate, shapesGraph: Store, instances: Term[], alreadyCheckedImports = new Set<string>()) {
|
|
76
|
+
for (const owlImport of context.owlImports) {
|
|
77
|
+
if (!alreadyCheckedImports.has(owlImport.id)) {
|
|
78
|
+
alreadyCheckedImports.add(owlImport.id)
|
|
79
|
+
instances.push(...shapesGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, owlImport))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (context.parent) {
|
|
83
|
+
findClassInstancesFromOwlImports(clazz, context.parent, shapesGraph, instances, alreadyCheckedImports)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function findInstancesOf(clazz: NamedNode, template: ShaclPropertyTemplate, indent = 0): InputListEntry[] {
|
|
88
|
+
// find instances in the shapes graph
|
|
89
|
+
const instances: Term[] = template.config.shapesGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, SHAPES_GRAPH)
|
|
90
|
+
// find instances in the data graph
|
|
91
|
+
instances.push(...template.config.dataGraph.getSubjects(RDF_PREDICATE_TYPE, clazz, null))
|
|
92
|
+
// find instances in imported taxonomies
|
|
93
|
+
findClassInstancesFromOwlImports(clazz, template, template.config.shapesGraph, instances)
|
|
94
|
+
|
|
95
|
+
const entries = createInputListEntries(instances, template.config.shapesGraph, template.config.languages, indent)
|
|
96
|
+
for (const subClass of template.config.shapesGraph.getSubjects(RDFS_PREDICATE_SUBCLASS_OF, clazz, null)) {
|
|
97
|
+
entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
|
|
98
|
+
}
|
|
99
|
+
if (template.config.shapesGraph.getQuads(clazz, RDF_PREDICATE_TYPE, OWL_OBJECT_NAMED_INDIVIDUAL, null).length > 0) {
|
|
100
|
+
entries.push(...createInputListEntries([ clazz ], template.config.shapesGraph, template.config.languages, indent))
|
|
101
|
+
for (const subClass of template.config.shapesGraph.getSubjects(SKOS_PREDICATE_BROADER, clazz, null)) {
|
|
102
|
+
entries.push(...findInstancesOf(subClass as NamedNode, template, indent + 1))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return entries
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isURL(input: string): boolean {
|
|
109
|
+
let url: URL
|
|
110
|
+
try {
|
|
111
|
+
url = new URL(input)
|
|
112
|
+
} catch (_) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function prioritizeByLanguage(languages: string[], text1?: Literal, text2?: Literal): Literal | undefined {
|
|
119
|
+
if (text1 === undefined) {
|
|
120
|
+
return text2
|
|
121
|
+
}
|
|
122
|
+
if (text2 === undefined) {
|
|
123
|
+
return text1
|
|
124
|
+
}
|
|
125
|
+
const index1 = languages.indexOf(text1.language)
|
|
126
|
+
if (index1 < 0) {
|
|
127
|
+
return text2
|
|
128
|
+
}
|
|
129
|
+
const index2 = languages.indexOf(text2.language)
|
|
130
|
+
if (index2 < 0) {
|
|
131
|
+
return text1
|
|
132
|
+
}
|
|
133
|
+
return index2 > index1 ? text1 : text2
|
|
134
|
+
}
|