@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/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 = '&#160;&#160;' + 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 = '&#160;&#160;' + 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
+ }