@ulb-darmstadt/shacl-form 1.6.2 → 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.
@@ -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)
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ declare module 'shacl-engine'
2
+ declare module '@rdfjs/data-model'
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); }