diff --git a/README.md b/README.md
index 0b03caa..8f2f469 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ data-shape-subject | Optional subject (id) of the SHACL node shape to use as roo
data-values | RDF triples (e.g. a turtle string) to use as existing data values to fill the form
data-values-url | When `data-values` is not set, the data triples are loaded from this URL
data-value-subject | The subject (id) of the generated data. If this is not set, a blank node with a new UUID will be used. If `data-values` or `data-values-url` is set, this id is also used to find existing data in the data graph to fill the form
-data-language | Language to use if shapes contain langStrings, e.g. in `sh:name` or `rdfs:label`
+data-language | Language to use if shapes contain langStrings, e.g. in `sh:name` or `rdfs:label`. Default is [`navigator.language`](https://www.w3schools.com/jsref/prop_nav_language.asp)
data‑ignore‑owl‑imports | By default, `owl:imports` IRIs are fetched and the resulting triples added to the shapes graph. Set this attribute to any value in order to disable this feature
data-submit-button | Whether to add a submit button to the form. The value of this attribute is used as the button label. `submit` events will only fire after successful validation
diff --git a/demo/complex-example-data.ttl b/demo/complex-example-data.ttl
index 8ddea3d..9e02adc 100644
--- a/demo/complex-example-data.ttl
+++ b/demo/complex-example-data.ttl
@@ -9,24 +9,24 @@
@prefix prov: .
example:4f2a8de3-9fc8-40a9-9237-d5964520ec54
- a dcat:Dataset, example:ArchitectureModelDataset ;
- dcterms:title "Einsteinturm"@de, "Einstein Tower"@en ;
- dcterms:description "Modell des Einsteinturms"@de, "Model of the Einstein Tower"@en ;
- dcterms:issued "2023-07-27"^^xsd:date ;
- dcterms:license ;
- schema:artworkSurface example:wood ;
- schema:width 200 ;
- dcterms:spatial [
- a dcterms:Location ;
- geo:asWKT "POLYGON((13.06382134836241 52.37900504575066,13.063796707503286 52.37896794299019,13.063798350228126 52.37875635638159,13.063926482692182 52.37875435081642,13.0639281254158 52.378964934657034,13.063905127281544 52.37900404297392,13.06382134836241 52.37900504575066))"^^geo:wktLiteral ;
- dcterms:description "Building has been realized here" ;
- ] ;
- prov:qualifiedAttribution [
- a prov:Attribution ;
- prov:agent [
- a foaf:Person ;
- foaf:name "Jane Doe";
- dcterms:identifier "https://orcid.org/0000-0002-1584-4316" ;
- ] ;
- dcat:hadRole ;
- ] .
+ a dcat:Dataset, example:ArchitectureModelDataset ;
+ dcterms:title "Einsteinturm"@de, "Einstein Tower"@en ;
+ dcterms:description "Modell des Einsteinturms"@de, "Model of the Einstein Tower"@en ;
+ dcterms:issued "2023-07-27"^^xsd:date ;
+ dcterms:license ;
+ schema:artworkSurface example:plaster ;
+ schema:width 200 ;
+ dcterms:spatial [
+ a dcterms:Location ;
+ geo:asWKT "POLYGON((13.06382134836241 52.37900504575066,13.063796707503286 52.37896794299019,13.063798350228126 52.37875635638159,13.063926482692182 52.37875435081642,13.0639281254158 52.378964934657034,13.063905127281544 52.37900404297392,13.06382134836241 52.37900504575066))"^^geo:wktLiteral ;
+ dcterms:description "Building has been realized here" ;
+ ] ;
+ prov:qualifiedAttribution [
+ a prov:Attribution ;
+ prov:agent [
+ a foaf:Person ;
+ foaf:name "Jane Doe";
+ dcterms:identifier "https://orcid.org/0000-0002-1584-4316" ;
+ ] ;
+ dcat:hadRole ;
+ ] .
diff --git a/demo/complex-example.ttl b/demo/complex-example.ttl
index 0d0580d..8eb8f73 100644
--- a/demo/complex-example.ttl
+++ b/demo/complex-example.ttl
@@ -14,166 +14,171 @@
@prefix example: .
example:ArchitectureModelDataset
- a sh:NodeShape ;
- sh:node example:Dataset ;
+ a sh:NodeShape ;
+ sh:node example:Dataset ;
- sh:property [ sh:description "Location of the building" ;
- sh:name "Location" ;
- sh:node example:Location ;
- sh:path dcterms:spatial
- ] ;
- sh:property [ sh:datatype xsd:integer ;
- sh:description "Width [mm] of the model" ;
- sh:group example:PhysicalPropertiesGroup ;
- sh:maxCount 1 ;
- sh:minInclusive 1 ;
- sh:name "Width" ;
- sh:path schema:width
- ] ;
- sh:property [ sh:datatype xsd:integer ;
- sh:description "Height [mm] of the model" ;
- sh:group example:PhysicalPropertiesGroup ;
- sh:maxCount 1 ;
- sh:minInclusive 1 ;
- sh:name "Height" ;
- sh:path schema:height
- ] ;
- sh:property [ sh:datatype xsd:integer ;
- sh:description "Depth [mm] of the model" ;
- sh:group example:PhysicalPropertiesGroup ;
- sh:maxCount 1 ;
- sh:minInclusive 1 ;
- sh:name "Depth" ;
- sh:path schema:depth
- ] ;
- sh:property [ sh:datatype xsd:string ;
- sh:description "Scale of the model, e.g. 1:20" ;
- sh:group example:PhysicalPropertiesGroup ;
- sh:maxCount 1 ;
- sh:name "Scale" ;
- sh:path dbpedia:scale ;
- sh:pattern "^\\d+:\\d+$"
- ] ;
- sh:property [ sh:description "Material used with this model"@en , "Beim Bau des Modells verwendetes Material"@de ;
- sh:group example:PhysicalPropertiesGroup ;
- sh:name "Artwork material" ;
- sh:path schema:artworkSurface ;
- sh:class example:Material ;
- ] .
+ sh:property [ sh:description "Location of the building" ;
+ sh:name "Location" ;
+ sh:node example:Location ;
+ sh:path dcterms:spatial
+ ] ;
+ sh:property [ sh:datatype xsd:integer ;
+ sh:description "Width [mm] of the model" ;
+ sh:group example:PhysicalPropertiesGroup ;
+ sh:maxCount 1 ;
+ sh:minInclusive 1 ;
+ sh:name "Width" ;
+ sh:path schema:width
+ ] ;
+ sh:property [ sh:datatype xsd:integer ;
+ sh:description "Height [mm] of the model" ;
+ sh:group example:PhysicalPropertiesGroup ;
+ sh:maxCount 1 ;
+ sh:minInclusive 1 ;
+ sh:name "Height" ;
+ sh:path schema:height
+ ] ;
+ sh:property [ sh:datatype xsd:integer ;
+ sh:description "Depth [mm] of the model" ;
+ sh:group example:PhysicalPropertiesGroup ;
+ sh:maxCount 1 ;
+ sh:minInclusive 1 ;
+ sh:name "Depth" ;
+ sh:path schema:depth
+ ] ;
+ sh:property [ sh:datatype xsd:string ;
+ sh:description "Scale of the model, e.g. 1:20" ;
+ sh:group example:PhysicalPropertiesGroup ;
+ sh:maxCount 1 ;
+ sh:name "Scale" ;
+ sh:path dbpedia:scale ;
+ sh:pattern "^\\d+:\\d+$"
+ ] ;
+ sh:property [ sh:description "Material used with this model"@en , "Beim Bau des Modells verwendetes Material"@de ;
+ sh:group example:PhysicalPropertiesGroup ;
+ sh:name "Artwork material" ;
+ sh:path schema:artworkSurface ;
+ sh:class example:Material ;
+ ] .
example:Dataset
- a sh:NodeShape ;
- sh:property [ sh:datatype rdf:langString ;
- sh:languageIn ( "en" "de" ) ;
- sh:uniqueLang true ;
- sh:description "The name of the dataset" , "Der Name des Datensatzes"@de ;
- sh:minCount 1 ;
- sh:maxCount 2 ;
- sh:name "Name" ;
- sh:path dcterms:title
- ] ;
- sh:property [ dash:singleLine false ;
- sh:datatype rdf:langString ;
- sh:languageIn ( "en" "de" ) ;
- sh:uniqueLang true ;
- sh:description "Description of the dataset" ;
- sh:minCount 1 ;
- sh:maxCount 2 ;
- sh:name "Description" ;
- sh:path dcterms:description
- ] ;
- sh:property [ sh:description "License of the dataset" ;
- sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:name "License" ;
- sh:nodeKind sh:IRI ;
- sh:path dcterms:license ;
- sh:in (
- # see below how labels are added to the list entries
-
-
- )
- ] ;
- sh:property [ sh:datatype xsd:date ;
- sh:description "Date when this dataset has been issued" ;
- sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:name "Issued" ;
- sh:path dcterms:issued
- ] ;
- sh:property [ sh:name "Attribution" ;
- sh:node example:Attribution ;
- sh:path prov:qualifiedAttribution
- ] ;
- sh:targetClass dcat:Dataset .
+ a sh:NodeShape ;
+ sh:property [ sh:datatype rdf:langString ;
+ sh:languageIn ( "en" "de" ) ;
+ sh:uniqueLang true ;
+ sh:description "The name of the dataset" , "Der Name des Datensatzes"@de ;
+ sh:minCount 1 ;
+ sh:maxCount 2 ;
+ sh:name "Name" ;
+ sh:path dcterms:title
+ ] ;
+ sh:property [ dash:singleLine false ;
+ sh:datatype rdf:langString ;
+ sh:languageIn ( "en" "de" ) ;
+ sh:uniqueLang true ;
+ sh:description "Description of the dataset" ;
+ sh:minCount 1 ;
+ sh:maxCount 2 ;
+ sh:name "Description" ;
+ sh:path dcterms:description
+ ] ;
+ sh:property [ sh:description "License of the dataset" ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:name "License" ;
+ sh:nodeKind sh:IRI ;
+ sh:path dcterms:license ;
+ sh:in (
+ # see below how labels are added to the list entries
+
+
+ )
+ ] ;
+ sh:property [ sh:datatype xsd:date ;
+ sh:description "Date when this dataset has been issued" ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:name "Issued" ;
+ sh:path dcterms:issued
+ ] ;
+ sh:property [ sh:name "Attribution" ;
+ sh:node example:Attribution ;
+ sh:path prov:qualifiedAttribution
+ ] ;
+ sh:targetClass dcat:Dataset .
example:PhysicalPropertiesGroup
- a sh:PropertyGroup ;
- rdfs:label "Physical properties" .
+ a sh:PropertyGroup ;
+ rdfs:label "Physical properties" .
example:Location
- a sh:NodeShape ;
- rdfs:label "Location" ;
- sh:property [ sh:datatype geo:wktLiteral ;
- sh:description "Format WKT, e.g. POINT(8.65 49.87)" ;
- sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:name "Coordinates" ;
- sh:path geo:asWKT ;
- sh:pattern "^POINT\\([+\\-]?(?:[0-9]*[.])?[0-9]+ [+\\-]?(?:[0-9]*[.])?[0-9]+\\)|POLYGON\\(\\((?:[+\\-]?(?:[0-9]*[.])?[0-9]+[ ,]?){3,}\\)\\)$"
- ] ;
- sh:property [ sh:description "Description of the location" ;
- sh:maxCount 1 ;
- sh:name "Description" ;
- sh:path dcterms:description
- ] ;
- sh:targetClass dcterms:Location .
+ a sh:NodeShape ;
+ rdfs:label "Location" ;
+ sh:property [ sh:datatype geo:wktLiteral ;
+ sh:description "Format WKT, e.g. POINT(8.65 49.87)" ;
+ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:name "Coordinates" ;
+ sh:path geo:asWKT ;
+ sh:pattern "^POINT\\([+\\-]?(?:[0-9]*[.])?[0-9]+ [+\\-]?(?:[0-9]*[.])?[0-9]+\\)|POLYGON\\(\\((?:[+\\-]?(?:[0-9]*[.])?[0-9]+[ ,]?){3,}\\)\\)$"
+ ] ;
+ sh:property [ sh:description "Description of the location" ;
+ sh:maxCount 1 ;
+ sh:name "Description" ;
+ sh:path dcterms:description
+ ] ;
+ sh:targetClass dcterms:Location .
example:Person
- a sh:NodeShape ;
- rdfs:label "Person" ;
- sh:property [ sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:name "Name" ;
- sh:path foaf:name
- ] ;
- sh:property [ sh:datatype xsd:string ;
- sh:maxCount 1 ;
- sh:name "ORCID ID" ;
- sh:path dcterms:identifier ;
- sh:pattern "^https://orcid.org/\\d{4}-\\d{4}-\\d{4}-\\d{4}$"
- ] ;
- sh:targetClass foaf:Person .
+ a sh:NodeShape ;
+ rdfs:label "Person" ;
+ sh:property [ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:name "Name" ;
+ sh:path foaf:name
+ ] ;
+ sh:property [ sh:datatype xsd:string ;
+ sh:maxCount 1 ;
+ sh:name "ORCID ID" ;
+ sh:path dcterms:identifier ;
+ sh:pattern "^https://orcid.org/\\d{4}-\\d{4}-\\d{4}-\\d{4}$"
+ ] ;
+ sh:targetClass foaf:Person .
example:Organisation
- a sh:NodeShape ;
- rdfs:label "Organisation" ;
- sh:property [ sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:name "Name" ;
- sh:path foaf:name
- ] ;
- sh:targetClass foaf:Organisation .
+ a sh:NodeShape ;
+ rdfs:label "Organisation" ;
+ sh:property [ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:name "Name" ;
+ sh:path foaf:name
+ ] ;
+ sh:property [ sh:name "Address" ;
+ sh:path example:Address
+ ] ;
+ sh:targetClass foaf:Organisation .
example:Attribution
- a sh:NodeShape ;
- # load an external taxonomy, which will be added to the shapes graph. In this case, the taxonomy provides class instances of prov:Role.
- owl:imports ;
+ a sh:NodeShape ;
+ # Import an external taxonomy to the shapes graph.
+ # In this case, the taxonomy provides class instances of prov:Role,
+ # which will be displayed in a dropdown to select from.
+ owl:imports ;
- sh:property [ sh:maxCount 1 ;
- sh:minCount 1 ;
- sh:path prov:agent ;
- sh:or (
- [ sh:node example:Person ; rdfs:label "Person" ]
- [ sh:node example:Organisation ; rdfs:label "Organisation" ]
- )
- ] ;
- sh:property [ sh:name "Role" ;
- sh:minCount 1 ;
- sh:path dcat:hadRole ;
- sh:class prov:Role
- ] ;
- sh:targetClass prov:Attribution .
+ sh:property [ sh:maxCount 1 ;
+ sh:minCount 1 ;
+ sh:path prov:agent ;
+ sh:or (
+ [ sh:node example:Person ; rdfs:label "Person" ]
+ [ sh:node example:Organisation ; rdfs:label "Organisation" ]
+ )
+ ] ;
+ sh:property [ sh:name "Role" ;
+ sh:minCount 1 ;
+ sh:path dcat:hadRole ;
+ sh:class prov:Role
+ ] ;
+ sh:targetClass prov:Attribution .
# add a label to the license IRIs
rdfs:label "CC-BY" .
diff --git a/demo/index.html b/demo/index.html
index e72d97c..e22a598 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -6,16 +6,17 @@
<shacl-form> demo
-
+
+
@@ -70,9 +71,9 @@
<shacl-form> demo
<shacl-form> is an HTML5 web component that takes SHACL shapes as input and generates an HTML form, allowing to enter data that conform to the given shapes.
- See the README for the documentation of all element attributes, functions and supported input/output RDF formats.
+ See the README for documentation of all element attributes, functions and supported input/output RDF formats.
-
Here's a basic usage example:
+
Basic usage example:
<html>
@@ -172,18 +173,19 @@
<shacl-form> demo
-
This is a more complex example, demonstrating the more advanced features. See below for an explanation.
+
This is a more complex example, demonstrating the advanced features of <shacl-form>. See below for an explanation.
-
+
-
The above uses the following features:
-
SHACL shape inheritance
+
The above uses the following features:
+
SHACL shape inheritance
-
Providing external data to the shapes graph
-
taxonomies (owl:import, classInstanceProvider)
-
Binding a data graph to the form
-
-
SHACL "or" constraint
-
-
Plugins
+
Providing additional data to the shapes graph
+
+ Apart from setting the element attributes data-shapes or data-shapes-url, there are two ways of adding RDF data to the shapes graph:
+
+
Using owl:imports: By default,
+
+
+
+
Binding a data graph to the form
+
SHACL "or" constraint
+
+ <shacl-form> supports using the sh:or constraint to let users select between different options on nodes or properties.
+ In the example above, the node shape example:Attribution defines a property with sh:path prov:agent,
+ whose values either conform to node shape example:Person or example:Organisation. After selecting one of the options, the respective input fields are added to the form.
+
+
When binding an existing data graph to the form, the sh:or constraint is tried to be resolved depending on the respective value:
+
+
For RDF literals, a sh:or option with a matching sh:datatype is chosen
+
For blank nodes or named nodes, the rdf:type of the value is tried to be matched with a node shape having a corresponding sh:targetClass or with a property shape having a corresponding sh:class
+
+
Plugins
+
The Javascript of this page contains the following code:
+
+
+import { MapBoxPlugin } from '@ulb-darmstadt/shacl-form'
+const form = document.getElementById("shacl-form")
+form.registerPlugin(new MapBoxPlugin({ datatype: 'http://www.opengis.net/ont/geosparql#wktLiteral' }))
+
+
+
Plugins can modify the rendering of the form and add functionality to edit certain RDF datatypes or predicates (or a combination of both).
shacl groups
lang strings (languageIn)
diff --git a/demo/style.css b/demo/style.css
index a995c0b..6f8ce5c 100644
--- a/demo/style.css
+++ b/demo/style.css
@@ -82,11 +82,10 @@ a:visited { color: inherit; }
}
.content h2 {
- margin-top: 0; color: #444;
+ margin-top: 1em; color: #444;
}
-.content p:first-child { margin-top: 0; }
-.content p { margin-top: 40px; line-height: 1.3em; font-size: 20px; color: #444; }
+.content p { margin-top: 0; line-height: 1.3em; font-size: 20px; color: #444; }
.wrapper {
display: grid;
@@ -96,7 +95,8 @@ a:visited { color: inherit; }
.w-100 { width: 100%; }
.h-100 { height: 100%; }
-.mt-1 { margin-top: 1em; }
+.mt-1 { margin-top: 1em !important; }
+.my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
.code { font-family: monospace; background-color: #ededed; padding: 3px; border-radius: 4px; color: #333; font-size: 14px; }
#shacl-output.valid legend::after { content: "[valid]"; padding-left: 5px; }
#shacl-output.invalid legend::after { content: "[not valid]"; padding-left: 5px; }
diff --git a/src/config.ts b/src/config.ts
index 43f9e6e..147d7bb 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -3,6 +3,7 @@ import { Term } from '@rdfjs/types'
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, SHAPES_GRAPH } from './constants'
import { ClassInstanceProvider, Plugins } from './plugin'
import { Loader } from './loader'
+import { NativeTheme, Theme } from './theme'
export class ElementAttributes {
shapes: string | null = null
@@ -11,7 +12,8 @@ export class ElementAttributes {
values: string | null = null
valuesUrl: string | null = null
valueSubject: string | null = null
- language: string | null = null
+ view: string | null = null
+ language: string = navigator.language
ignoreOwlImports: string | null = null
submitButton: string | null = null
}
@@ -22,10 +24,12 @@ export class Config {
classInstanceProvider: ClassInstanceProvider | undefined
prefixes: Prefixes = {}
plugins = new Plugins()
+ editMode = true
dataGraph = new Store()
lists: Record = {}
groups: Array = []
+ theme: Theme = new NativeTheme()
private _shapesGraph = new Store()
updateAttributes(elem: HTMLElement) {
@@ -35,8 +39,8 @@ export class Config {
atts[key] = elem.dataset[key]
}
}
- if (!atts.language) {
- atts.language = navigator.language
+ if (atts.view) {
+ this.editMode = false
}
this.attributes = atts
}
diff --git a/src/form.ts b/src/form.ts
index 68ecfcd..9153cdc 100644
--- a/src/form.ts
+++ b/src/form.ts
@@ -4,7 +4,7 @@ import { ClassInstanceProvider, Plugin } from './plugin'
import { Quad, Store, NamedNode, DataFactory } from 'n3'
import { PREFIX_SHACL, RDF_PREDICATE_TYPE, SHACL_OBJECT_NODE_SHAPE, SHACL_PREDICATE_TARGET_CLASS, SHAPES_GRAPH } from './constants'
import { focusFirstInputElement } from './util'
-import { Editor } from './editors'
+import { Editor, Theme } from './theme'
import { serialize } from './serialize'
import SHACLValidator from 'rdf-validate-shacl'
import './styles.css'
@@ -102,6 +102,11 @@ export class ShaclForm extends HTMLElement {
this.initialize()
}
+ public setTheme(theme: Theme) {
+ this.config.theme = theme
+ this.initialize()
+ }
+
public async validate(ignoreEmptyValues = false): Promise {
for (const elem of this.querySelectorAll(':scope .validation-error')) {
elem.remove()
diff --git a/src/index.ts b/src/index.ts
index c817df7..2e67750 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,3 @@
export { ShaclForm } from './form'
export { Plugin, ClassInstanceProvider } from './plugin'
-export { Editor } from './editors'
\ No newline at end of file
+export { Editor } from './theme'
\ No newline at end of file
diff --git a/src/plugins/fixed-list.ts b/src/plugins/fixed-list.ts
index 005e11e..fdc571e 100644
--- a/src/plugins/fixed-list.ts
+++ b/src/plugins/fixed-list.ts
@@ -2,7 +2,7 @@ import { Plugin, PluginOptions } from '../plugin'
import { Term } from '@rdfjs/types'
import { ShaclPropertyTemplate } from '../property-template'
-import { createListEditor, InputListEntry } from '../editors'
+import { InputListEntry } from '../theme'
export class FixedListPlugin extends Plugin {
entries: InputListEntry[]
@@ -13,6 +13,6 @@ export class FixedListPlugin extends Plugin {
}
createInstance(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
- return createListEditor(template, this.entries, value)
+ return template.config.theme.createListEditor(template, this.entries, value)
}
}
\ No newline at end of file
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index db33b27..d84d310 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -1,3 +1,3 @@
export { FixedListPlugin } from './fixed-list'
export { MapBoxPlugin } from './mapbox'
-export { InputListEntry } from '../editors'
\ No newline at end of file
+export { InputListEntry } from '../theme'
\ No newline at end of file
diff --git a/src/plugins/mapbox.ts b/src/plugins/mapbox.ts
index b752833..2695232 100644
--- a/src/plugins/mapbox.ts
+++ b/src/plugins/mapbox.ts
@@ -1,7 +1,7 @@
import { Term } from '@rdfjs/types'
import { Plugin, PluginOptions } from '../plugin'
import { ShaclPropertyTemplate } from '../property-template'
-import { createTextEditor, Editor } from '../editors'
+import { Editor } from '../theme'
import mapboxgl from 'mapbox-gl'
import MapboxDraw from '@mapbox/mapbox-gl-draw'
import 'mapbox-gl/dist/mapbox-gl.css'
@@ -64,7 +64,7 @@ export class MapBoxPlugin extends Plugin {
}
createInstance(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
- const instance = createTextEditor(template, value)
+ const instance = template.config.theme.createTextEditor(template, value)
const button = document.createElement('button')
button.type = 'button'
button.innerHTML = 'Open map...'
diff --git a/src/property-template.ts b/src/property-template.ts
index 7ed28aa..6f347ef 100644
--- a/src/property-template.ts
+++ b/src/property-template.ts
@@ -5,8 +5,8 @@ import { Config } from './config'
import { findLabel, removePrefixes } from './util'
const mappers: Record void> = {
- [`${PREFIX_SHACL}name`]: (template, term) => { const literal = term as Literal; if (!template.name || (template.config.attributes.language && literal.language === template.config.attributes.language)) { template.name = literal } },
- [`${PREFIX_SHACL}description`]: (template, term) => { const literal = term as Literal; if (!template.description || (template.config.attributes.language && literal.language === template.config.attributes.language)) { template.description = literal } },
+ [`${PREFIX_SHACL}name`]: (template, term) => { const literal = term as Literal; if (!template.name || literal.language === template.config.attributes.language) { template.name = literal } },
+ [`${PREFIX_SHACL}description`]: (template, term) => { const literal = term as Literal; if (!template.description || literal.language === template.config.attributes.language) { template.description = literal } },
[`${PREFIX_SHACL}path`]: (template, term) => { template.path = term.value },
[`${PREFIX_SHACL}node`]: (template, term) => { template.node = term as NamedNode },
[`${PREFIX_SHACL}datatype`]: (template, term) => { template.datatype = term as NamedNode },
diff --git a/src/property.ts b/src/property.ts
index 53a6f48..6c6c8cd 100644
--- a/src/property.ts
+++ b/src/property.ts
@@ -6,7 +6,7 @@ import { SHAPES_GRAPH } from './constants'
import { createShaclOrConstraint, resolveShaclOrConstraint } from './constraints'
import { Config } from './config'
import { ShaclPropertyTemplate } from './property-template'
-import { Editor, editorFactory, toRDF } from './editors'
+import { Editor, editorFactory, toRDF } from './theme'
export class ShaclProperty extends HTMLElement {
template: ShaclPropertyTemplate
@@ -43,12 +43,15 @@ export class ShaclProperty extends HTMLElement {
valuesContainHasValue = true
}
}
- if (this.template.hasValue && !valuesContainHasValue) {
+ if (config.editMode && this.template.hasValue && !valuesContainHasValue) {
this.addPropertyInstance(this.template.hasValue)
}
}
- this.addEventListener('change', () => { this.updateControls() })
- this.updateControls()
+
+ if (config.editMode) {
+ this.addEventListener('change', () => { this.updateControls() })
+ this.updateControls()
+ }
}
addPropertyInstance(value?: Term): HTMLElement {
diff --git a/src/theme.ts b/src/theme.ts
new file mode 100644
index 0000000..eb76978
--- /dev/null
+++ b/src/theme.ts
@@ -0,0 +1,260 @@
+import { DataFactory, Literal, NamedNode } from 'n3'
+import { Term } from '@rdfjs/types'
+import { PREFIX_SHACL, PREFIX_XSD, PREFIX_RDF } from './constants'
+import { createInputListEntries, findInstancesOf } from './util'
+import { ShaclPropertyTemplate } from './property-template'
+
+let idCtr = 0
+
+export type Editor = HTMLElement & { value: string }
+export type InputListEntry = { value: Term | string, label?: string }
+export abstract class Theme {
+ abstract createListEditor(template: ShaclPropertyTemplate, listEntries: InputListEntry[], value?: Term): HTMLElement
+ abstract createLangStringEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
+ abstract createTextEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
+ abstract createNumberEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
+ abstract createDateEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
+ abstract createBooleanEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement
+ createDefaultTemplate(template: ShaclPropertyTemplate, editor: Editor, value?: Term): HTMLElement {
+ editor.id = `e${idCtr++}`
+ editor.classList.add('editor', 'form-control')
+ if (template.datatype) {
+ // store datatype on editor, this is used for RDF serialization
+ editor['shacl-datatype'] = template.datatype
+ }
+ if (template.minCount !== undefined) {
+ editor.dataset.minCount = String(template.minCount)
+ }
+ if (template.class) {
+ editor.dataset.class = template.class.value
+ }
+ if (template.nodeKind) {
+ editor.dataset.nodeKind = template.nodeKind.value
+ }
+ editor.value = value?.value || template.defaultValue?.value || ''
+
+ const label = document.createElement('label')
+ label.htmlFor = editor.id
+ label.innerText = template.label
+ if (template.description) {
+ label.setAttribute('title', template.description.value)
+ }
+
+ const placeholder = template.description ? template.description.value : template.pattern ? template.pattern : null
+ if (placeholder) {
+ editor.setAttribute('placeholder', placeholder)
+ }
+ if (template.minCount !== undefined && template.minCount > 0) {
+ editor.setAttribute('required', 'true')
+ label.classList.add('required')
+ }
+
+ const result = document.createElement('div')
+ result.classList.add('property-instance')
+ result.appendChild(label)
+ result.appendChild(editor)
+ return result
+ }
+}
+export class NativeTheme extends Theme {
+ createDateEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ const editor = document.createElement('input')
+ if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
+ editor.type = 'datetime-local'
+ }
+ else {
+ editor.type = 'date'
+ }
+ editor.classList.add('pr-0')
+ const result = this.createDefaultTemplate(template, editor)
+ if (value) {
+ let isoDate = new Date(value.value).toISOString()
+ if (template.datatype?.value === PREFIX_XSD + 'dateTime') {
+ isoDate = isoDate.slice(0, 19)
+ } else {
+ isoDate = isoDate.slice(0, 10)
+ }
+ editor.value = isoDate
+ }
+ return result
+ }
+
+ createTextEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ let editor
+ if (template.singleLine === false) {
+ editor = document.createElement('textarea')
+ editor.rows = 5
+ }
+ else {
+ editor = document.createElement('input')
+ editor.type = 'text'
+ }
+
+ if (template.minLength) {
+ editor.minLength = template.minLength
+ }
+ if (template.maxLength) {
+ editor.maxLength = template.maxLength
+ }
+ if (template.pattern) {
+ editor.pattern = template.pattern
+ }
+ return this.createDefaultTemplate(template, editor, value)
+ }
+
+ createLangStringEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ const result = this.createTextEditor(template, value)
+ const editor = result.querySelector(':scope .editor') as Editor
+ let langChooser
+ if (template.languageIn?.length) {
+ langChooser = document.createElement('select')
+ for (const lang of template.languageIn) {
+ const option = document.createElement('option')
+ option.innerText = lang.value
+ langChooser.appendChild(option)
+ }
+ } else {
+ langChooser = document.createElement('input')
+ langChooser.maxLength = 5 // e.g. en-US
+ }
+ langChooser.title = 'Language of the text'
+ langChooser.placeholder = 'lang?'
+ langChooser.classList.add('lang-chooser')
+ // if lang chooser changes, fire a change event on the text input instead. this is for shacl validation handling.
+ langChooser.addEventListener('change', (ev) => {
+ ev.stopPropagation();
+ if (editor) {
+ editor.dataset.lang = langChooser.value
+ editor.dispatchEvent(new Event('change', { bubbles: true }))
+ }
+ })
+ if (value instanceof Literal) {
+ langChooser.value = value.language
+ }
+ editor.dataset.lang = langChooser.value
+ editor.after(langChooser)
+ return result
+ }
+
+ createBooleanEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ const editor = document.createElement('input')
+ editor.type = 'checkbox'
+ editor.classList.add('ml-0')
+
+ const result = this.createDefaultTemplate(template, editor, value)
+
+ // 'required' on checkboxes forces the user to tick the checkbox, which is not what we want here
+ editor.removeAttribute('required')
+ result.querySelector(':scope label')?.classList.remove('required')
+ if (value instanceof Literal) {
+ editor.checked = value.value === 'true'
+ }
+ return result
+ }
+
+ createNumberEditor(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ const editor = document.createElement('input')
+ editor.type = 'number'
+ editor.classList.add('pr-0')
+ const min = template.minInclusive ? template.minInclusive : template.minExclusive ? template.minExclusive + 1 : undefined
+ const max = template.maxInclusive ? template.maxInclusive : template.maxExclusive ? template.maxExclusive - 1 : undefined
+ if (min) {
+ editor.min = String(min)
+ }
+ if (max) {
+ editor.max = String(max)
+ }
+ if (template.datatype?.value !== PREFIX_XSD + 'integer') {
+ editor.step = '0.1'
+ }
+ return this.createDefaultTemplate(template, editor, value)
+ }
+
+ createListEditor(template: ShaclPropertyTemplate, listEntries: InputListEntry[], value?: Term): HTMLElement {
+ const editor = document.createElement('select')
+ const result = this.createDefaultTemplate(template, editor)
+ // add an empty element
+ const emptyOption = document.createElement('option')
+ emptyOption.value = ''
+ editor.options.add(emptyOption)
+
+ for (const item of listEntries) {
+ const option = document.createElement('option')
+ const itemValue = (typeof item.value === 'string') ? item.value : item.value.value
+ option.innerHTML = item.label ? item.label : itemValue
+ option.value = itemValue
+ if (value && value.value === itemValue) {
+ option.selected = true
+ }
+ editor.options.add(option)
+ }
+ if (value) {
+ editor.value = value.value
+ }
+ return result
+ }
+}
+
+export function toRDF(editor: Editor): Literal | NamedNode | undefined {
+ let datatype = editor['shacl-datatype']
+ let value: number | string = editor.value
+ if (value) {
+ if (editor.dataset.class || editor.dataset.nodeKind === PREFIX_SHACL + 'IRI') {
+ return DataFactory.namedNode(value)
+ } else {
+ if (editor.dataset.lang) {
+ datatype = editor.dataset.lang
+ }
+ else if (editor['type'] === 'number') {
+ value = parseFloat(value)
+ }
+ return DataFactory.literal(value, datatype)
+ }
+ } else if (editor['type'] === 'checkbox') {
+ // emit boolean 'false' only when required
+ if (editor['checked'] || parseInt(editor.dataset.minCount || '0') > 0) {
+ return DataFactory.literal(editor['checked'] ? 'true' : 'false', datatype)
+ }
+ }
+}
+
+export function editorFactory(template: ShaclPropertyTemplate, value?: Term): HTMLElement {
+ // if we have a class, find the instances and display them in a list
+ if (template.class) {
+ return template.config.theme.createListEditor(template, findInstancesOf(template.class, template.config), value)
+ }
+
+ // check if it is a list
+ if (template.shaclIn) {
+ const list = template.config.lists[template.shaclIn]
+ if (list?.length) {
+ return template.config.theme.createListEditor(template, createInputListEntries(list, template.config.shapesGraph, template.config.attributes.language), value)
+ }
+ else {
+ console.error('list not found:', template.shaclIn, 'existing lists:', template.config.lists)
+ }
+ }
+
+ // check if it is a langstring
+ if (template.datatype?.value === `${PREFIX_RDF}langString` || template.languageIn?.length) {
+ return template.config.theme.createLangStringEditor(template, value)
+ }
+
+ switch (template.datatype?.value.replace(PREFIX_XSD, '')) {
+ case 'string':
+ return template.config.theme.createTextEditor(template, value)
+ case 'integer':
+ case 'float':
+ case 'double':
+ case 'decimal':
+ return template.config.theme.createNumberEditor(template, value)
+ case 'date':
+ case 'dateTime':
+ return template.config.theme.createDateEditor(template, value)
+ case 'boolean':
+ return template.config.theme.createBooleanEditor(template, value)
+ }
+
+ // nothing found, fallback to 'text'
+ return template.config.theme.createTextEditor(template, value)
+}
diff --git a/src/util.ts b/src/util.ts
index 83c7349..975caf2 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,10 +1,10 @@
import { NamedNode, Prefixes, Quad, Store } from 'n3'
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'
import { Term } from '@rdfjs/types'
-import { InputListEntry } from './editors'
+import { InputListEntry } from './theme'
import { Config } from './config'
-export function findObjectValueByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, language?: string | null): string {
+export function findObjectValueByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, language?: string): string {
let result = ''
const object = findObjectByPredicate(quads, predicate, prefix, language)
if (object) {
@@ -13,7 +13,7 @@ export function findObjectValueByPredicate(quads: Quad[], predicate: string, pre
return result
}
-export function findObjectByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, language?: string | null): Term | undefined {
+export function findObjectByPredicate(quads: Quad[], predicate: string, prefix: string = PREFIX_SHACL, language?: string): Term | undefined {
let candidate: Term | undefined
const prefixedPredicate = prefix + predicate
for (const quad of quads) {
@@ -40,7 +40,7 @@ export function focusFirstInputElement(context: HTMLElement) {
(context.querySelector('input,select,textarea') as HTMLElement)?.focus()
}
-export function findLabel(quads: Quad[], language?: string | null): string {
+export function findLabel(quads: Quad[], language: string): string {
let label = findObjectValueByPredicate(quads, 'prefLabel', PREFIX_SKOS, language)
if (label) {
return label
@@ -48,7 +48,7 @@ export function findLabel(quads: Quad[], language?: string | null): string {
return findObjectValueByPredicate(quads, 'label', PREFIX_RDFS, language)
}
-export function createInputListEntries(subjects: Term[], shapesGraph: Store, language?: string | null): InputListEntry[] {
+export function createInputListEntries(subjects: Term[], shapesGraph: Store, language: string): InputListEntry[] {
const entries: InputListEntry[] = []
for (const subject of subjects) {
entries.push({ value: subject, label: findLabel(shapesGraph.getQuads(subject, null, null, null), language) })