├── .babelrc.json ├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── field.test.html ├── field.test.js ├── form.ttl ├── index.html ├── index.js ├── inline.html ├── inline.js ├── package.json ├── snowpack.config.js ├── snowpack.crt ├── snowpack.key └── tests │ ├── elements │ ├── checkbox.form.ttl │ ├── checkbox.ttl │ ├── dropdown.form.ttl │ ├── dropdown.ttl │ ├── reference.form.ttl │ ├── reference.ttl │ ├── unknown.form.ttl │ ├── unknown.ttl │ ├── url-image.form.ttl │ ├── url-image.ttl │ ├── url-uppy.form.ttl │ ├── url-uppy.ttl │ ├── wysiwyg.form.ttl │ └── wysiwyg.ttl │ ├── example.form.ttl │ └── example.ttl ├── package-lock.json ├── package.json ├── src ├── Translations │ ├── RdfForm.en.js │ └── RdfForm.nl.js ├── core │ ├── Comunica.ts │ ├── Debug.ts │ ├── FormDefinition.ts │ ├── JsonLdProxy.ts │ ├── Language.ts │ ├── RdfFormData.ts │ ├── Registry.ts │ ├── Renderer.ts │ └── i18n.ts ├── declaration.d.ts ├── elements │ ├── Checkbox.ts │ ├── Color.ts │ ├── Container.ts │ ├── Date.ts │ ├── Details.ts │ ├── Dropdown.ts │ ├── ElementBase.ts │ ├── Group.ts │ ├── LanguagePicker.ts │ ├── Mail.ts │ ├── Number.ts │ ├── Reference.ts │ ├── String.ts │ ├── Textarea.ts │ ├── Unknown.ts │ ├── Url.ts │ ├── UrlImage.ts │ ├── UrlUppy.ts │ ├── WYSIWYG.ts │ └── Wrapper.ts ├── helpers │ ├── applyProxy.ts │ ├── attributesDiff.ts │ ├── containerProxy.ts │ ├── createPixelArray.ts │ ├── dbpediaSuggestions.ts │ ├── debounce.ts │ ├── expand.ts │ ├── fa.ts │ ├── flatMapProxy.ts │ ├── getImage.ts │ ├── getImageColor.ts │ ├── getImageDimensionsByUrl.ts │ ├── getUriMeta.ts │ ├── icons.ts │ ├── importGlobalScript.ts │ ├── isFetchable.ts │ ├── kebabize.ts │ ├── lastPart.ts │ ├── onlyUnique.ts │ ├── searchSuggestionsSparqlQuery.ts │ ├── sparqlQueryToList.ts │ └── streamToString.ts ├── index.ts ├── languages.ts ├── plugins.ts ├── scss │ ├── _layout.scss │ ├── components │ │ ├── _actions.scss │ │ ├── _base.scss │ │ ├── _button.scss │ │ ├── _checkbox-label.scss │ │ ├── _form-element.scss │ │ ├── _items.scss │ │ ├── _pell.scss │ │ ├── _rdf-form.scss │ │ ├── _search-suggestions.scss │ │ ├── _select.scss │ │ ├── _slim-select.scss │ │ └── _uppy.scss │ ├── display-only.scss │ ├── elements │ │ ├── _checkbox.scss │ │ ├── _color.scss │ │ ├── _container.scss │ │ ├── _details.scss │ │ ├── _duration.scss │ │ ├── _group.scss │ │ ├── _language-picker.scss │ │ ├── _password.scss │ │ ├── _reference.scss │ │ ├── _url-image.scss │ │ └── _wysiwyg.scss │ └── rdf-form.scss ├── types │ ├── CoreComponent.ts │ ├── ElementInstance.ts │ ├── ExpandedJsonLdObject.ts │ ├── Field.ts │ └── Form.ts └── vendor │ ├── ProxyHandlerStatic-browser.js │ ├── comunica-browser.js │ ├── fontawesome-svg-core.js │ ├── pell.js │ ├── slimselect.js │ └── ttl2jsonld.js ├── test └── blah.test.ts ├── tsconfig.json └── tsdx.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "targets": { 4 | "esmodules": true 5 | } 6 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniel Beeke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Documentation for RDF form 2 | 3 | RDF form provides rendered forms via a form definition provided in RDF turtle format. It is easy to serialize to JSON-ld or turtle. This form builder is extendable and is build as generic as possible. 4 | 5 | See [rdf-form.danielbeeke.nl](http://rdf-form.danielbeeke.nl/) for documentation. 6 | 7 | ![screenshot][screenshot] 8 | 9 | [screenshot]: https://raw.githubusercontent.com/danielbeeke/rdf-form/master/screenshot/progress.png "Screenshot of progress" 10 | -------------------------------------------------------------------------------- /demo/field.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RDF Form 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/field.test.js: -------------------------------------------------------------------------------- 1 | import 'rdf-form' 2 | 3 | const fieldType = location.hash.substr(1) ? location.hash.substr(1) : 'unknown' 4 | 5 | const form = document.createElement('rdf-form') 6 | form.setAttribute('form', `/tests/elements/${fieldType}.form.ttl`) 7 | // form.setAttribute('debug', null) 8 | form.setAttribute('data', `/tests/elements/${fieldType}.ttl`) 9 | // form.setAttribute('proxy', 'https://thingproxy.freeboard.io/fetch/') 10 | 11 | form.addEventListener('submit', (event) => { 12 | console.log(event.detail) 13 | }) 14 | 15 | form.addEventListener('file-deleted', (event) => { 16 | console.log('file-deleted', event.detail) 17 | }) 18 | 19 | form.addEventListener('file-added', (event) => { 20 | console.log('file-added', event.detail) 21 | }) 22 | 23 | form.addEventListener('dropdown-options', (event) => { 24 | const options = event.detail.options 25 | 26 | options.push({ 27 | value: 'Lorem', 28 | label: 'Test', 29 | jsonldKey: 'value' 30 | }) 31 | 32 | console.log(options) 33 | }) 34 | 35 | 36 | document.body.appendChild(form) 37 | -------------------------------------------------------------------------------- /demo/form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix rdfs: . 3 | @prefix form: . 4 | @prefix owl: . 5 | @prefix dcterms: . 6 | @prefix vann: . 7 | 8 | form: 9 | rdf:type owl:Ontology ; 10 | dcterms:title "Form ontology"@en ; 11 | dcterms:description "An ontology for adding form definitions to Ontologies."@en ; 12 | vann:preferredNamespacePrefix "form" ; 13 | vann:preferredNamespaceURI . 14 | 15 | form:Form 16 | a rdfs:Class ; 17 | rdfs:label "Form"@en ; 18 | rdfs:comment 19 | "A form definition for an owl class. Use [RDF-form](https://github.com/danielbeeke/rdf-form) to render the form or to read documentation."@en . 20 | 21 | form:SubForm 22 | a rdfs:Class ; 23 | rdfs:label "Subform"@en ; 24 | rdfs:comment 25 | "A subform definition for an owl class. Use [RDF-form](https://github.com/danielbeeke/rdf-form) to render the form or to read documentation."@en . 26 | 27 | form:Field 28 | a rdfs:Class ; 29 | rdfs:label "Field"@en ; 30 | rdfs:comment "A form field."@en . 31 | 32 | form:Container 33 | a rdfs:Class ; 34 | rdfs:label "Container"@en ; 35 | rdfs:comment "A container of fields."@en . 36 | 37 | form:UiComponent 38 | a rdfs:Class ; 39 | rdfs:label "UI component"@en ; 40 | rdfs:comment "A UI component of RDF form."@en . 41 | 42 | form:label 43 | rdf:type rdf:Property ; 44 | rdfs:comment "The label to display above the field."@en ; 45 | rdfs:label "Label text"@en . 46 | 47 | form:cssClass 48 | rdf:type rdf:Property ; 49 | rdfs:comment "Extra CSS class."@en ; 50 | rdfs:label "CSS Class"@en . 51 | 52 | form:saveColor 53 | rdf:type rdf:Property ; 54 | rdfs:comment "Save the dominant color to the image object."@en ; 55 | rdfs:label "Save color"@en . 56 | 57 | form:widget 58 | rdf:type rdf:Property ; 59 | rdfs:comment 60 | "The field widget, the widget that will be displayed. It may be a dropdown, radios, a map to select a location or something else. You can also implement your own."@en ; 61 | rdfs:label "Field widget"@en . 62 | 63 | form:order 64 | rdf:type rdf:Property ; 65 | rdfs:comment "The position of the field in the form. This will be used to sort the fields. No order means 0."@en ; 66 | rdfs:label "Order"@en . 67 | 68 | form:required 69 | rdf:type rdf:Property ; 70 | rdfs:comment 71 | "The minimum items that are required or true for single fields. So you can give a boolean or a number."@en ; 72 | rdfs:label "Required"@en . 73 | 74 | form:multiple 75 | rdf:type rdf:Property ; 76 | rdfs:comment "This field may have multiple items."@en ; 77 | rdfs:label "Multiple"@en . 78 | 79 | form:alternatives 80 | rdf:type rdf:Property ; 81 | rdfs:comment "This field may have alternative values for one item. THis should be used in use with multiple."@en ; 82 | rdfs:label "Alternatives"@en . 83 | 84 | form:disabled 85 | rdf:type rdf:Property ; 86 | rdfs:comment "This field is disabled."@en ; 87 | rdfs:label "Disabled"@en . 88 | 89 | form:range 90 | rdf:type rdf:Property ; 91 | rdfs:comment "You can use this for the duration field. Use any of the letters from the ISO-8601, include the T so the parsers works correctly. Use THMS do display hours, minutes, and seconds. PYMDTHMS"@en ; 92 | rdfs:label "Disabled"@en . 93 | 94 | form:rows 95 | rdf:type rdf:Property ; 96 | rdfs:comment "The number of rows for a textarea. This is mapped directly to the html rows attribute."@en ; 97 | rdfs:label "Rows"@en . 98 | 99 | form:binding 100 | rdf:type rdf:Property ; 101 | form:isBindingProperty true ; 102 | rdfs:comment 103 | "The predicate(s) this field maps to. Some widgets have multiple bindings, like a geo location or an address field. Just supply them all."@en ; 104 | rdfs:label "Binding(s)"@en . 105 | 106 | form:readonly 107 | rdf:type rdf:Property ; 108 | rdfs:comment "This field is read only."@en ; 109 | rdfs:label "Read-only"@en . 110 | 111 | form:additionalBindings 112 | rdf:type rdf:Property ; 113 | rdfs:comment 114 | "The predicate(s) this field maps to. Some widgets have multiple bindings, like a geo location or an address field. Just supply them all."@en ; 115 | rdfs:label "Additional binding(s)"@en . 116 | 117 | form:innerBinding 118 | rdf:type rdf:Property ; 119 | form:isBindingProperty true ; 120 | rdfs:comment 121 | "A predicate that wraps the contents ."@en ; 122 | rdfs:label "Wrapper binding"@en . 123 | 124 | form:type 125 | rdf:type rdf:Property ; 126 | rdfs:comment 127 | "A type that the wrapper has ."@en ; 128 | rdfs:label "Type"@en . 129 | 130 | form:innerType 131 | rdf:type rdf:Property ; 132 | rdfs:comment 133 | "A type that the wrapper has ."@en ; 134 | rdfs:label "Wrapper type"@en . 135 | 136 | form:translatable 137 | rdf:type rdf:Property ; 138 | rdfs:comment 139 | "If the field is translatable. This will allow translations. It will start untranslated, but the user may add translations."@en ; 140 | rdfs:label "Translatable"@en . 141 | 142 | form:autoCompleteSource 143 | rdf:type rdf:Property ; 144 | rdfs:comment 145 | "A Comunica source for the autocomplete suggestions query. See [Comunica source types](https://comunica.dev/docs/query/advanced/source_types/)."@en ; 146 | rdfs:label "Autocomplete source"@en . 147 | 148 | form:autoCompleteQuery 149 | rdf:type rdf:Property ; 150 | rdfs:comment 151 | "Sparql query for autocomplete suggestions. Comunica is used to query. See [Comunica](https://comunica.dev)."@en ; 152 | rdfs:label "Autocomplete query"@en . 153 | 154 | form:optionsSource 155 | rdf:type rdf:Property ; 156 | rdfs:comment 157 | "A Comunica source for the options query. See [Comunica source types](https://comunica.dev/docs/query/advanced/source_types/)."@en ; 158 | rdfs:label "Options source"@en . 159 | 160 | form:optionsQuery 161 | rdf:type rdf:Property ; 162 | rdfs:comment "Sparql query for options. Comunica is used to query. See [Comunica](https://comunica.dev)."@en ; 163 | rdfs:label "Options query"@en . 164 | 165 | form:emptyText 166 | rdf:type rdf:Property ; 167 | rdfs:comment 168 | "The text to display when the field is empty. This is used for the first option of a dropdown field."@en ; 169 | rdfs:label "Empty text"@en . 170 | 171 | form:description 172 | rdf:type rdf:Property ; 173 | rdfs:comment "The text to display below the field."@en ; 174 | rdfs:label "Description text"@en . 175 | 176 | form:placeholder 177 | rdf:type rdf:Property ; 178 | rdfs:comment "The text to display when the text field is empty as a placeholder."@en ; 179 | rdfs:label "Placeholder text"@en . 180 | 181 | form:group 182 | rdf:type rdf:Property ; 183 | rdfs:comment "Place fields within this type to have a nested field group that may be repeated."@en ; 184 | rdfs:label "Field group"@en . 185 | 186 | form:open 187 | rdf:type rdf:Property ; 188 | rdfs:comment "The HTML open attribute."@en ; 189 | rdfs:label "Open"@en . 190 | 191 | form:saveEmptyValue 192 | rdf:type rdf:Property ; 193 | rdfs:comment "Useful for checkboxes if you want to save the false state also."@en ; 194 | rdfs:label "Save empty value"@en . 195 | 196 | form:container 197 | rdf:type rdf:Property ; 198 | rdfs:comment "Place fields within this to have a visual container."@en ; 199 | rdfs:label "Container"@en . 200 | 201 | form:containerWidget 202 | rdf:type rdf:Property ; 203 | rdfs:comment 204 | "The container widget, it may be collapsible or something alike."@en ; 205 | rdfs:label "Container widget"@en . 206 | 207 | form:region 208 | rdf:type rdf:Property ; 209 | rdfs:comment 210 | "The region the container will be placed in."@en ; 211 | rdfs:label "Region"@en . 212 | 213 | form:value 214 | rdf:type rdf:Property ; 215 | rdfs:comment "Used for option list values."@en ; 216 | rdfs:label "Value"@en . 217 | 218 | form:option 219 | rdf:type rdf:Property ; 220 | rdfs:comment "Used for option list values."@en ; 221 | rdfs:label "Option"@en . 222 | 223 | form:subform 224 | rdf:type rdf:Property ; 225 | rdfs:comment "Used for including a subform."@en ; 226 | rdfs:label "Subform"@en . 227 | 228 | form:jsonLdKey 229 | rdf:type rdf:Property ; 230 | rdfs:comment "Will this field save URI's or text values?."@en ; 231 | rdfs:label "JSON-ld key"@en . 232 | 233 | form:dimensions 234 | rdf:type rdf:Property ; 235 | rdfs:comment "Save meta data aside from the image."@en ; 236 | form:isBindingProperty true ; 237 | rdfs:label "Save meta"@en . 238 | 239 | form:focalPoint 240 | rdf:type rdf:Property ; 241 | rdfs:comment "Save a focal point."@en ; 242 | form:isBindingProperty true ; 243 | rdfs:label "Focal point"@en . 244 | 245 | form:access 246 | rdf:type rdf:Property ; 247 | rdfs:comment "Access roles."@en ; 248 | rdfs:label "Access roles"@en . 249 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RDF Form 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import 'rdf-form' 2 | 3 | const form = document.createElement('rdf-form') 4 | form.setAttribute('form', `/tests/example.form.ttl`) 5 | form.setAttribute('debug', null) 6 | // form.setAttribute('proxy', 'https://thingproxy.freeboard.io/fetch/') 7 | form.setAttribute('ui-languages', JSON.stringify({"en": "English", "nl": "Nederlands"})) 8 | form.setAttribute('data', `/tests/example.ttl`) 9 | form.setAttribute('selected-l10n-language', `nl`) 10 | form.setAttribute('selected-language', `nl`) 11 | 12 | form.addEventListener('indexing-languages', (event) => { 13 | event.detail.languages.push(['lorem', 'Lorem']) 14 | event.detail.languages.push(['rmy-x-bsa', 'South Slavic Ardilean Bayash']) 15 | event.detail.languages.push(['nl-Latn', 'Nederlands']) 16 | event.detail.languages.push(['nl-Cyrl', 'Nederlands']) 17 | event.detail.languages.push(['sr-latn-x-mltlngl-rmy-x-vg', 'Serbian multilingual Vlach Romani']) 18 | event.detail.languages.push(['rmy-x-vg-x-mltlngl-rmy-x-vg', 'Vlach Romani multilingual']) 19 | }) 20 | 21 | form.addEventListener('submit', (event) => { 22 | console.log(event.detail) 23 | }) 24 | 25 | document.body.appendChild(form) 26 | -------------------------------------------------------------------------------- /demo/inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RDF Form 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/inline.js: -------------------------------------------------------------------------------- 1 | import 'rdf-form' 2 | 3 | const form = document.createElement('rdf-form') 4 | form.setAttribute('form', ` 5 | @prefix rdf: . 6 | @prefix rdfs: . 7 | @prefix form: . 8 | @prefix ex: . 9 | @prefix dc: . 10 | @prefix : . 11 | 12 | :form a form:Form ; 13 | form:binding ex:Todo . 14 | 15 | :todo 16 | a form:Field ; 17 | form:binding ex:items ; 18 | form:widget "group" ; 19 | form:multiple true ; 20 | form:label "Todo items" ; 21 | form:required true ; 22 | form:order 1 . 23 | 24 | :todoItem 25 | a form:Field ; 26 | form:binding ex:task ; 27 | form:widget "string" ; 28 | form:group "todo" ; 29 | form:label "Task" ; 30 | form:required true ; 31 | form:order 1 . 32 | 33 | :todoCheck 34 | a form:Field ; 35 | form:binding ex:completed ; 36 | form:widget "checkbox" ; 37 | form:group "todo" ; 38 | form:label "Done" ; 39 | form:order 2 . 40 | `) 41 | form.setAttribute('data', ` 42 | { 43 | "@type": "https://example.org/#Todo", 44 | "https://example.org/#items": { 45 | "@list": [ 46 | { 47 | "https://example.org/#completed": true, 48 | "https://example.org/#task": "Wake up" 49 | }, 50 | { 51 | "https://example.org/#completed": true, 52 | "https://example.org/#task": "Drink coffee" 53 | }, 54 | { 55 | "https://example.org/#task": "Breakfast" 56 | }, 57 | { 58 | "https://example.org/#task": "Save the world" 59 | } 60 | ] 61 | } 62 | } 63 | `) 64 | 65 | form.addEventListener('submit', (event) => { 66 | console.log(event.detail) 67 | }) 68 | 69 | document.body.appendChild(form) -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "snowpack dev" 4 | } 5 | } -------------------------------------------------------------------------------- /demo/snowpack.config.js: -------------------------------------------------------------------------------- 1 | // Snowpack Configuration File 2 | // See all supported options: https://www.snowpack.dev/reference/configuration 3 | 4 | /** @type {import("snowpack").SnowpackUserConfig } */ 5 | module.exports = { 6 | mount: { 7 | '.': '/', 8 | }, 9 | plugins: [ 10 | /* ... */ 11 | ], 12 | packageOptions: { 13 | /* ... */ 14 | }, 15 | devOptions: { 16 | port: 8070, 17 | secure: true 18 | }, 19 | buildOptions: { 20 | /* ... */ 21 | }, 22 | workspaceRoot: '../' 23 | }; -------------------------------------------------------------------------------- /demo/snowpack.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEMzCCApugAwIBAgIQW6rO9TIjN6LgHo9u1t8pwTANBgkqhkiG9w0BAQsFADB5 3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJzAlBgNVBAsMHmRhbmll 4 | bEBkYW5pZWwtR0w1MDNWTSAoRGFuaWVsKTEuMCwGA1UEAwwlbWtjZXJ0IGRhbmll 5 | bEBkYW5pZWwtR0w1MDNWTSAoRGFuaWVsKTAeFw0yMjAxMTcxNTM2MDFaFw0yNDA0 6 | MTcxNDM2MDFaMFIxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZp 7 | Y2F0ZTEnMCUGA1UECwweZGFuaWVsQGRhbmllbC1HTDUwM1ZNIChEYW5pZWwpMIIB 8 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzge9efPVyXIoZTXEdLs+7ehi 9 | 4WoPT6t8Hm364fTNUIIfLFFzsLSY6G1ooBzGU/82SOUqy/kLIFQdy7s+f0/nP7cv 10 | iwyIDVsgcDzI6T1dnacIlaWlI2xMJFQk+8T0UvqMT6KAmF3BulFCFIpU4RXsIOJg 11 | QCUTVctMnVci41mhPrL1mjZAyYyjV9ycHg/8kLO04Wip/wfvAd+p8dbH3VfRhgdu 12 | Dlq/M9MDQg+9b1rm4vfHE76prQUZVwv2lqtdMDif0uDM4aI+P48VqRu3xH8M59fr 13 | K8zAfhygTG64yHcAo1S9vt36CUDSARdN0lhmREqy91WBev28ig8z2JjBN3uPxwID 14 | AQABo14wXDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD 15 | VR0jBBgwFoAUNaz7RgvTDCMWOhonjHuyxdC1aWQwFAYDVR0RBA0wC4IJbG9jYWxo 16 | b3N0MA0GCSqGSIb3DQEBCwUAA4IBgQApL2MxhFc7+PrbIiU7IvKbMlZgDzVZVENq 17 | FpkeAsA8g03t/lo12S1mx/iO50Ii4XpX5CmMiVROUu1TRsgyJbiYjRzz1b/9QjNx 18 | TzLLe2NVixTlY1xhe9qonOvk5fISx0hMaFymCgGRwWbN97cnEtZZjzktbIW8HoAx 19 | AO86EFWbw2dt7dgQcepvnkOn3SM0bLiVALkbX1eZQFXy7mW9PBIK/AAut/S3C4y3 20 | zi2FqtzM8OL5ahUc15jfupYrSODZAHOIc4sGkL3BdcHO3wxKfgCAviQQJVrpxePj 21 | ZrLBBlNbh4+1iP1OwJRaQGGr5nV+I/M5R2+Ya8O131T7r5TZ0WrGdIVKTNnXjqa8 22 | tdn/UjEyMRV7wSIpFZwrfDhucD8k2jdfdGsJOOONmvp4Pbn9gviZI6a0kzVDHVMO 23 | GZ4mx5pAIzkZPkdYP1Jv6lTaO+edsdpcLa0iEPidBqLEXTa1z/jVbnbR1zgCLew0 24 | x3PSyKFGpuV3I+1rt8o0LBJoTyrtJdE= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /demo/snowpack.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOB71589XJcihl 3 | NcR0uz7t6GLhag9Pq3webfrh9M1Qgh8sUXOwtJjobWigHMZT/zZI5SrL+QsgVB3L 4 | uz5/T+c/ty+LDIgNWyBwPMjpPV2dpwiVpaUjbEwkVCT7xPRS+oxPooCYXcG6UUIU 5 | ilThFewg4mBAJRNVy0ydVyLjWaE+svWaNkDJjKNX3JweD/yQs7ThaKn/B+8B36nx 6 | 1sfdV9GGB24OWr8z0wNCD71vWubi98cTvqmtBRlXC/aWq10wOJ/S4Mzhoj4/jxWp 7 | G7fEfwzn1+srzMB+HKBMbrjIdwCjVL2+3foJQNIBF03SWGZESrL3VYF6/byKDzPY 8 | mME3e4/HAgMBAAECggEBAKb09sKAqdoYvEtoCs9dfV1lV9u7CrVRDb/K9+drbYW0 9 | LHbJeqrTbqXLI2G8b6tZwS/JJaktI6sK/yo9jiO1KHwlgk131jicg+jwGQ+JPvem 10 | h/pcxTmzZPB7j4zGygcEKffOg871Cyxk9NuYTbgo/7SWPdE9OjHoESnfltINq/EV 11 | uFgxSVce72K1CUjj+JYe9qKK+8qWlwWRCZLTb7xQm3LCl8g80I8V92EwuxYSEJtm 12 | Sv9xhcm6xyAKVmPPEp0aaRIf16OvsyApP+c4mtbDPNf9yBWSD8QmuKBhxLrTBYDE 13 | Vmmpt5v8/hYrjrqG4X/n345uq5dfjMI0ABIVqytCRMECgYEA4mjtRJke0+Xzen9P 14 | 4lEMYMsT2RpqwMHs0JmLokRapLidSBdawFFKqzE431LqhG3mO918B2HS34HGsa9O 15 | wbd/yyGgHsL1JITRN5hfFOg862thJaPCmHK0m1FoGzpk3HphA2x/lxaXc7+u13qr 16 | PeSA+IDsWtYAyGfnRwa5IssECMkCgYEA6PT2/jFGqCkj8Jjr3UVERqQK0l7AJkQ6 17 | lIeuuSjsZroao3k5X5noC5l8jUn57PfhGOI/mdBBPfcLbq0k2lj60iAY7rv6QFnU 18 | yvSGj57bK3BrbwmyeidVSxyQZTsfilBufzH0t8wiY0+wDM4Te/wE4vFH4XPm0WE4 19 | 0u8dd72YrA8CgYBs9tzCOANLLg9pNB6JKEKRzwrFYN5h2LMVjeBS/xy0zBj+GidW 20 | CYmrLGxXprsxcwbsZuMLVnw7j2TGHT4FI0BAzfUW+PMsWTOr0wxnroGrN6mwiMjd 21 | v87GNX6qJAdoyQkpsa0SVRAc5/LIx8PkbLXZY4rdCMOlr8PyPf0aDqTpaQKBgA2g 22 | 1xpD07hev8WBjLrjJH1ld2SbOm6Cq1KpJWWbqUjRNmG948dd/58+GXVCkKZ2Uerc 23 | wY/ECS0Q2NBevLsxXWsRiaPdx2QgXTyKVZztVDEUYJScYp6W0nyUbTYe4Vd8IRq7 24 | 128xOAnLTadSHv2v3rFQID5mQ2iYYXSlnHm208mtAoGBAKuDKhJMFy4NJCodTv1j 25 | pfUe7zEkghI+UnXDFBiKgoPUbRmfTv9THBSvJGSmRYtYxzT8TwcfChqim6RGb1dh 26 | Bo1i0R2aaaSdTawDM/Qb0yl0bL5ovoArFLruBvBhGNZDxWu/XUBRxx+wVKEHl4CO 27 | U4qv3lXfKWiW/m0fdG8U47q2 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /demo/tests/elements/checkbox.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix fieldsForm: . 4 | @prefix schema: . 5 | @prefix recipe: . 6 | 7 | 8 | form:binding schema:Thing ; 9 | a form:Form . 10 | 11 | fieldsForm:language 12 | a form:UiComponent ; 13 | form:widget "language-picker" ; 14 | form:order 1 . 15 | 16 | 17 | # fieldsForm:accept 18 | # a form:Field ; 19 | # form:widget "checkbox" ; 20 | # form:label "Accept"@en ; 21 | # form:binding recipe:accept ; 22 | # form:translatable "always" ; 23 | # form:order 1 . 24 | 25 | 26 | # fieldsForm:confirm 27 | # a form:Field ; 28 | # form:widget "checkbox" ; 29 | # form:label "Confirm"@en ; 30 | # form:binding recipe:confirm ; 31 | # form:translatable "always" ; 32 | 33 | # form:order 2 . 34 | 35 | fieldsForm:confirm2 36 | a form:Field ; 37 | form:widget "checkbox" ; 38 | form:label "Confirm"@en ; 39 | form:binding recipe:confirm2 ; 40 | form:translatable "always" ; 41 | 42 | form:order 2 . 43 | -------------------------------------------------------------------------------- /demo/tests/elements/checkbox.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | @prefix focalPoint: . 5 | 6 | [] 7 | a schema:Thing ; 8 | recipe:accept "true"@en, 9 | "true"@fr ; 10 | recipe:confirm2 true . 11 | 12 | -------------------------------------------------------------------------------- /demo/tests/elements/dropdown.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix fieldsForm: . 4 | @prefix schema: . 5 | @prefix recipe: . 6 | 7 | 8 | form:binding schema:Thing ; 9 | a form:Form . 10 | 11 | 12 | 13 | fieldsForm:options2 14 | a form:Field ; 15 | form:widget "dropdown" ; 16 | form:option ([ 17 | form:value "test" ; 18 | form:label "Test"@en ; 19 | ] [ 20 | form:value "test1" ; 21 | form:label "Test 1"@en ; 22 | ]) ; 23 | form:label "Static code dropdown"@en ; 24 | form:binding recipe:confirm1 ; 25 | form:order 2 . 26 | 27 | fieldsForm:options 28 | a form:Field ; 29 | form:widget "dropdown" ; 30 | form:label "Dynamic code dropdown"@en ; 31 | form:binding recipe:confirm ; 32 | form:order 2 . 33 | -------------------------------------------------------------------------------- /demo/tests/elements/dropdown.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | @prefix focalPoint: . 5 | 6 | [] 7 | a schema:Thing ; 8 | recipe:accept true ; 9 | recipe:confirm false . 10 | 11 | -------------------------------------------------------------------------------- /demo/tests/elements/reference.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix fieldsForm: . 4 | @prefix schema: . 5 | @prefix recipe: . 6 | @prefix focalPoint: . 7 | 8 | 9 | form:binding schema:Thing ; 10 | a form:Form . 11 | 12 | fieldsForm:ingredient 13 | a form:Field ; 14 | form:widget "reference" ; 15 | form:label "Ingredient"@en ; 16 | form:label "Ingrediënt"@nl ; 17 | form:option ([ 18 | form:value "*" ; 19 | form:label "Magic Ingredient"@en ; 20 | ]) ; 21 | form:binding recipe:ingredient ; 22 | form:placeholder "Search for an ingredient."@en ; 23 | form:placeholder "Zoek naar een ingredient."@nl ; 24 | form:multiple true ; 25 | form:autoCompleteQuery """ 26 | 27 | PREFIX rdfs: 28 | PREFIX dbo: 29 | PREFIX bif: 30 | 31 | SELECT DISTINCT ?uri ?label ?image { 32 | 33 | ?o dbo:ingredient ?uri . 34 | ?uri rdfs:label ?label . 35 | ?uri dbo:thumbnail ?image . 36 | ?label bif:contains "'SEARCH_TERM'" . 37 | 38 | filter langMatches(lang(?label), "LANGUAGE") 39 | } 40 | 41 | LIMIT 10 42 | 43 | """ ; 44 | form:order 1 . 45 | 46 | 47 | fieldsForm:translationOfWork 48 | a form:Field ; 49 | form:widget "reference" ; 50 | form:label "Original work"@en ; 51 | form:label "Het orgineel"@nl ; 52 | form:binding schema:translationOfWork ; 53 | form:order 4 . 54 | -------------------------------------------------------------------------------- /demo/tests/elements/reference.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | @prefix focalPoint: . 5 | 6 | [] 7 | a schema:Thing ; 8 | recipe:ingredient . -------------------------------------------------------------------------------- /demo/tests/elements/unknown.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix fieldsForm: . 4 | @prefix schema: . 5 | @prefix recipe: . 6 | 7 | 8 | form:binding schema:Thing ; 9 | a form:Form . 10 | 11 | fieldsForm:accept 12 | a form:Field ; 13 | form:widget "unknown" ; 14 | form:label "Accept"@en ; 15 | form:binding recipe:accept ; 16 | form:order 1 . 17 | -------------------------------------------------------------------------------- /demo/tests/elements/unknown.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | @prefix focalPoint: . 5 | 6 | [] 7 | a schema:Thing ; 8 | recipe:accept true ; 9 | recipe:confirm false . 10 | 11 | -------------------------------------------------------------------------------- /demo/tests/elements/url-image.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix personForm: . 4 | @prefix schema: . 5 | @prefix focalPoint: . 6 | 7 | 8 | form:binding schema:Person ; 9 | a form:Form . 10 | 11 | personForm:language 12 | a form:UiComponent ; 13 | form:widget "language-picker" ; 14 | form:order 0 . 15 | 16 | personForm:imageWrapper 17 | a form:Container ; 18 | form:widget "container" ; 19 | form:binding schema:image ; 20 | form:open true ; 21 | form:label "Images"@en ; 22 | form:label "Afbeeldingen"@nl . 23 | 24 | 25 | personForm:image 26 | a form:Field ; 27 | form:container "imageWrapper" ; 28 | form:widget "url-image" ; 29 | form:label "Image"@en ; 30 | form:label "Afbeelding"@nl ; 31 | form:type schema:ImageObject ; 32 | form:binding schema:url ; 33 | form:saveColor true ; 34 | form:focalPoint focalPoint:x1, focalPoint:y1, focalPoint:x2, focalPoint:y2 ; 35 | form:dimensions schema:width, schema:height ; 36 | form:order 3 ; 37 | form:multiple true ; 38 | form:translatable true . 39 | -------------------------------------------------------------------------------- /demo/tests/elements/url-image.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix focalPoint: . 4 | 5 | [] 6 | a schema:Person ; 7 | schema:jobTitle "Professor" ; 8 | schema:name "Jane Doe" ; 9 | schema:givenName "Jane" ; 10 | schema:familyName "Doe" ; 11 | schema:telephone "(425) 123-4567" ; 12 | schema:image ( [ 13 | a schema:ImageObject ; 14 | focalPoint:x1 20 ; 15 | focalPoint:y1 20 ; 16 | focalPoint:x2 80 ; 17 | focalPoint:y2 80 ; 18 | schema:width 100 ; 19 | schema:height 100 ; 20 | schema:url "https://images.unsplash.com/photo-1491349174775-aaafddd81942?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80"@en 21 | ] [ 22 | a schema:ImageObject ; 23 | focalPoint:x1 10 ; 24 | focalPoint:y1 10 ; 25 | focalPoint:x2 90 ; 26 | focalPoint:y2 90 ; 27 | schema:width 120 ; 28 | schema:height 120 ; 29 | schema:url "https://images.unsplash.com/photo-1654776017274-5ba5f3acac6b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=930&q=80"@fr 30 | ] ) ; 31 | schema:url . 32 | -------------------------------------------------------------------------------- /demo/tests/elements/url-uppy.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix urls3Form: . 4 | @prefix schema: . 5 | @prefix focalPoint: . 6 | 7 | 8 | form:binding schema:Person ; 9 | a form:Form . 10 | 11 | 12 | urls3Form:language 13 | a form:UiComponent ; 14 | form:widget "language-picker" ; 15 | form:order 1 . 16 | 17 | # urls3Form:languageTabs 18 | # a form:UiComponent ; 19 | # form:widget "language-tabs" . 20 | 21 | # urls3Form:url 22 | # a form:Field ; 23 | # form:widget "url-uppy" ; 24 | # form:uppyCompanion "http://localhost:8080/companion/" ; 25 | # form:uppyDomain "https://example.com/test" ; 26 | # form:label "Download"@en ; 27 | # form:label "Download"@nl ; 28 | # form:binding schema:url ; 29 | # form:order 3 ; 30 | # form:multiple true ; 31 | # form:translatable true . 32 | 33 | urls3Form:imageWrapper 34 | a form:Container ; 35 | form:widget "container" ; 36 | form:order 7 ; 37 | form:binding schema:image . 38 | 39 | urls3Form:image 40 | a form:Field ; 41 | form:widget "url-uppy" ; 42 | form:label "Image"@en ; 43 | form:uppyCompanion "http://localhost:8080/companion/" ; 44 | form:uppyDomain "https://example.com" ; 45 | form:label "Afbeelding"@nl ; 46 | form:type schema:ImageObject ; 47 | form:container "imageWrapper" ; 48 | form:binding schema:url ; 49 | form:saveColor true ; 50 | form:focalPoint focalPoint:x1, focalPoint:y1, focalPoint:x2, focalPoint:y2 ; 51 | form:dimensions schema:width, schema:height ; 52 | form:order 7 ; 53 | form:multiple true . 54 | 55 | # urls3Form:imageWrapper2 56 | # a form:Container ; 57 | # form:widget "container" ; 58 | # form:order 7 . 59 | 60 | # urls3Form:image2 61 | # a form:Field ; 62 | # form:widget "url-uppy" ; 63 | # form:label "Image2"@en ; 64 | # form:uppyCompanion "http://localhost:8080/companion/" ; 65 | # form:uppyDomain "https://example.com" ; 66 | # form:label "Afbeelding"@nl ; 67 | # form:container "imageWrapper2" ; 68 | # form:binding schema:url2 ; 69 | # form:saveColor true ; 70 | # form:order 7 ; 71 | # form:multiple true . 72 | -------------------------------------------------------------------------------- /demo/tests/elements/url-uppy.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix focalPoint: . 4 | 5 | [] 6 | a schema:Person ; 7 | 8 | schema:image ([ 9 | schema:url "https://danielbeeke.nl/images/daniel.jpg"@en ; 10 | schema:width 100 ; 11 | schema:height 100 ; 12 | ][ 13 | schema:url "https://images.unsplash.com/photo-1643575102128-0d6b42fbdda1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2064&q=80"@nl ; 14 | schema:width 100 ; 15 | schema:height 100 ; 16 | focalPoint:x1 47 ; 17 | focalPoint:y1 43 ; 18 | focalPoint:x2 95 ; 19 | focalPoint:y2 86 ; 20 | ]) . 21 | 22 | -------------------------------------------------------------------------------- /demo/tests/elements/wysiwyg.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdfs: . 2 | @prefix form: . 3 | @prefix fieldsForm: . 4 | @prefix schema: . 5 | @prefix recipe: . 6 | 7 | 8 | form:binding schema:Thing ; 9 | a form:Form . 10 | 11 | fieldsForm:accept 12 | a form:Field ; 13 | form:widget "wysiwyg" ; 14 | form:label "Content"@en ; 15 | form:binding recipe:text ; 16 | form:order 1 . 17 | -------------------------------------------------------------------------------- /demo/tests/elements/wysiwyg.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | @prefix focalPoint: . 5 | 6 | [] 7 | a schema:Thing . 8 | # recipe:text """

Lorem ipsum

""" . 9 | 10 | -------------------------------------------------------------------------------- /demo/tests/example.form.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix rdfs: . 3 | @prefix recipe: . 4 | @prefix owl: . 5 | @prefix dcterms: . 6 | @prefix vann: . 7 | @prefix form: . 8 | @prefix recipeForm: . 9 | @prefix schema: . 10 | 11 | 12 | form:binding schema:Thing ; 13 | a form:Form . 14 | 15 | recipeForm:language 16 | a form:UiComponent ; 17 | form:widget "language-picker" ; 18 | form:order 1 . 19 | 20 | # recipeForm:languageTabs 21 | # a form:UiComponent ; 22 | # form:widget "language-tabs" . 23 | 24 | recipeForm:title 25 | a form:Field ; 26 | form:widget "string" ; 27 | form:label "Title"@en ; 28 | form:label "Titel"@nl ; 29 | form:binding schema:name ; 30 | form:order 1 ; 31 | form:translatable true ; 32 | form:required true . 33 | 34 | recipeForm:ingredients 35 | a form:Field ; 36 | form:widget "group" ; 37 | form:label "Ingredients"@en ; 38 | form:label "Ingrediënten"@nl ; 39 | form:binding recipe:ingredients ; 40 | form:multiple true ; 41 | form:order 2 . 42 | 43 | recipeForm:ingredient 44 | a form:Field ; 45 | form:widget "reference" ; 46 | form:label "Ingredient"@en ; 47 | form:label "Ingrediënt"@nl ; 48 | form:group "ingredients" ; 49 | form:required true ; 50 | form:binding recipe:ingredient ; 51 | form:placeholder "Search for an ingredient."@en ; 52 | form:placeholder "Zoek naar een ingredient."@nl ; 53 | form:autoCompleteQuery """ 54 | PREFIX rdfs: 55 | PREFIX dbo: 56 | PREFIX bif: 57 | SELECT DISTINCT ?uri ?label ?image { 58 | ?o dbo:ingredient ?uri . 59 | ?uri rdfs:label ?label . 60 | ?uri dbo:thumbnail ?image . 61 | ?label bif:contains "'SEARCH_TERM'" . 62 | filter langMatches(lang(?label), "LANGUAGE") 63 | } 64 | LIMIT 10 65 | """ ; 66 | form:order 1 . 67 | 68 | recipeForm:quantity 69 | a form:Field ; 70 | form:widget "number" ; 71 | form:label "Quantity"@en ; 72 | form:label "Aantal"@nl ; 73 | form:required true ; 74 | form:binding recipe:quantity ; 75 | form:group "ingredients" ; 76 | form:order 2 . 77 | 78 | recipeForm:measurementUnit 79 | a form:Field ; 80 | form:widget "dropdown" ; 81 | form:label "Unit"@en ; 82 | form:label "Soort"@nl ; 83 | form:jsonLdKey "id" ; 84 | form:required true ; 85 | form:binding recipe:measurementUnit ; 86 | form:option [ form:label "- Select -"@en ; 87 | form:label "- Selecteer -"@nl ; 88 | form:value "" ; ] ; 89 | form:option [ form:label "Litre"@en ; 90 | form:label "Liter"@nl ; 91 | form:value ; ] ; 92 | form:option [ form:label "Millilitre"@en ; 93 | form:label "Milliliter"@nl ; 94 | form:value ; ] ; 95 | form:group "ingredients" ; 96 | form:order 3 . 97 | 98 | 99 | recipeForm:instructions 100 | a form:Field ; 101 | form:widget "textarea" ; 102 | form:label "Instructions"@en ; 103 | form:label "Instructies"@nl ; 104 | form:binding recipe:instructions ; 105 | form:rows 6 ; 106 | form:order 3 ; 107 | form:multiple true ; 108 | form:translatable true ; 109 | form:required true . 110 | 111 | 112 | # recipeForm:duration 113 | # a form:Field ; 114 | # form:widget "duration" ; 115 | # form:label "Duration"@en ; 116 | # form:label "Tijdsduur"@nl ; 117 | # form:binding recipe:duration ; 118 | # form:range "THM" ; 119 | # form:order 4 ; 120 | # form:required true . 121 | 122 | 123 | recipeForm:author 124 | form:widget "reference" ; 125 | form:label "Author"@en ; 126 | form:label "Auteur"@nl ; 127 | form:binding recipe:author ; 128 | form:order 5 . -------------------------------------------------------------------------------- /demo/tests/example.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix xsd: . 3 | @prefix recipe: . 4 | 5 | [] 6 | a schema:Thing ; 7 | schema:name "Soup recipe"@en ; 8 | schema:name "Soep recept"@nl ; 9 | recipe:ingredients ( [ recipe:ingredient ; 10 | recipe:measurementUnit ; 11 | recipe:quantity 1 ; ] 12 | [ recipe:ingredient ; 13 | recipe:measurementUnit ; 14 | recipe:quantity 350 ; ] 15 | [ recipe:ingredient ; 16 | recipe:measurementUnit ; 17 | recipe:quantity 2 ; ] 18 | [ recipe:ingredient ; 19 | recipe:measurementUnit ; 20 | recipe:quantity 2 ; ] ) . -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.28", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src", 9 | "scss", 10 | "demo" 11 | ], 12 | "engines": { 13 | "node": ">=10" 14 | }, 15 | "scripts": { 16 | "demo": "cd demo && npm run start", 17 | "start": "tsdx watch --format esm --noClean", 18 | "build": "NODE_ENV=production tsdx build --format cjs,esm,umd", 19 | "test": "tsdx test", 20 | "lint": "tsdx lint", 21 | "prepare": "tsdx build --format cjs,esm,umd", 22 | "size": "size-limit", 23 | "analyze": "size-limit --why" 24 | }, 25 | "peerDependencies": { 26 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 27 | "@fortawesome/free-regular-svg-icons": "^5.15.1", 28 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 29 | "@frogcat/ttl2jsonld": "^0.0.6", 30 | "cypress": "^7.0.1", 31 | "jsonld": "^5.2.0", 32 | "n3": "^1.6.4", 33 | "quantize": "^1.0.2", 34 | "tinyduration": "^3.2.0", 35 | "uhtml": "*" 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "tsdx lint" 40 | } 41 | }, 42 | "prettier": { 43 | "printWidth": 80, 44 | "semi": true, 45 | "singleQuote": true, 46 | "trailingComma": "es5" 47 | }, 48 | "name": "rdf-form", 49 | "author": "Daniel Beeke", 50 | "module": "dist/rdf-form.esm.js", 51 | "size-limit": [ 52 | { 53 | "path": "dist/rdf-form.cjs.production.min.js", 54 | "limit": "10 KB" 55 | }, 56 | { 57 | "path": "dist/rdf-form.esm.js", 58 | "limit": "10 KB" 59 | } 60 | ], 61 | "devDependencies": { 62 | "@size-limit/preset-small-lib": "^7.0.5", 63 | "fast-glob": "^3.2.11", 64 | "husky": "^7.0.4", 65 | "rollup-plugin-scss": "^3.0.0", 66 | "sass": "^1.49.0", 67 | "size-limit": "^7.0.5", 68 | "snowpack": "^3.8.8", 69 | "tsdx": "^0.14.1", 70 | "tslib": "^2.3.1", 71 | "typescript": "^4.5.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Translations/RdfForm.en.js: -------------------------------------------------------------------------------- 1 | export let Translations = {} 2 | -------------------------------------------------------------------------------- /src/Translations/RdfForm.nl.js: -------------------------------------------------------------------------------- 1 | export let Translations = { 2 | 'Save': 'Opslaan', 3 | 'Add translation': 'Vertaling toevoegen', 4 | 'Add item': 'Onderdeel toevoegen', 5 | 'Remove translations': 'Vertalingen verwijderen', 6 | 'Create translation': 'Vertalen', 7 | 'Remove item': 'Verwijder onderdeel', 8 | 'Loading...': 'Laden...', 9 | 'Interface language': 'Interface taal', 10 | 'Hours': 'Uren', 11 | 'Minutes': 'Minuten', 12 | 'Seconds': 'Seconden', 13 | 'Years': 'Jaren', 14 | 'Weeks': 'Weken', 15 | 'Days': 'Dagen', 16 | '- Select a value -': '- Selecteer een waarde -', 17 | 'Months': 'Maanden', 18 | 'Add {searchTerm} as text without reference.': 'Voeg {searchTerm} toe als tekst zonder een referentie.', 19 | } 20 | -------------------------------------------------------------------------------- /src/core/Comunica.ts: -------------------------------------------------------------------------------- 1 | import { Comunica } from '../vendor/comunica-browser.js' 2 | export const newEngine = Comunica.newEngine -------------------------------------------------------------------------------- /src/core/Debug.ts: -------------------------------------------------------------------------------- 1 | export const expandProxiesInConsole = () => { 2 | const originalConsoleLog = console.log 3 | console.log = (...rawInputs) => { 4 | const inputs = [...rawInputs] 5 | for (let [index, input] of inputs.entries()) { 6 | if (input?.isProxy) inputs[index] = input.$ 7 | } 8 | originalConsoleLog(...inputs) 9 | } 10 | } -------------------------------------------------------------------------------- /src/core/FormDefinition.ts: -------------------------------------------------------------------------------- 1 | import { ttl2jsonld } from '../vendor/ttl2jsonld' 2 | import { expand as JsonLdExpand } from 'jsonld' 3 | import { ExpandedJsonLdObject } from '../types/ExpandedJsonLdObject' 4 | import { lastPart } from '../helpers/lastPart' 5 | import { expand } from '../helpers/expand' 6 | import { CoreComponent } from '../types/CoreComponent' 7 | import { JsonLdProxy } from './JsonLdProxy' 8 | import { Language } from './Language' 9 | import { applyProxy } from '../helpers/applyProxy' 10 | import { isFetchable } from '../helpers/isFetchable' 11 | 12 | export const only = (...type) => { 13 | return (item: ExpandedJsonLdObject) => item['@type']?.some(rdfClass => type.includes(lastPart(rdfClass))) 14 | } 15 | 16 | export class FormDefinition extends EventTarget implements CoreComponent { 17 | 18 | private formAsTextOrUrl: string 19 | private sourceDefinitionCompacted: object = {} 20 | private sourceDefinitionExpanded: Array 21 | public context = { form: '' } 22 | public ready: boolean = false 23 | public chain = new Map() 24 | public chainReferences = new Map() 25 | private ontology: Array = [] 26 | protected roles: Array 27 | protected form: any 28 | 29 | constructor (form: any) { 30 | super() 31 | this.form = form 32 | this.formAsTextOrUrl = this.form.getAttribute('form') 33 | if (!this.formAsTextOrUrl) throw new Error('No data attribute "form" was found on the custom element.') 34 | this.init() 35 | } 36 | 37 | async init () { 38 | const proxy = this.form.getAttribute('proxy') ?? '' 39 | this.roles = this.form.getAttribute('roles') ? this.form.getAttribute('roles').split(',') : [] 40 | let definitionTurtle 41 | 42 | if (isFetchable(this.formAsTextOrUrl)) { 43 | const definitionResponse = await fetch(applyProxy(this.formAsTextOrUrl, proxy), { 44 | method: 'GET', 45 | headers: { 46 | 'Accept' : 'text/turtle' 47 | } 48 | }) 49 | definitionTurtle = await definitionResponse.text() 50 | } 51 | else { 52 | definitionTurtle = this.formAsTextOrUrl 53 | } 54 | 55 | this.sourceDefinitionCompacted = ttl2jsonld(definitionTurtle) 56 | Object.assign(this.context, this.sourceDefinitionCompacted['@context']) 57 | if (!this.context.form) { 58 | this.context.form = 'http://rdf.danielbeeke.nl/form/form-dev.ttl#'; 59 | } 60 | if (!this.sourceDefinitionCompacted['@graph']) throw new Error('Missing fields inside form definition') 61 | this.sourceDefinitionExpanded = JsonLdProxy(await JsonLdExpand(this.sourceDefinitionCompacted), this.context, { 62 | '_': (value) => Language.multilingualValue(value, 'ui') 63 | }) 64 | await this.resolveSubForms(this.sourceDefinitionExpanded) 65 | if (!this.info) throw new Error('The form definition did not define a form itself.') 66 | 67 | /** @ts-ignore */ 68 | const ontologyCompacted = await fetch(applyProxy(this.context.form, proxy)).then(async response => ttl2jsonld(await response.text())) 69 | Object.assign(this.context, ontologyCompacted['@context']) 70 | this.ontology = JsonLdProxy(await JsonLdExpand(ontologyCompacted), this.context) 71 | this.chain = this.createChain() 72 | this.ready = true 73 | this.dispatchEvent(new CustomEvent('ready')) 74 | } 75 | 76 | get prefix () { 77 | return this.context.form 78 | } 79 | 80 | get info () { 81 | return this.sourceDefinitionExpanded.find(only('Form')) 82 | } 83 | 84 | get fieldsToRemove () { 85 | const formRemovals = JSON.parse(this.form.getAttribute('fields-to-remove') ?? '[]') 86 | return [...(this.info['form:remove']?.map(item => item._) ?? []), ...formRemovals].map(collapsed => expand(collapsed, this.context)) ?? [] 87 | } 88 | 89 | get fields (): Array { 90 | return this.sourceDefinitionExpanded.filter(only('Field')) 91 | .filter(field => !this.fieldsToRemove.includes(field['@id'])) 92 | } 93 | 94 | get elements (): Array { 95 | return this.sourceDefinitionExpanded.filter(only('Field', 'Container', 'UiComponent')) 96 | .filter(field => !this.fieldsToRemove.includes(field['@id'])) 97 | } 98 | 99 | async resolveSubForms (formDefinition) { 100 | const proxy = this.form.getAttribute('proxy') ?? '' 101 | const fields = formDefinition.filter(only('Field')) 102 | 103 | for (const field of fields) { 104 | const subformUrl = field['form:subform'] 105 | 106 | if (subformUrl?.length > 1) throw new Error('Multiple sub forms were found for one field.') 107 | 108 | if (subformUrl) { 109 | const subformResponse = await fetch(applyProxy(subformUrl._, proxy)) 110 | const subformTurtle = await subformResponse.text() 111 | const subformDefinitionCompacted = ttl2jsonld(subformTurtle) 112 | const subformDefinitionExpanded = JsonLdProxy(await JsonLdExpand(subformDefinitionCompacted), subformDefinitionCompacted['@context'], { 113 | '_': (value) => Language.multilingualValue(value, 'ui') 114 | }); 115 | 116 | await this.resolveSubForms(subformDefinitionExpanded) 117 | 118 | Object.assign(this.context, subformDefinitionCompacted['@context']) 119 | 120 | // Some properties may be inherit from the parent, such as container and order. 121 | for (const subFormfield of subformDefinitionExpanded) { 122 | if (field['form:container']) { 123 | subFormfield['form:container'] = field['form:container'].$ 124 | } 125 | 126 | if (field['form:order']?._) { 127 | subFormfield['form:order'] = [{ '@value': (field['form:order']?._ ?? 0) + parseFloat('0.' + subFormfield['form:order']?._)}] 128 | } 129 | } 130 | 131 | const fieldIndex = formDefinition.map(field => field.$).indexOf(field.$) 132 | formDefinition.$.splice(fieldIndex, 1, ...subformDefinitionExpanded.map(field => field.$)) 133 | } 134 | } 135 | 136 | return formDefinition 137 | } 138 | 139 | applyFieldAccessRoles (fields: Array) { 140 | return fields.filter(field => { 141 | if (field['form:access']) { 142 | return this.roles.some(userRole => field['form:access'].map(role => role['@id']).includes(userRole)) 143 | } 144 | return true 145 | }) 146 | } 147 | 148 | createChain () { 149 | const recursiveChainCreator = (fields) => { 150 | const chain = new Map() 151 | 152 | fields.sort((a, b) => (a['form:order']?._ ?? 0) - (b['form:order']?._ ?? 0)) 153 | 154 | for (const field of fields) { 155 | const fieldBindings = this.getBindingsOfField(field) 156 | let children = [] 157 | if (field['form:widget']?._ === 'group' || lastPart(field['@type'][0]) === 'Container') { 158 | const nestingType = field['form:widget']?._ === 'group' ? 'group' : 'container' 159 | /** @ts-ignore */ 160 | children = this.applyFieldAccessRoles(this.elements.filter(innerField => innerField?.[`form:${nestingType}`]?._ === lastPart(field['@id']))) 161 | } 162 | 163 | chain.set(fieldBindings.length ? fieldBindings : field.$, [field, recursiveChainCreator(children)]) 164 | } 165 | 166 | return chain 167 | } 168 | 169 | const firstLevelFields = this.applyFieldAccessRoles(this.elements.filter(field => !field['form:group'] && !field['form:container'])) 170 | return recursiveChainCreator(firstLevelFields) 171 | } 172 | 173 | getBindingsOfField (field: object) { 174 | const bindings = [] 175 | // Goes through all the fields properties, check which items have bindings. 176 | for (const [fieldProperty, propertySetting] of Object.entries(field)) { 177 | const fieldMetaProperties = this.ontology.find(predicate => lastPart(predicate?.['@id']) === lastPart(fieldProperty)) 178 | if (fieldMetaProperties && fieldMetaProperties['form:isBindingProperty'] && Array.isArray(propertySetting)) { 179 | /* @ts-ignore */ 180 | bindings.push(...propertySetting.$.flatMap(item => item['@id'])) 181 | } 182 | } 183 | return bindings 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/core/JsonLdProxy.ts: -------------------------------------------------------------------------------- 1 | import { lastPart } from '../helpers/lastPart' 2 | 3 | export const JsonLdProxy = (data, context, extraCommands: { [key: string]: (value) => any} = {}, defaultAlias: string | null = null) => { 4 | if (typeof data !== 'object') return data 5 | 6 | const convertProp = (prop) => { 7 | if (prop.toString().includes(':')) { 8 | const propSplit = prop.toString().split(':') 9 | if (context?.[propSplit[0]]) { 10 | prop = prop.toString().replace(propSplit[0] + ':', context[propSplit[0]]) 11 | } 12 | } 13 | 14 | return prop 15 | } 16 | 17 | return new Proxy(data, { 18 | get(target, prop, receiver) { 19 | if (prop === '_proxyType') return 'JsonLdProxy' 20 | prop = convertProp(prop) 21 | if (prop === '$' && !('$' in extraCommands)) return target 22 | if (prop === '_alias') return defaultAlias 23 | if (prop === '_' && !('_' in extraCommands)) { 24 | const getFirst = (thing) => Array.isArray(thing) ? getFirst(thing[0]) : thing?.['@id'] ?? thing?.['@value'] ?? thing 25 | return JsonLdProxy(getFirst(target), context, extraCommands, defaultAlias) 26 | } 27 | if (prop === 'isProxy') return true 28 | 29 | for (const [command, callback] of Object.entries(extraCommands)) { 30 | if (prop === command) return callback(target) 31 | } 32 | 33 | if (prop[0] === '*') { 34 | const lastPartToFind = prop.toString().substr(1) 35 | for (const key of Object.keys(target)) { 36 | if (lastPart(key) === lastPartToFind) { 37 | prop = key 38 | } 39 | } 40 | } 41 | 42 | const isOurProperty = !Reflect.has({}, prop) && !Reflect.has([], prop) && Reflect.has(target, prop) 43 | 44 | if (defaultAlias && !prop.toString().includes(':') && !Reflect.has({}, prop) && !Reflect.has([], prop)) { 45 | const newProp = convertProp(defaultAlias + ':' + prop.toString()) 46 | const isOurProperty = !Reflect.has({}, newProp) && !Reflect.has([], newProp) && Reflect.has(target, newProp) 47 | if (isOurProperty && Reflect.has(target, newProp)) { 48 | return JsonLdProxy(target[newProp], context, extraCommands, defaultAlias) 49 | } 50 | } 51 | 52 | if (target[prop]?.[0]?.['@list'] && isOurProperty) { 53 | return JsonLdProxy(target[prop][0]['@list'], context, extraCommands, defaultAlias) 54 | } 55 | 56 | if (isOurProperty && target[prop]) { 57 | return JsonLdProxy(target[prop], context, extraCommands, defaultAlias) 58 | } 59 | 60 | if (['filter'].includes(prop.toString())) { 61 | const requestedMethod = Reflect.get(target, prop, receiver) 62 | return (...input) => { 63 | return requestedMethod.apply(target.map(item => JsonLdProxy(item, context, extraCommands, defaultAlias)), input) 64 | } 65 | } 66 | 67 | return Reflect.get(target, prop, receiver) 68 | }, 69 | 70 | set (target, prop, value) { 71 | prop = convertProp(prop) 72 | target[prop] = value 73 | return true 74 | } 75 | }) 76 | } -------------------------------------------------------------------------------- /src/core/Language.ts: -------------------------------------------------------------------------------- 1 | import { CoreComponent } from '../types/CoreComponent' 2 | import { I18n } from './i18n' 3 | import { languages } from '../languages' 4 | 5 | /** 6 | * Fetches languages according to BCP47 7 | * 8 | * Provides a way to get language names by langCodes. 9 | */ 10 | 11 | export const getLanguageLabel = async (langCode) => { 12 | const langCodeParts = langCode.split('-') 13 | 14 | const testParts: Array = [] 15 | const testers: Array = [] 16 | 17 | for (const langCodePart of langCodeParts) { 18 | testParts.push(langCodePart) 19 | testers.push(testParts.join('-')) 20 | } 21 | 22 | testers.reverse() 23 | 24 | let languageLabel = '' 25 | for (const tester of testers) { 26 | if (!languageLabel) { 27 | languageLabel = languages.find(language => language[0].startsWith(tester))?.[1] ?? '' 28 | } 29 | } 30 | 31 | const scriptMapping = { 32 | 'latn': t.direct('Latin').toString(), 33 | 'cyrl': t.direct('Cyrillic').toString() 34 | } 35 | 36 | let hasScript = '' 37 | for (const script of Object.keys(scriptMapping)) { 38 | if (langCodeParts.some(item => item.toLowerCase() === script)) { 39 | hasScript = `${scriptMapping[script.toLowerCase()]}` 40 | } 41 | } 42 | 43 | return `${languageLabel} ${hasScript}`.trim() 44 | } 45 | 46 | export const filterLanguages = async (search) => { 47 | if (!search) return [] 48 | return languages.filter(language => language[1].toLowerCase().includes(search.toLowerCase())) 49 | } 50 | 51 | export const langCodesToObject = async (langCodes: Array) => { 52 | const languages = {} 53 | for (const langCode of langCodes) { 54 | languages[langCode] = await getLanguageLabel(langCode) 55 | } 56 | return languages 57 | } 58 | 59 | /** 60 | * Language service 61 | * The t function get be imported to do translations. 62 | * Using it as template literal with t`Lorem Ipsum` returns a Hole for uHtml, 63 | * Using t.direct('Lorem Ipsum') returns a string. 64 | */ 65 | 66 | let currentUiLanguage = 'en' 67 | let currentL10nLanguage: string 68 | let uiLanguages: object = { 'en': 'English' } 69 | let l10nLanguages: object = { 'en': 'English' } 70 | let requiredL10nLanguages = [] 71 | 72 | export class LanguageService extends EventTarget implements CoreComponent { 73 | 74 | public ready: boolean = false 75 | 76 | constructor () { 77 | super() 78 | } 79 | 80 | async init (rdfForm) { 81 | await this.setUiLanguage('en') 82 | 83 | this.dispatchEvent(new CustomEvent('indexing-languages', { detail: { languages }})) 84 | 85 | const continueInit = async () => { 86 | const usedLanguages = await this.extractUsedLanguages(rdfForm.formData.proxy) 87 | 88 | const defaultLanguages = JSON.parse(rdfForm.getAttribute('languages')) ?? ( 89 | usedLanguages.length ? await langCodesToObject(usedLanguages) : {} 90 | ) 91 | const parsedLanguages = JSON.parse(rdfForm.getAttribute('l10n-languages')) 92 | this.l10nLanguages = Object.assign({}, parsedLanguages, defaultLanguages) 93 | 94 | if (rdfForm.getAttribute('required-l10n-languages')) { 95 | requiredL10nLanguages = rdfForm.getAttribute('required-l10n-languages').split(',') 96 | } 97 | 98 | if (rdfForm.getAttribute('selected-l10n-language') && rdfForm.getAttribute('selected-l10n-language').toLowerCase() in this.l10nLanguages) { 99 | this.l10nLanguage = rdfForm.getAttribute('selected-l10n-language').toLowerCase() 100 | } 101 | 102 | this.uiLanguages = JSON.parse(rdfForm.getAttribute('ui-languages')) ?? {} 103 | await this.setUiLanguage(rdfForm.getAttribute('selected-language') ?? 'en') 104 | 105 | this.ready = true 106 | this.dispatchEvent(new CustomEvent('ready')) 107 | } 108 | 109 | rdfForm.formData.ready ? continueInit() : rdfForm.formData.addEventListener('ready', continueInit, { once: true}) 110 | } 111 | 112 | get requiredL10nLanguages (): Array { 113 | return requiredL10nLanguages 114 | } 115 | 116 | get uiLanguage () { 117 | return currentUiLanguage 118 | } 119 | 120 | async setUiLanguage (languageCode) { 121 | currentUiLanguage = languageCode 122 | t = await I18n(languageCode, 'RdfForm', Object.keys(this.uiLanguages), 'en') 123 | this.dispatchEvent(new CustomEvent('language-change')) 124 | } 125 | 126 | set l10nLanguage (langCode) { 127 | currentL10nLanguage = langCode 128 | this.dispatchEvent(new CustomEvent('l10n-change', { 129 | detail: langCode 130 | })) 131 | } 132 | 133 | get l10nLanguage () { 134 | return currentL10nLanguage 135 | } 136 | 137 | set l10nLanguages (languages) { 138 | const oldLanguageCodes = Object.keys(l10nLanguages) 139 | const newLanguageCodes = Object.keys(languages) 140 | 141 | let languageCodesToAdd = newLanguageCodes.filter(x => !oldLanguageCodes.includes(x)); 142 | let languageCodesToDelete = oldLanguageCodes.filter(x => !newLanguageCodes.includes(x)); 143 | 144 | if (languageCodesToDelete.includes(currentL10nLanguage)) { 145 | currentL10nLanguage = newLanguageCodes[0] 146 | } 147 | 148 | for (const langCode of languageCodesToAdd) { 149 | this.dispatchEvent(new CustomEvent('this.added', { 150 | detail: langCode 151 | })) 152 | } 153 | 154 | for (const langCode of languageCodesToDelete) { 155 | this.dispatchEvent(new CustomEvent('this.removed', { 156 | detail: langCode 157 | })) 158 | } 159 | 160 | l10nLanguages = languages 161 | if (!currentL10nLanguage) { 162 | currentL10nLanguage = Object.keys(l10nLanguages)[0] 163 | } 164 | } 165 | 166 | get l10nLanguages () { 167 | return l10nLanguages 168 | } 169 | 170 | set uiLanguages (languages) { 171 | uiLanguages = languages 172 | } 173 | 174 | get uiLanguages () { 175 | return uiLanguages 176 | } 177 | 178 | /** 179 | * Helper function to extract a language value of a RDF quad/triple. 180 | * @param values 181 | */ 182 | multilingualValue (values, type = 'ui') { 183 | if (!Array.isArray(values)) values = [values] 184 | const currentLanguageMatch = values.find(value => value['@language'] === (type === 'ui' ? this.uiLanguage : this.l10nLanguage)) 185 | const fallbackNoLanguageMatch = values.find(value => !value['@language']) 186 | return currentLanguageMatch?.['@value'] ?? fallbackNoLanguageMatch?.['@value'] ?? 187 | currentLanguageMatch?.['@id'] ?? fallbackNoLanguageMatch?.['@id'] 188 | } 189 | 190 | /** 191 | * Extracts the used languages of JSON-ld. 192 | * @param jsonLd 193 | */ 194 | extractUsedLanguages (jsonLd: object): Array { 195 | const languageCodes = new Set() 196 | 197 | const process = (thing) => { 198 | if (!thing) return 199 | const iterateble = Array.isArray(thing) ? thing.entries() : Object.entries(thing) 200 | 201 | for (const [key, value] of iterateble) { 202 | if (key === '@language') languageCodes.add(value) 203 | if (typeof value !== 'string') process(value) 204 | } 205 | } 206 | 207 | process(jsonLd) 208 | 209 | return [...languageCodes.values()] 210 | } 211 | } 212 | 213 | export const Language = new LanguageService() 214 | export let t: any 215 | 216 | -------------------------------------------------------------------------------- /src/core/RdfFormData.ts: -------------------------------------------------------------------------------- 1 | import { CoreComponent } from '../types/CoreComponent' 2 | import { ttl2jsonld } from '../vendor/ttl2jsonld' 3 | import { expand as JsonLdExpand } from 'jsonld' 4 | import { JsonLdProxy } from './JsonLdProxy' 5 | import { Language } from './Language' 6 | import { isFetchable } from '../helpers/isFetchable' 7 | import { FormDefinition } from './FormDefinition' 8 | import { RdfForm } from '..' 9 | import { applyProxy } from '../helpers/applyProxy' 10 | 11 | export class RdfFormData extends EventTarget implements CoreComponent { 12 | 13 | public ready: boolean = false 14 | private dataAsTextOrUrl: string | null 15 | private sourceData: any 16 | public get: () => any 17 | public proxy = { $: null } 18 | protected formDefinition: FormDefinition 19 | private sourceDataCompacted: object 20 | protected form: RdfForm 21 | 22 | constructor (form: RdfForm) { 23 | super() 24 | this.form = form 25 | this.formDefinition = this.form.formDefinition 26 | this.dataAsTextOrUrl = this.form.getAttribute('data') 27 | 28 | this.formDefinition.addEventListener('ready', () => this.init(), { once: true }) 29 | } 30 | 31 | async init () { 32 | const proxy = this.form.getAttribute('proxy') ?? '' 33 | let dataText 34 | if (!this.dataAsTextOrUrl) this.sourceData = [] 35 | 36 | if (this.dataAsTextOrUrl && isFetchable(this.dataAsTextOrUrl)) { 37 | const dataResponse = await fetch(applyProxy(this.dataAsTextOrUrl, proxy), { 38 | method: 'GET', 39 | headers: { 40 | 'Accept': 'application/ld+json' 41 | } 42 | }) 43 | dataText = await dataResponse.text() 44 | } 45 | else { 46 | dataText = this.dataAsTextOrUrl 47 | } 48 | 49 | try { 50 | this.sourceDataCompacted = JSON.parse(dataText) 51 | } 52 | catch (e) { 53 | this.sourceDataCompacted = ttl2jsonld(dataText) 54 | } 55 | 56 | this.sourceData = await JsonLdExpand(this.sourceDataCompacted); 57 | 58 | if (Array.isArray(this.sourceData)) this.sourceData = this.sourceData.pop() 59 | 60 | 61 | // The new empty object. 62 | if (!this.sourceData) this.sourceData = {} 63 | if (!this.sourceData?.['@type']) this.sourceData['@type'] = this.formDefinition.info['form:binding'].map(rdfClass => rdfClass['@id']) 64 | 65 | this.createProxy() 66 | this.ready = true 67 | this.dispatchEvent(new CustomEvent('ready')) 68 | } 69 | 70 | get context () { 71 | return Object.assign({}, this.formDefinition.context, this.sourceDataCompacted?.['@context']) 72 | } 73 | 74 | createProxy () { 75 | const context = this.context 76 | this.proxy = JsonLdProxy(this.sourceData, context, { 77 | '_': (value) => Language.multilingualValue(value, 'l10n') 78 | }) 79 | } 80 | } -------------------------------------------------------------------------------- /src/core/Registry.ts: -------------------------------------------------------------------------------- 1 | import { CoreComponent } from '../types/CoreComponent' 2 | import { ElementInstance } from '../types/ElementInstance' 3 | import { RdfForm } from '..' 4 | import BaseFields from '../plugins' 5 | 6 | export class Registry extends EventTarget implements CoreComponent { 7 | 8 | public ready: boolean = false 9 | private registeredFieldClasses = {} 10 | private form: RdfForm 11 | 12 | constructor (rdfForm: RdfForm) { 13 | super() 14 | this.form = rdfForm 15 | this.init() 16 | } 17 | 18 | async init () { 19 | const event = new CustomEvent('register-elements', { detail: { fields: [] } }) 20 | this.form.dispatchEvent(event) 21 | Object.assign(this.registeredFieldClasses, BaseFields, event.detail.fields) 22 | this.ready = true 23 | this.dispatchEvent(new CustomEvent('ready')) 24 | } 25 | 26 | async setupElement (definition, bindings: Array, value = null, itemValues = {}, parentValues = null, render: any = () => null, parent, index: number | null = null, children = []): Promise { 27 | const widget = definition['form:widget']?._ && this.registeredFieldClasses[definition['form:widget']?._] ? definition['form:widget']._ : 'unknown' 28 | let elementClass = this.registeredFieldClasses[widget] 29 | 30 | if (!elementClass) { 31 | console.log(this.registeredFieldClasses) 32 | throw new Error('Unknown widget type: ' + definition['form:widget']?._) 33 | } 34 | return new elementClass(definition, bindings, value, itemValues, parentValues, render, parent, index, children) 35 | } 36 | } -------------------------------------------------------------------------------- /src/core/Renderer.ts: -------------------------------------------------------------------------------- 1 | import { render, html } from 'uhtml/async' 2 | 3 | import { CoreComponent } from '../types/CoreComponent' 4 | import { ElementInstance } from '../types/ElementInstance' 5 | import { Registry } from './Registry' 6 | import { lastPart } from '../helpers/lastPart' 7 | import { flatMapProxy } from '../helpers/flatMapProxy' 8 | import { containerProxy } from '../helpers/containerProxy' 9 | import { t, Language } from './Language' 10 | import { RdfForm } from '..' 11 | import RdfFormCss from '../scss/rdf-form.scss' 12 | import OnlyDisplay from '../scss/display-only.scss' 13 | 14 | export class Renderer extends EventTarget implements CoreComponent { 15 | public ready: boolean = false 16 | private fieldInstances: Map = new Map() 17 | protected form: RdfForm 18 | public extraStylesheets = new Set() 19 | 20 | constructor (rdfForm: RdfForm) { 21 | super() 22 | this.init() 23 | this.form = rdfForm 24 | 25 | if (this.form.getAttribute('extra-stylesheets')) { 26 | const urls = this.form.getAttribute('extra-stylesheets')?.split(',') 27 | if (urls) { 28 | for (const url of urls) { 29 | this.extraStylesheets.add(url) 30 | } 31 | } 32 | } 33 | } 34 | 35 | async init () { 36 | this.ready = true 37 | this.dispatchEvent(new CustomEvent('ready')) 38 | } 39 | 40 | async render () { 41 | const templates = await this.nest(this.form.formDefinition.chain, this.form.registry, this.form.formData.proxy, this.form) 42 | 43 | const formSubmit = (event) => { 44 | event.preventDefault() 45 | event.stopImmediatePropagation() 46 | this.form.dispatchEvent(new CustomEvent('submit', { detail: { 47 | proxy: this.form.formData.proxy, 48 | expanded: this.form.formData.proxy.$, 49 | } })) 50 | } 51 | const isDisplayOnly = this.form.getAttribute('display') 52 | 53 | render(this.form.shadow, html` 54 | 55 | 56 | 57 | ${[...this.extraStylesheets.values()].map(link => html``)} 58 | 59 | ${isDisplayOnly ? html`` : null} 60 | 61 | ${!isDisplayOnly ? html` 62 |
63 | ${templates} 64 |
65 | 66 |
67 |
68 | 69 | ` : templates} 70 | `) 71 | } 72 | 73 | async nest (formDefinition: Map, registry: Registry, formData: any, parent: any) { 74 | const templates: Array = [] 75 | 76 | const isDisplayOnly = this.form.getAttribute('display') 77 | 78 | for (const [bindings, [field, children]] of formDefinition.entries()) { 79 | const mainBinding = field['form:binding']?._ 80 | 81 | const isContainer = lastPart(field['@type'][0]) === 'Container' 82 | const isUiComponent = lastPart(field['@type'][0]) === 'UiComponent' 83 | 84 | let wrapperFieldInstance = isUiComponent || isContainer ? this.fieldInstances.get(field.$) : false 85 | 86 | if (!wrapperFieldInstance) wrapperFieldInstance = await registry.setupElement( 87 | field, bindings, null, {}, formData, () => this.render(), parent, null, children 88 | ) 89 | 90 | if (!wrapperFieldInstance) continue; 91 | 92 | if (!this.fieldInstances.has(field.$)) this.fieldInstances.set(field.$, wrapperFieldInstance) 93 | 94 | const innerTemplates: Array = [] 95 | 96 | if (mainBinding && !isContainer) { 97 | 98 | /** 99 | * Existing values. 100 | */ 101 | let applicableValues = formData?.[mainBinding] ? [...formData[mainBinding].values()] 102 | .filter((value) => !value['@language'] || value['@language'] === Language.l10nLanguage) : [] 103 | 104 | if (formData && Array.isArray(formData.$)) { 105 | applicableValues = flatMapProxy(formData, mainBinding) 106 | .filter((value) => value && !value['@language'] || value && value['@language'] === Language.l10nLanguage) 107 | } 108 | 109 | if (applicableValues.length) { 110 | for (const [index, value] of applicableValues.entries()) { 111 | let itemValues = formData?.[index] 112 | 113 | if (formData && Array.isArray(formData.$)) { 114 | const item = flatMapProxy(formData, mainBinding).find(itemValue => itemValue.$ === value.$) 115 | 116 | for (const [innerIndex, formDataItem] of formData.$.entries()) { 117 | if (formDataItem[mainBinding][0] === item.$) { 118 | itemValues = formData[innerIndex] 119 | } 120 | } 121 | } 122 | 123 | const fieldInstance = this.fieldInstances.get(value.$) ?? await registry.setupElement( 124 | field, bindings, value, itemValues, formData, () => this.render(), parent, index, children 125 | ) 126 | if (!this.fieldInstances.has(value.$)) this.fieldInstances.set(value.$, fieldInstance) 127 | 128 | const childValues = field['form:widget']?._ === 'group' ? formData[mainBinding][index] : formData[mainBinding] 129 | const childTemplates = children.size ? await this.nest(children, registry, childValues, wrapperFieldInstance) : [] 130 | innerTemplates.push(isDisplayOnly ? fieldInstance.itemDisplay(childTemplates) : fieldInstance.item(childTemplates)) 131 | } 132 | } 133 | 134 | /** 135 | * New items 136 | */ 137 | else if (!isDisplayOnly) { 138 | const childTemplates = children.size ? await this.nest(children, registry, [], wrapperFieldInstance) : [] 139 | innerTemplates.push(isDisplayOnly ? wrapperFieldInstance.itemDisplay(childTemplates) : wrapperFieldInstance.item(childTemplates)) 140 | } 141 | } 142 | 143 | /** 144 | * UI components 145 | */ 146 | else if (isUiComponent) { 147 | const childTemplates = children.size ? await this.nest(children, registry, formData, wrapperFieldInstance) : [] 148 | innerTemplates.push(isDisplayOnly ? wrapperFieldInstance.itemDisplay(childTemplates) : wrapperFieldInstance.item(childTemplates)) 149 | } 150 | 151 | /** 152 | * Containers 153 | */ 154 | else if (isContainer) { 155 | const childTemplates = children.size ? await this.nest(children, registry, mainBinding ? containerProxy(formData, mainBinding) : formData, wrapperFieldInstance) : [] 156 | innerTemplates.push(isDisplayOnly ? wrapperFieldInstance.itemDisplay(childTemplates) : wrapperFieldInstance.item(childTemplates)) 157 | } 158 | 159 | templates.push(isDisplayOnly ? wrapperFieldInstance.wrapperDisplay(innerTemplates) : wrapperFieldInstance.wrapper(innerTemplates)) 160 | } 161 | 162 | return templates 163 | } 164 | } -------------------------------------------------------------------------------- /src/core/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file fetches the translations. 3 | */ 4 | 5 | import { Hole } from 'uhtml' 6 | 7 | class TranslatedText extends Hole { 8 | 9 | readonly text: string 10 | public context: Array | string 11 | 12 | constructor(type, template = [], values = []) { 13 | /** @ts-ignore */ 14 | super(type, template, values); 15 | const text = type 16 | const context = template 17 | 18 | this.text = text; 19 | /** @ts-ignore */ 20 | this.template = [text]; 21 | /** @ts-ignore */ 22 | this.values = []; 23 | this.context = context; 24 | /** @ts-ignore */ 25 | this.type = 'html'; 26 | } 27 | 28 | toString () { 29 | return this.text; 30 | } 31 | } 32 | 33 | function mixString (a, b, asCodeString = false) { 34 | let total = Math.max(a.length, b.length); 35 | let string = ''; 36 | 37 | for (let part = 0; part < total; part++) { 38 | let valueString = ''; 39 | if (typeof b[part] === 'object') { 40 | let keys = Object.keys(b[part]); 41 | valueString = asCodeString ? '{' + keys[0] + '}' : b[part][keys[0]]; 42 | } 43 | else if (typeof b[part] === 'string') { 44 | valueString = b[part]; 45 | } 46 | 47 | string += a[part] + valueString; 48 | } 49 | 50 | return string; 51 | } 52 | 53 | export async function I18n (language, prefix = '', possibleLanguageCodes, skipImportLanguage = 'en') { 54 | let translations = {}; 55 | translations[language] = {}; 56 | if (possibleLanguageCodes.includes(language) && language !== skipImportLanguage) { 57 | try { 58 | const filePath = `/js/Translations/${(prefix ? prefix + '.' : '') + language}.js` 59 | translations[language] = (await import(filePath)).Translations; 60 | } 61 | catch (exception) { 62 | console.info(exception) 63 | } 64 | } 65 | 66 | /** 67 | * 68 | * @param context 69 | * @param values 70 | * @returns {TranslatedText} 71 | * @constructor 72 | */ 73 | function Translate (context, ...values): any { 74 | if (typeof context === 'string') { 75 | return (strings, ...values) => { 76 | let translatedText = Translate(strings, ...values); 77 | translatedText.context = context; 78 | return translatedText; 79 | } 80 | } 81 | else { 82 | let stringsToTranslate = context; 83 | let codeString = mixString(stringsToTranslate, values, true); 84 | 85 | /** 86 | * Translation is not available. 87 | */ 88 | if (typeof translations[language][codeString] === 'undefined') { 89 | return new TranslatedText(mixString(stringsToTranslate, values)); 90 | } 91 | 92 | /** 93 | * We have a translation. Fill in the tokens. 94 | */ 95 | else { 96 | let translatedString = translations[language][codeString]; 97 | let tokens = Object.assign({}, ...values); 98 | 99 | let replacements = translatedString.match(/{[a-zA-Z]*}/g); 100 | if (replacements) { 101 | replacements.forEach(replacement => { 102 | let variableName = replacement.substr(1).substr(0, replacement.length - 2); 103 | translatedString = translatedString.replace(replacement, tokens[variableName]); 104 | }); 105 | } 106 | 107 | return new TranslatedText(translatedString); 108 | } 109 | } 110 | } 111 | 112 | Translate.constructor.prototype.direct = (variable) => { 113 | if (typeof translations[language][variable] === 'undefined') { 114 | return new TranslatedText(variable); 115 | } 116 | else { 117 | return new TranslatedText(translations[language][variable]); 118 | } 119 | }; 120 | 121 | return Translate; 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss"; -------------------------------------------------------------------------------- /src/elements/Checkbox.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | 4 | export class Checkbox extends ElementBase { 5 | 6 | async on (event) { 7 | if (['click'].includes(event.type)) { 8 | if (!this.value) await this.addItem() 9 | this.value[`@${this.jsonldKey}`] = this.definition['form:translatable']?._ && this.value['@language'] ? event.target.checked.toString() : event.target.checked 10 | this.dispatchChange() 11 | this.render() 12 | } 13 | } 14 | 15 | disableLanguage () { 16 | super.disableLanguage() 17 | const values = this.parentValues[this.mainBinding] 18 | if (values?.[0]?.['@value']) { 19 | values[0]['@value'] = values[0]['@value'] === 'true' 20 | } 21 | } 22 | 23 | input () { 24 | const checked = this.value?._ === true || this.value?._ === 'true' 25 | 26 | return html` 27 | 35 | ` 36 | } 37 | 38 | get removable () { 39 | if (this.definition?.['form:removable']?._ === false) return false 40 | const hasValue = this.value?._ 41 | const parentIsGroup = this.parent instanceof ElementBase ? this.parent?.definition?.['form:widget']?._ === 'group' : false 42 | const isGroup = this.definition?.['form:widget']?._ === 'group' 43 | const isRequired = this.definition?.['form:required']?._ 44 | 45 | return !isRequired && hasValue && (!parentIsGroup || isGroup) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/elements/Color.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | 3 | export class Color extends ElementBase { 4 | constructor (...args) { 5 | super(...args) 6 | this.attributes.type = 'color' 7 | } 8 | } -------------------------------------------------------------------------------- /src/elements/Container.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | 4 | export class Container extends ElementBase { 5 | 6 | input () { return html`` } 7 | } -------------------------------------------------------------------------------- /src/elements/Date.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | 3 | export class Date extends ElementBase { 4 | constructor (...args) { 5 | super(...args) 6 | this.attributes.type = 'date' 7 | } 8 | } -------------------------------------------------------------------------------- /src/elements/Details.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html, render } from 'uhtml/async' 3 | import { kebabize } from '../helpers/kebabize' 4 | import { attributesDiff } from '../helpers/attributesDiff' 5 | 6 | export class Details extends ElementBase { 7 | 8 | constructor (...args: any[]) { 9 | super(...args) 10 | 11 | const childValues = [...this.children.values()].flatMap(([fieldDefinition]: [any]) => { 12 | const childBinding = fieldDefinition['form:binding']?._ 13 | if (childBinding && this.parentValues[childBinding]?._) { 14 | return this.parentValues[childBinding]?.$ 15 | } 16 | }).filter(item => item) 17 | 18 | if (!('open' in this.wrapperAttributes) && ( 19 | this.mainBinding && this.parentValues?.[this.mainBinding]?.length || 20 | !this.mainBinding && childValues.length 21 | )) { 22 | /** @ts-ignore */ 23 | this.wrapperAttributes.open = true 24 | } 25 | 26 | } 27 | 28 | input () { return html`` } 29 | 30 | async wrapper (innerTemplates: Array = [], isDisplayOnly = false) { 31 | 32 | if (isDisplayOnly) { 33 | const resolvedInnerTemplates = await Promise.all(innerTemplates) 34 | const tester = document.createElement('div') 35 | await render(tester, html`${resolvedInnerTemplates}`) 36 | const text = tester.innerText.replace(/\n|\r/g, '').trim() 37 | 38 | if (text === '') return html`` 39 | } 40 | 41 | const toggle = () => { 42 | this.wrapperAttributes.open = !this.wrapperAttributes.open 43 | } 44 | 45 | const type = kebabize(this.constructor.name) 46 | return html` 47 |
48 | ${this.label()} 49 | ${innerTemplates.length ? html` 50 |
51 | ${innerTemplates} 52 |
53 | ` : ''} 54 | ` 55 | } 56 | } -------------------------------------------------------------------------------- /src/elements/Dropdown.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { sparqlQueryToList } from '../helpers/sparqlQueryToList' 3 | import { html } from 'uhtml/async' 4 | import { Language } from '../core/Language' 5 | import { attributesDiff } from '../helpers/attributesDiff' 6 | import { onlyUnique } from '../helpers/onlyUnique' 7 | 8 | export class Dropdown extends ElementBase { 9 | 10 | constructor (...args) { 11 | super(...args) 12 | 13 | this.form.dispatchEvent(new CustomEvent('dropdown-options', { 14 | detail: { 15 | options: this.options, 16 | element: this 17 | } 18 | })) 19 | 20 | if (!this.options.length) { 21 | if (!this.definition['form:optionsQuery']?._ || !this.definition['form:optionsSource']?._) { 22 | throw new Error('optionsQuery and optionsSource are needed for the field dropdown. Please improve the form definition.') 23 | } 24 | } 25 | } 26 | 27 | removeButton () { 28 | const isMultiple = this.definition['form:multiple']?._ 29 | return isMultiple ? null : super.removeButton() 30 | } 31 | 32 | item (childTemplates: Array = []) { 33 | return this.definition['form:multiple']?._ !== true || this.index < 1 ? super.item(childTemplates) : html`` 34 | } 35 | 36 | on (event) { 37 | if (['keyup', 'change'].includes(event.type)) { 38 | const selectedItems = event.target.options ? [...event.target.options].filter(option => option.selected) : [...event.target.parentElement.parentElement.querySelectorAll(':checked')] 39 | const selectedValues = selectedItems.map(option => { 40 | return { 41 | ['@' + this.jsonldKey]: option.value 42 | } 43 | }) 44 | 45 | if (!this.parentValues[this.mainBinding]) this.parentValues[this.mainBinding] = [] 46 | this.parentValues[this.mainBinding].splice(0) 47 | this.parentValues[this.mainBinding].push(...selectedValues) 48 | this.dispatchChange() 49 | } 50 | } 51 | 52 | async input () { 53 | if (!this.options.length && this.definition['form:optionsQuery']?._ && this.definition['form:optionsSource']?._) { 54 | const proxy = this.form.getAttribute('proxy') ?? '' 55 | const source = this.definition['form:optionsSourceType']?._ ? { 56 | value: this.definition['form:optionsSource']._, type: this.definition['form:optionsSourceType']._ 57 | } : this.definition['form:optionsSource']._ 58 | this.options = await sparqlQueryToList(this.definition['form:optionsQuery']._, source, proxy) 59 | } 60 | 61 | const selectedValues = this.parentValues?.[this.mainBinding]?.map(option => option['@' + this.jsonldKey]) ?? [] 62 | 63 | const isMultiple = this.definition['form:multiple']?._ 64 | 65 | const hasGroups = this.options.every(item => item?.group) 66 | const groups = hasGroups ? this.options.map(item => item?.group).filter(onlyUnique).sort() : [] 67 | 68 | const createGroup = (groupName) => { 69 | const options = this.options.filter(option => option.group === groupName) 70 | return html` 71 |

${groupName}

72 | ${options.map(createCheckbox)} 73 | ` 74 | } 75 | 76 | const createCheckbox = (option) => html` 77 | 81 | ` 82 | 83 | return isMultiple ? ( 84 | hasGroups ? html`${groups.map(createGroup)}` : html`${this.options.map(createCheckbox)}` 85 | ) : html` 86 | ` 99 | } 100 | 101 | addButton () { 102 | return html`` 103 | } 104 | } -------------------------------------------------------------------------------- /src/elements/ElementBase.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'uhtml/async' 2 | import { faTimes, faPlus, faLanguage } from '../helpers/icons' 3 | import { kebabize } from '../helpers/kebabize' 4 | import { attributesDiff } from '../helpers/attributesDiff' 5 | import { Language } from '../core/Language' 6 | import { lastPart } from '../helpers/lastPart' 7 | import { isFetchable } from '../helpers/isFetchable' 8 | import { fa } from '../helpers/fa' 9 | import { debounce } from '../helpers/debounce'; 10 | 11 | export class ElementBase extends EventTarget { 12 | 13 | public definition: any 14 | protected bindings: Array 15 | protected value: any 16 | protected parentValues: any 17 | protected itemValues: any 18 | public parent: ElementBase | any 19 | protected jsonldKey = 'value' 20 | protected mainBinding: string 21 | public render = () => null 22 | protected suggestions: Array = [] 23 | protected index: number 24 | protected debouncedRender: any 25 | protected children = [] 26 | protected attributes: { 27 | type: string, 28 | class: [], 29 | disabled: null | boolean, 30 | readonly: null | boolean, 31 | placeholder: null | boolean, 32 | required: null | boolean, 33 | multiple: null | boolean, 34 | rows: null | boolean | number, 35 | open: null | boolean, 36 | } = { 37 | type: 'input', 38 | class: [], 39 | disabled: null, 40 | readonly: null, 41 | placeholder: null, 42 | required: null, 43 | multiple: null, 44 | rows: null, 45 | open: null, 46 | } 47 | 48 | public wrapperAttributes: { 49 | open: boolean, 50 | class: Array 51 | } = { 52 | open: false, 53 | class: ['form-element'] 54 | } 55 | 56 | protected labelAttributes: { 57 | class: Array 58 | } = { 59 | class: ['label'] 60 | } 61 | 62 | protected options: Array = [] 63 | 64 | constructor (...args: any[]) { 65 | super() 66 | const [ definition, bindings, value, itemValues, parentValues, render, parent, index, children ] = args 67 | 68 | this.definition = definition 69 | this.bindings = bindings 70 | 71 | this.mainBinding = definition['form:binding']?._ 72 | this.parentValues = parentValues 73 | this.itemValues = itemValues 74 | this.value = value 75 | this.render = render 76 | this.parent = parent 77 | this.index = index 78 | this.children = children 79 | 80 | this.debouncedRender = debounce(this.render.bind(this), 300) 81 | 82 | if (this.definition['form:jsonLdKey']) { 83 | this.jsonldKey = this.definition['form:jsonLdKey']._ 84 | } 85 | 86 | if (this.definition['form:placeholder']?._) this.attributes.placeholder = this.definition['form:placeholder']?._ 87 | if (this.definition['form:required']?._ === true) this.attributes.required = true 88 | if (this.definition['form:multiple']?._ === true) this.attributes.multiple = true 89 | if (this.definition['form:readonly']?._ === true) this.attributes.readonly = true 90 | if (this.definition['form:disabled']?._ === true) this.attributes.disabled = true 91 | if (this.definition['form:open']?._ !== undefined) this.wrapperAttributes.open = this.definition['form:open']._ 92 | if (this.definition['form:rows']?._ !== undefined) this.attributes.rows = parseInt(this.definition['form:rows']._) 93 | if (this.definition['form:cssClass']?._) this.wrapperAttributes.class.push(this.definition['form:cssClass']._) 94 | if (!this.definition['form:label']?._) this.wrapperAttributes.class.push('no-label') 95 | 96 | if (this.definition['form:option']) { 97 | this.options.push(...this.definition['form:option'].map(option => { 98 | return { 99 | label: option['form:label']?._, 100 | image: option['form:image']?._, 101 | uri: option['form:value']?._, 102 | jsonldKey: (Object.keys(option['form:value'][0])[0]).substr(1), 103 | } 104 | })) 105 | } 106 | 107 | this.addEventListener('destroy', () => { 108 | this.destroy() 109 | }, { once: true }) 110 | } 111 | 112 | async destroy () {} 113 | 114 | get proxy () { 115 | return this.form?.proxy ?? '' 116 | } 117 | 118 | get t () { 119 | return this.form?.t 120 | } 121 | 122 | get form (): HTMLElement & { t: any, proxy: string, formDefinition: any, renderer: any } { 123 | let pointer = this 124 | while (pointer.parent) { 125 | /** @ts-ignore */ 126 | pointer = pointer.parent 127 | } 128 | 129 | /** @ts-ignore */ 130 | return pointer.registry ? pointer : null 131 | } 132 | 133 | on (event) { 134 | if (['keyup', 'change'].includes(event.type)) { 135 | if (!this.value) this.addItem() 136 | if (this.value) { 137 | this.value[`@${this.jsonldKey}`] = event.target.value 138 | this.dispatchChange() 139 | } 140 | } 141 | } 142 | 143 | dispatchChange () { 144 | this.form.dispatchEvent(new CustomEvent('fieldchange', { 145 | detail: { 146 | value: this.value, 147 | field: this, 148 | /** @ts-ignore */ 149 | proxy: this.form.formData.proxy, 150 | /** @ts-ignore */ 151 | expanded: this.form.formData.proxy.$, 152 | } 153 | })) 154 | } 155 | 156 | get removable () { 157 | if (this.definition?.['form:removable']?._ === false) return false 158 | const hasValue = this.value?._ 159 | const parentIsGroup = this.parent instanceof ElementBase ? this.parent?.definition?.['form:widget']?._ === 'group' : false 160 | const parentIsReadonly = this.parent instanceof ElementBase ? this.parent?.definition?.['form:readonly']?._ : false 161 | const isGroup = this.definition?.['form:widget']?._ === 'group' 162 | const isRequired = this.definition?.['form:required']?._ 163 | const isReadonly = this.definition?.['form:readonly']?._ 164 | 165 | return !isRequired && hasValue && !parentIsGroup && !parentIsReadonly || ( isGroup && !isReadonly) 166 | } 167 | 168 | get languages () { 169 | return Language.extractUsedLanguages(this.parentValues?.[this.mainBinding]) 170 | } 171 | 172 | addItem () { 173 | if (this.bindings.length > 1) { 174 | if (!this.parentValues[this.mainBinding]) this.parentValues[this.mainBinding] = [] 175 | const emptyObject = {} 176 | for (const binding of this.bindings) { 177 | emptyObject[binding] = [] 178 | } 179 | emptyObject[this.mainBinding].push({}) 180 | this.parentValues.push(emptyObject) 181 | this.itemValues = emptyObject 182 | this.value = emptyObject[this.mainBinding][0] 183 | 184 | if (this.parent?.definition['form:widget']?._ === 'container' && this.parent?.definition['form:binding']?._) { 185 | const hasLanguageValues = this.parentValues?.flatMap(parentValue => parentValue[this.definition['form:binding']?._].map(value => value['@language'])) 186 | 187 | if (hasLanguageValues.length) { 188 | this.value['@language'] = Language.l10nLanguage 189 | } 190 | } 191 | } 192 | else if (this.definition['form:widget']?._ === 'group') { 193 | if (!this.parentValues[this.mainBinding]) { 194 | this.parentValues[this.mainBinding] = [{'@list': [{}]}] 195 | } 196 | 197 | const firstItem = this.parentValues[this.mainBinding]?.[0]?.$ 198 | const clone = JSON.parse(JSON.stringify(firstItem)) 199 | 200 | for (const [_field, values] of Object.entries(clone)) { 201 | /** @ts-ignore */ 202 | if (values?.[0]['@id']) values[0]['@id'] = null 203 | /** @ts-ignore */ 204 | if (values?.[0]['@value']) values[0]['@value'] = '' 205 | /** @ts-ignore */ 206 | if (values?.[0]['@language']) values[0]['@value'] = Language.l10nLanguage 207 | } 208 | 209 | this.parentValues?.[this.mainBinding].push(clone) 210 | this.value = clone 211 | } 212 | else { 213 | const value = { [`@${this.jsonldKey}`]: null } 214 | /** @ts-ignore */ 215 | if (this.languages.length || this.definition['form:translatable']?._ === 'always') value['@language'] = Language.l10nLanguage 216 | if (!this.parentValues?.[this.mainBinding]) this.parentValues[this.mainBinding] = [] 217 | this.parentValues?.[this.mainBinding].push(value) 218 | this.value = value 219 | } 220 | } 221 | 222 | removeItem () { 223 | if (this.bindings.length > 1) { 224 | const valueArray = this.parentValues.$ 225 | 226 | if (valueArray) { 227 | const index = valueArray.indexOf(this.itemValues.$) 228 | valueArray.splice(index, 1) 229 | } 230 | } 231 | else { 232 | const valueArray = this.parentValues[this.definition['form:binding']?._]?.$ 233 | 234 | if (valueArray) { 235 | const index = valueArray.indexOf(this.value.$) 236 | valueArray.splice(index, 1) 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Start of templates 243 | */ 244 | wrapperDisplay (innerTemplates: Array = []) { 245 | return this.wrapper(innerTemplates, true) 246 | } 247 | 248 | itemDisplay (childTemplates: Array = []) { 249 | return html` 250 |
251 | ${this.valueDisplay()} 252 | ${childTemplates} 253 |
` 254 | } 255 | 256 | valueDisplay () { 257 | return html`${this.value?._}` 258 | } 259 | 260 | async wrapper (innerTemplates: Array = [], isDisplayOnly = false) { 261 | const type = kebabize(this.constructor.name) 262 | const shouldShowEmpty = this.definition['form:translatable']?._ === 'always' && !Language.l10nLanguage 263 | const isReadOnly = this.definition['form:readonly']?._ ?? false 264 | return html` 265 | ${!shouldShowEmpty && (!isDisplayOnly || innerTemplates.length > 0) ? html` 266 |
267 | ${this.label()} 268 | ${innerTemplates.length ? html` 269 |
270 | ${this.description()} 271 | ${innerTemplates} 272 |
273 | ` : ''} 274 | ${this.definition['form:multiple']?._ && !isReadOnly && !isDisplayOnly ? html`
${this.addButton()}
` : html``} 275 |
276 | ` : html``}` 277 | } 278 | 279 | description () { 280 | return this.definition['form:description']?._ ? html`

${this.definition['form:description']?._}

` : null 281 | } 282 | 283 | item (childTemplates: Array = []) { 284 | return html` 285 |
286 | ${this.input()} 287 | ${this.removeButton()} 288 | ${childTemplates} 289 |
` 290 | } 291 | 292 | addButton () { 293 | return html`` 299 | } 300 | 301 | removeButton () { 302 | return this.removable ? html`` : html`` 308 | } 309 | 310 | input () { 311 | return html` 312 | this.on(event)} 316 | onkeyup=${(event) => this.on(event)} 317 | /> 318 | ` 319 | } 320 | 321 | disableLanguage () { 322 | const values = this.parentValues[this.mainBinding] 323 | if (values) { 324 | values.splice(1) 325 | delete values[0]['@language'] 326 | } 327 | } 328 | 329 | enableLanguage() { 330 | if (!this.parentValues[this.mainBinding]) this.parentValues[this.mainBinding] = this.parentValues[this.mainBinding] = [] 331 | const values = this.parentValues[this.mainBinding] 332 | if (values.length) { 333 | for (const value of values) { 334 | value['@language'] = Language.l10nLanguage 335 | if (value['@value']) value['@value'] = value['@value'].toString() 336 | } 337 | } 338 | else { 339 | values.push({ '@language': Language.l10nLanguage }) 340 | } 341 | } 342 | 343 | async label () { 344 | let languageLabel = '' 345 | 346 | /** @ts-ignore */ 347 | const isDisplayOnly = this.form?.getAttribute('display') 348 | 349 | if (this.definition['form:translatable']?._) { 350 | const applicableValues = this.parentValues?.[this.mainBinding] ? [...this.parentValues[this.mainBinding].values()] 351 | .filter((value) => !value['@language'] || value['@language'] === Language.l10nLanguage) : [] 352 | 353 | let hasLanguageValues = this.parentValues?.[this.mainBinding] ? [...this.parentValues[this.mainBinding].values()].filter(item => item['@language']) : [] 354 | 355 | if (this.parent?.definition['form:widget']?._ === 'container' && this.parent?.definition['form:binding']?._) { 356 | hasLanguageValues = this.parentValues?.flatMap ? this.parentValues?.flatMap(parentValue => parentValue[this.definition['form:binding']?._].map(value => value['@language'])) : null 357 | } 358 | 359 | const language = hasLanguageValues && hasLanguageValues.length ? Language.l10nLanguage : '' 360 | 361 | if (language) { 362 | languageLabel = `(${Language.l10nLanguages[language]})` 363 | } 364 | else if (applicableValues.length === 0 && this.definition['form:translatable']?._ === 'always') { 365 | languageLabel = `(${Language.l10nLanguages[Language.l10nLanguage]})` 366 | } 367 | } 368 | 369 | const disableLanguage = () => { 370 | this.disableLanguage() 371 | this.render() 372 | } 373 | 374 | const enableLanguage = () => { 375 | this.enableLanguage() 376 | this.render() 377 | } 378 | 379 | return this.definition['form:label']?._ ? html` 380 | 388 | ` : html`` 389 | } 390 | 391 | async referenceLabel (uri, meta) { 392 | if (!meta) { 393 | const subject = lastPart(uri).replace(/_|-/g, ' ') 394 | meta = { label: subject } 395 | } 396 | 397 | return html` 398 |
399 | ${meta?.label === false ? html`${this.value?._}` : html` 400 | ${meta?.thumbnail ? html`
` : ''} 401 | ${meta?.label ? ( 402 | isFetchable(uri) ? html`${meta?.label}` : html`${meta?.label}` 403 | ) : html`${this.t`Loading...`}`} 404 | `} 405 |
406 | ` 407 | } 408 | 409 | } -------------------------------------------------------------------------------- /src/elements/Group.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | 4 | export class Group extends ElementBase { 5 | 6 | constructor (...args: any[]) { 7 | super(...args) 8 | if (!this.value) { 9 | this.addItem() 10 | this.removeItem() 11 | } 12 | } 13 | 14 | item (childTemplates: Array = []) { 15 | 16 | return html` 17 |
18 | ${childTemplates} 19 | ${this.removeButton()} 20 |
` 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/elements/LanguagePicker.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'uhtml/async' 2 | import { ElementBase } from './ElementBase' 3 | import { Language, t } from '../core/Language' 4 | import { langCodesToObject, filterLanguages } from '../core/Language' 5 | import { SlimSelect } from '../vendor/slimselect.js' 6 | import { faGlobe, faTimes } from '../helpers/icons' 7 | import { fa } from '../helpers/fa' 8 | import { icon } from '../vendor/fontawesome-svg-core.js' 9 | import { languages } from '../languages' 10 | 11 | export class LanguagePicker extends ElementBase { 12 | 13 | protected initiated = false 14 | 15 | protected isDragging = false 16 | protected dragX: null | number = null 17 | protected newestItem = null 18 | 19 | itemDisplay () { 20 | return Object.keys(Language.l10nLanguages).length > 1 ? this.item() : html`` 21 | } 22 | 23 | item () { 24 | const onChange = async (event) => { 25 | if (!this.initiated) return 26 | const selectedLanguages = event.currentTarget.slim.selected() 27 | Language.l10nLanguages = await langCodesToObject(selectedLanguages) 28 | this.render() 29 | } 30 | 31 | return html` 32 |
33 | 34 | ${this.uiLanguageSwitcher()} 35 |
` 36 | } 37 | 38 | l10nLanguagePicker (select) { 39 | if (select.slim) { 40 | this.attachEvents(select) 41 | return 42 | } 43 | 44 | const initialLanguages = [...Object.entries(Language.l10nLanguages)] 45 | let deleteIcon = icon(faTimes).html[0] 46 | const slimSelect = new SlimSelect({ 47 | select: select, 48 | deselectLabel: deleteIcon, 49 | addable: function (value) { 50 | const label = prompt(t`Language label`.toString(), value) ?? '' 51 | const key = prompt(t`BCP 47`.toString(), value) ?? '' 52 | 53 | const languageFound = languages.find(item => item[0] === key) 54 | 55 | if (key && label && !languageFound) { 56 | const newItem = { 57 | text: label, 58 | value: key, 59 | mandatory: Language.requiredL10nLanguages.includes(key) 60 | } 61 | 62 | if (!languages.find(item => item.join('-') === [key, label].join('-'))) { 63 | languages.push([key, label]) 64 | } 65 | 66 | return newItem 67 | } 68 | 69 | if (languageFound) alert(t`The language key ${{key}} is already used.`.toString()) 70 | 71 | return false 72 | }, 73 | beforeOnChange: (values) => { 74 | const newItem = values.map(value => value.value).find(item => !Object.keys(Language.l10nLanguages).includes(item)) 75 | Language.l10nLanguage = newItem 76 | }, 77 | ajax: async function (search, callback) { 78 | const languages = await filterLanguages(search) 79 | const options = languages.map(language => { 80 | return { 81 | text: Language.l10nLanguages?.[language[0]] ?? language[1], 82 | value: language[0], 83 | } 84 | }) 85 | 86 | callback(options) 87 | } 88 | }) 89 | 90 | const selection = initialLanguages.map(([langCode, language]) => { 91 | return { 92 | text: Language.l10nLanguages[langCode] ?? language, 93 | value: langCode, 94 | mandatory: Language.requiredL10nLanguages.includes(langCode) 95 | } 96 | }) 97 | 98 | slimSelect.setData([{'placeholder': true, 'text': t.direct('Search for a language').toString() }, ...selection]) 99 | slimSelect.set(selection.map(option => option.value)) 100 | this.attachEvents(select) 101 | this.setScrollClasses(select) 102 | this.initiated = true // Without this it would trigger a needless render. 103 | } 104 | 105 | setScrollClasses(select) { 106 | const tabsWrapper = select.parentElement.querySelector('.ss-multi-selected') 107 | if (!tabsWrapper.scrollWidth) return 108 | tabsWrapper.classList.remove('hide-left-shadow') 109 | tabsWrapper.classList.remove('hide-right-shadow') 110 | 111 | if (tabsWrapper.scrollLeft === 0) { 112 | tabsWrapper.classList.add('hide-left-shadow') 113 | } 114 | 115 | if (tabsWrapper.scrollWidth - 1 <= tabsWrapper.clientWidth + tabsWrapper.scrollLeft) { 116 | tabsWrapper.classList.add('hide-right-shadow') 117 | } 118 | } 119 | 120 | attachEvents (select) { 121 | const langCodes = [...Object.entries(Language.l10nLanguages)].map(([langCode]) => langCode) 122 | const tabs = [...select.parentElement.querySelectorAll('.ss-value')] 123 | const tabsWrapper = select.parentElement.querySelector('.ss-multi-selected') 124 | this.setActive(tabs, langCodes) 125 | if (tabsWrapper.initiated) return 126 | tabsWrapper.initiated = true 127 | 128 | Language.addEventListener('l10n-change', () => { 129 | for (const tab of tabs) tab.classList.remove('active') 130 | }) 131 | 132 | tabsWrapper.addEventListener('mousedown', () => { 133 | this.isDragging = true 134 | this.dragX = null 135 | }) 136 | 137 | tabsWrapper.addEventListener('scroll', () => { 138 | this.setScrollClasses(select) 139 | }) 140 | 141 | this.setScrollClasses(select) 142 | setTimeout(() => { 143 | this.setScrollClasses(select) 144 | }, 100); 145 | 146 | tabsWrapper.addEventListener('mousemove', (event) => { 147 | if (this.isDragging) { 148 | if (this.dragX !== null) { 149 | const delta = this.dragX ? this.dragX - event.clientX : 0 150 | tabsWrapper.scrollTo({ 151 | top: 0, 152 | left: tabsWrapper.scrollLeft + delta 153 | }) 154 | } 155 | 156 | this.dragX = event.clientX 157 | 158 | event.preventDefault() 159 | event.stopImmediatePropagation() 160 | } 161 | }) 162 | 163 | tabsWrapper.addEventListener('click', (event) => { 164 | if (this.isDragging && this.dragX !== null) { 165 | event.preventDefault() 166 | event.stopImmediatePropagation() 167 | } 168 | 169 | this.isDragging = false 170 | this.dragX = null 171 | }, { 172 | capture: true 173 | }) 174 | } 175 | 176 | setActive (tabs, langCodes) { 177 | for (const [index, tab] of tabs.entries()) { 178 | if (tab.querySelector('.ss-value-delete')) tab.querySelector('.ss-value-delete').title = t.direct('Delete all translations for this language').toString() 179 | if (langCodes[index] === Language.l10nLanguage) tab.classList.add('active') 180 | if (tab.hasEvent) continue 181 | tab.hasEvent = true 182 | 183 | tab.addEventListener('click', (event) => { 184 | event.preventDefault() 185 | event.stopImmediatePropagation() 186 | for (const innerTab of tabs) if (innerTab !== tab) innerTab.classList.remove('active') 187 | tab.classList.add('active') 188 | Language.l10nLanguage = langCodes[index] 189 | this.render() 190 | }) 191 | } 192 | } 193 | 194 | uiLanguageSwitcher () { 195 | return Object.keys(Language.uiLanguages).length ? html`
196 | ${fa(faGlobe)} 197 |
208 | ` : html`` 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /src/elements/Mail.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | 3 | export class Mail extends ElementBase { 4 | constructor (...args) { 5 | super(...args) 6 | this.attributes.type = 'mail' 7 | } 8 | } -------------------------------------------------------------------------------- /src/elements/Number.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | 3 | export class Number extends ElementBase { 4 | 5 | constructor (...args) { 6 | super(...args) 7 | this.attributes.type = 'number' 8 | } 9 | 10 | async on (event) { 11 | if (['keyup', 'change'].includes(event.type)) { 12 | if (!this.value) await this.addItem() 13 | this.value[`@${this.jsonldKey}`] = parseInt(event.target.value) 14 | this.dispatchChange() 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/elements/Reference.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | import { fa } from '../helpers/fa' 4 | import { faPencilAlt, faCheck, faReply } from '../helpers/icons' 5 | import { searchSuggestionsSparqlQuery } from '../helpers/searchSuggestionsSparqlQuery' 6 | import { dbpediaSuggestions } from '../helpers/dbpediaSuggestions' 7 | import { sparqlQueryToList } from '../helpers/sparqlQueryToList' 8 | import { t, Language } from '../core/Language' 9 | import { attributesDiff } from '../helpers/attributesDiff' 10 | import { debounce } from '../helpers/debounce' 11 | import { isFetchable } from '../helpers/isFetchable' 12 | import { getUriMeta } from '../helpers/getUriMeta' 13 | import { kebabize } from '../helpers/kebabize' 14 | import { lastPart } from '../helpers/lastPart' 15 | 16 | export class Reference extends ElementBase { 17 | 18 | protected jsonldKey = 'id' 19 | protected expanded = false 20 | protected searchTerm: string | null = null 21 | protected previousValue = null 22 | protected inputElement 23 | 24 | constructor (...args) { 25 | super(...args) 26 | this.autocomplete = debounce(this.autocomplete.bind(this), 400) 27 | } 28 | 29 | async on (event) { 30 | if (['keyup', 'change'].includes(event.type)) { 31 | if (!this.value) await this.addItem() 32 | if (event.target.value.substr(0, 4) === 'http') { 33 | this.value[`@${this.jsonldKey}`] = event.target.value 34 | this.dispatchChange() 35 | } 36 | else { 37 | this.searchTerm = event.target.value 38 | } 39 | 40 | if (this.definition['form:autoCompleteQuery']?._) { 41 | this.autocomplete() 42 | } 43 | } 44 | } 45 | 46 | get removable () { 47 | const parentIsGroup = this.parent instanceof ElementBase ? this.parent?.definition?.['form:widget']?._ === 'group' : false 48 | if (this.index < 1 && this.definition['form:required']?._ === true) return false 49 | return !parentIsGroup 50 | } 51 | 52 | autocomplete () { 53 | if (!this.searchTerm) return 54 | const querySetting = this.definition['form:autoCompleteQuery']?._ 55 | const sourceSetting = this.definition['form:autoCompleteSource']?._ 56 | 57 | const { 58 | query, 59 | source 60 | /** @ts-ignore */ 61 | } = querySetting || sourceSetting ? searchSuggestionsSparqlQuery(querySetting, sourceSetting, this.searchTerm) : dbpediaSuggestions(this.searchTerm) 62 | 63 | const textSuggestion = { 64 | label: t`Add ${{searchTerm: this.searchTerm}} as text without reference.`, 65 | value: this.searchTerm 66 | } 67 | 68 | if (query && source) { 69 | const proxy = this.form.getAttribute('proxy') ?? '' 70 | sparqlQueryToList(query, source, proxy).then(searchSuggestions => { 71 | searchSuggestions.push(textSuggestion) 72 | this.suggestions = searchSuggestions 73 | this.render() 74 | }).catch(exception => { 75 | console.error(exception) 76 | this.searchTerm = null 77 | this.suggestions = [] 78 | this.render() 79 | }) 80 | } 81 | else { 82 | this.suggestions = [textSuggestion] 83 | this.render() 84 | } 85 | } 86 | 87 | input () { 88 | return html` 89 | this.inputElement = element)} 91 | value=${this.searchTerm ?? this.value?._ ?? ''} 92 | onchange=${(event) => this.on(event)} 93 | onkeyup=${(event) => this.on(event)} 94 | /> 95 | ` 96 | } 97 | 98 | async item (childTemplates: Array = [], isDisplayOnly = false) { 99 | const value = this.value?._ 100 | const uri = value?.substr(0, 4) === 'http' ? value : false 101 | 102 | const editButton = () => html`` 112 | 113 | const acceptButton = () => html`` 125 | 126 | const restoreButton = () => html` 127 | ` 137 | 138 | const isCollapsed = (uri || this.value?.['@value']) && !this.expanded && !this.searchTerm 139 | 140 | let meta = uri && isFetchable(uri) ? await getUriMeta(uri, this.proxy) : false 141 | 142 | if (this.value?.['@value']) { 143 | meta = { 144 | label: this.value?.['@value'] 145 | } 146 | } 147 | 148 | return html` 149 |
150 | ${isCollapsed ? 151 | html`${this.referenceLabel(uri ? uri : '', meta)} ${isDisplayOnly ? null : editButton()}` : 152 | html`
${this.input()} ${!this.searchTerm ? acceptButton() : ''} ${restoreButton()} ${this.removeButton()}
` 153 | } 154 | 155 | ${this.searchTerm ? this.searchSuggestions() : ''} 156 | 157 | ${!isCollapsed && this.options.length && !this.suggestions.length ? html` 158 |
159 | ${this.options.map(option => { 160 | return html` 161 | { 162 | if (!this.value) this.addItem() 163 | this.value['@' + option.jsonldKey] = option.uri 164 | const oppositeSymbol = option.jsonldKey === 'value' ? 'id' : 'value' 165 | delete this.value['@' + oppositeSymbol] 166 | this.dispatchChange() 167 | this.expanded = false 168 | this.searchTerm = null 169 | this.suggestions = [] 170 | 171 | this.render() 172 | }}> 173 | ${option?.label?.[Language.uiLanguage] ?? option?.label ?? ''} 174 | 175 | `})} 176 |
177 | ` : null} 178 | 179 | ${childTemplates} 180 |
` 181 | } 182 | 183 | searchSuggestions () { 184 | const hasResults = !(this.suggestions[0]?.value) 185 | 186 | return this.suggestions.length ? html` 187 |
    188 | ${!hasResults ? html`
  • 189 | ${t`Nothing found`} 190 |
  • ` : ''} 191 | ${this.suggestions.map(suggestion => html` 192 |
  • 208 | ${suggestion.image ? html`
    ` : ''} 209 | ${suggestion.label?.[Language.uiLanguage] ?? suggestion.label} 210 |
  • `)} 211 |
212 | ` : '' 213 | } 214 | 215 | 216 | wrapper (innerTemplates: Array = [], isDisplayOnly = false) { 217 | const type = kebabize(this.constructor.name) 218 | const shouldShowEmpty = this.definition['form:translatable']?._ === 'always' && !Language.l10nLanguage 219 | 220 | return html` 221 | ${!shouldShowEmpty && (!isDisplayOnly || isDisplayOnly && innerTemplates.length > 0) ? html` 222 |
223 | ${this.label()} 224 | ${innerTemplates.length ? html` 225 |
226 | ${innerTemplates} 227 | ${this.definition['form:multiple']?._ && !isDisplayOnly ? html`
${this.addButton()}
` : html``} 228 |
229 | ` : ''} 230 | 231 |
232 | ` : html``}` 233 | } 234 | 235 | itemDisplay (childTemplates: Array = []) { 236 | return this.item(childTemplates, true) 237 | } 238 | 239 | valueDisplay () { 240 | return html`${this.value?._}` 241 | } 242 | 243 | } -------------------------------------------------------------------------------- /src/elements/String.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | 3 | export class String extends ElementBase { 4 | constructor (...args) { 5 | super(...args) 6 | } 7 | } -------------------------------------------------------------------------------- /src/elements/Textarea.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | import { attributesDiff } from '../helpers/attributesDiff' 4 | 5 | export class Textarea extends ElementBase { 6 | input () { 7 | let textValue = this.value?._ ?? '' 8 | if (typeof textValue === 'string') textValue.trim() 9 | 10 | return html` 11 | ` 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/elements/Unknown.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | 4 | export class Unknown extends ElementBase { 5 | input () { 6 | console.log(this.definition) 7 | return html`Unknown field type ${this.definition['form:widget']?._}` 8 | } 9 | } -------------------------------------------------------------------------------- /src/elements/Url.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | import { lastPart } from '../helpers/lastPart' 4 | 5 | export class Url extends ElementBase { 6 | constructor (...args) { 7 | super(...args) 8 | this.attributes.type = 'url' 9 | } 10 | 11 | valueDisplay () { 12 | return html`${lastPart(this.value?._)}` 13 | } 14 | } -------------------------------------------------------------------------------- /src/elements/UrlImage.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { getImageDimensionsByUrl } from '../helpers/getImageDimensionsByUrl' 3 | import { getImageColor } from '../helpers/getImageColor' 4 | import { html } from 'uhtml/async' 5 | import { attributesDiff } from '../helpers/attributesDiff' 6 | 7 | const imagesCache = new Map() 8 | 9 | export class UrlImage extends ElementBase { 10 | 11 | protected focalPoint: { x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number } 12 | protected isDragging = false 13 | protected focalPointDiv: HTMLDivElement 14 | 15 | constructor (...args) { 16 | super(...args) 17 | 18 | const focalPointPrefix = this.form.formDefinition.context.focalPoint 19 | 20 | this.focalPoint = { 21 | x1: this.itemValues?.[`${focalPointPrefix}x1`]?._, 22 | y1: this.itemValues?.[`${focalPointPrefix}y1`]?._, 23 | x2: this.itemValues?.[`${focalPointPrefix}x2`]?._, 24 | y2: this.itemValues?.[`${focalPointPrefix}y2`]?._, 25 | x3: this.itemValues?.[`${focalPointPrefix}x3`]?._, 26 | y3: this.itemValues?.[`${focalPointPrefix}y3`]?._, 27 | x4: this.itemValues?.[`${focalPointPrefix}x4`]?._, 28 | y4: this.itemValues?.[`${focalPointPrefix}y4`]?._, 29 | } 30 | } 31 | 32 | async on(event: Event) { 33 | if (['keyup', 'change'].includes(event.type)) { 34 | if (!this.value) this.addItem() 35 | if (this.value) { 36 | this.value[`@${this.jsonldKey}`] = (event.target as HTMLInputElement).value 37 | 38 | this.dispatchChange() 39 | } 40 | } 41 | 42 | const dimensionsEnabled = this.definition['form:dimensions']?.length > 0 43 | const saveColor = this.definition['form:saveColor']?.length > 0 44 | const url = this.value?._ 45 | const schemaPrefix = this.form.formDefinition.context.schema 46 | 47 | if (dimensionsEnabled && url) { 48 | getImageDimensionsByUrl(url).then(({ width, height }) => { 49 | this.itemValues[`${schemaPrefix}width`] = [{ '@value': width }] 50 | this.itemValues[`${schemaPrefix}height`] = [{ '@value': height }] 51 | }) 52 | } 53 | 54 | if (saveColor) { 55 | getImageColor(url).then(({ color }) => { 56 | this.itemValues[`${schemaPrefix}color`] = [{ '@value': color }] 57 | }) 58 | } 59 | 60 | this.render() 61 | } 62 | 63 | async destroy () { 64 | for (const imageDestroyer of imagesCache.values()) { 65 | imageDestroyer() 66 | } 67 | } 68 | 69 | attachImageEvents (image: HTMLImageElement) { 70 | const onmousedown = (event: MouseEvent) => { 71 | event.preventDefault() 72 | this.reset() 73 | this.isDragging = true 74 | this.reCalc() 75 | this.focalPoint.x3 = 100 / image.width * event.offsetX 76 | this.focalPoint.y3 = 100 / image.height * event.offsetY 77 | } 78 | 79 | const onmousemove = (event: MouseEvent) => { 80 | if (this.isDragging) { 81 | this.focalPoint.x4 = 100 / image.width * event.offsetX 82 | this.focalPoint.y4 = 100 / image.height * event.offsetY 83 | this.reCalc() 84 | } 85 | } 86 | 87 | const onmouseup = (event: MouseEvent) => { 88 | this.isDragging = false 89 | const endX = 100 / image.width * event.offsetX 90 | const endY = 100 / image.height * event.offsetY 91 | this.reCalc() 92 | 93 | const allowedMouseMove = 4 94 | if (Math.abs(endX - this.focalPoint.x3) < allowedMouseMove && Math.abs(endY - this.focalPoint.y3) < allowedMouseMove) { 95 | this.focalPointDiv.removeAttribute('style') 96 | this.reset() 97 | } 98 | else { 99 | const focalPointPrefix = this.form.formDefinition.context.focalPoint 100 | 101 | this.itemValues[`${focalPointPrefix}x1`] = [{ '@value': this.focalPoint.x1 }] 102 | this.itemValues[`${focalPointPrefix}y1`] = [{ '@value': this.focalPoint.y1 }] 103 | this.itemValues[`${focalPointPrefix}x2`] = [{ '@value': this.focalPoint.x2 }] 104 | this.itemValues[`${focalPointPrefix}y2`] = [{ '@value': this.focalPoint.y2 }] 105 | } 106 | } 107 | 108 | image.parentElement?.querySelector('.focal-point')?.remove() 109 | image.parentElement?.querySelector('.image-background')?.remove() 110 | 111 | this.focalPointDiv = document.createElement('div') 112 | this.focalPointDiv.classList.add('focal-point') 113 | this.setStyle() 114 | image.before(this.focalPointDiv) 115 | 116 | const imageBackground = document.createElement('img') 117 | imageBackground.classList.add('image-background') 118 | imageBackground.src = image.src 119 | image.before(imageBackground) 120 | 121 | image.parentElement?.addEventListener('mousedown', onmousedown) 122 | image.parentElement?.addEventListener('mousemove', onmousemove) 123 | image.parentElement?.addEventListener('mouseup', onmouseup) 124 | 125 | imagesCache.set(image, () => { 126 | image.parentElement?.removeEventListener('mousedown', onmousedown) 127 | image.parentElement?.removeEventListener('mousemove', onmousemove) 128 | image.parentElement?.removeEventListener('mouseup', onmouseup) 129 | }) 130 | 131 | this.setStyle() 132 | } 133 | 134 | input () { 135 | const focalPointEnabled = this.definition['form:focalPoint']?.length > 0 136 | 137 | return html` 138 |
139 | this.on(event)} 143 | onkeyup=${(event) => this.on(event)} 144 | /> 145 | ${this.removeButton()} 146 |
147 | 148 | ${this.value?._ ? 149 | focalPointEnabled ? 150 | html` 151 |
152 | 156 |
` : 157 | html`` 158 | : ''} 159 | ` 160 | } 161 | 162 | 163 | item (childTemplates: Array = []) { 164 | return html` 165 |
166 | ${this.input()} 167 | ${childTemplates} 168 |
` 169 | } 170 | 171 | setStyle = () => { 172 | this.focalPointDiv.style.left = this.focalPoint.x1 + '%' 173 | this.focalPointDiv.style.top = this.focalPoint.y1 + '%' 174 | this.focalPointDiv.style.width = (this.focalPoint.x2 - this.focalPoint.x1) + '%' 175 | this.focalPointDiv.style.height = (this.focalPoint.y2 - this.focalPoint.y1) + '%' 176 | 177 | const image = this.focalPointDiv.parentElement?.querySelector('.uppy-Dashboard-Item-previewImg') as HTMLImageElement 178 | if (!image) return 179 | 180 | image.style.clipPath = `inset( 181 | ${this.focalPoint.y1}% 182 | ${100 - this.focalPoint.x2}% 183 | ${100 - this.focalPoint.y2}% 184 | ${this.focalPoint.x1}% 185 | )` 186 | } 187 | 188 | reCalc = () => { 189 | if (this.focalPoint.x3 === null || this.focalPoint.x4 === null) { 190 | this.reset() 191 | this.focalPointDiv.removeAttribute('style') 192 | return 193 | } 194 | 195 | this.focalPoint.x1 = Math.round(Math.min(this.focalPoint.x3, this.focalPoint.x4)) 196 | this.focalPoint.x2 = Math.round(Math.max(this.focalPoint.x3, this.focalPoint.x4)) 197 | this.focalPoint.y1 = Math.round(Math.min(this.focalPoint.y3, this.focalPoint.y4)) 198 | this.focalPoint.y2 = Math.round(Math.max(this.focalPoint.y3, this.focalPoint.y4)) 199 | 200 | this.setStyle() 201 | } 202 | 203 | reset () { 204 | /** @ts-ignore */ 205 | this.focalPoint = { x1: null, y1: null, x2: null, y2: null, x3: null, y3: null, x4: null, y4: null } 206 | const image = this.focalPointDiv.parentElement?.querySelector('.uppy-Dashboard-Item-previewImg') as HTMLImageElement 207 | if (!image) return 208 | 209 | image.removeAttribute('style') 210 | } 211 | 212 | valueDisplay () { 213 | return html`` 214 | } 215 | } -------------------------------------------------------------------------------- /src/elements/UrlUppy.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'uhtml/async' 2 | import { importGlobalScript } from '../helpers/importGlobalScript' 3 | import { Language, t } from '../core/Language' 4 | import { UrlImage } from './UrlImage' 5 | 6 | function assertServerError (res) { 7 | if (res && res.error) { 8 | const error = new Error(res.message) 9 | Object.assign(error, res.error) 10 | throw error 11 | } 12 | return res 13 | } 14 | 15 | const uppys = new Map() 16 | const instances = new Set() 17 | const failedPreviews = new Set() 18 | 19 | export class UrlUppy extends UrlImage { 20 | 21 | public uppy: any 22 | 23 | constructor (...args) { 24 | super(...args) 25 | this.attributes.type = 'url' 26 | 27 | const url = 'https://releases.transloadit.com/uppy/v2.4.1/uppy.min.css' 28 | this.form.renderer.extraStylesheets.add(url) 29 | } 30 | 31 | clickEvents (event: MouseEvent) { 32 | /** @ts-ignore */ 33 | const removeButton = event?.target?.closest('.uppy-Dashboard-Item-action--remove') 34 | if (removeButton) { 35 | const result = confirm(t.direct(`Are you sure?`)) 36 | if (!result) event.stopPropagation() 37 | } 38 | } 39 | 40 | async getUppy (id) { 41 | if (uppys.has(id)) return await uppys.get(id) 42 | const promise = this.createUppy() 43 | uppys.set(id, promise) 44 | return promise 45 | } 46 | 47 | async createUppy () { 48 | const Uppy = await importGlobalScript('https://releases.transloadit.com/uppy/v2.4.1/uppy.js', 'Uppy') 49 | const element = document.createElement('div') 50 | 51 | const definition = this.definition 52 | 53 | 54 | const uppy = new Uppy.Core({ 55 | id: location.pathname + this.definition['@id'] + Language.l10nLanguage, 56 | allowedFileTypes: [] 57 | }) 58 | .use(Uppy.Url, { companionUrl: this.definition['form:uppyCompanion']?._ }) 59 | .use(Uppy.Dashboard, { 60 | inline: true, 61 | hideCancelButton: true, 62 | showRemoveButtonAfterComplete: true, 63 | target: element, 64 | proudlyDisplayPoweredByUppy: false, 65 | plugins: ['Url'] 66 | }) 67 | .use(Uppy.AwsS3Multipart, { 68 | limit: 4, 69 | createMultipartUpload: function (file) { 70 | this.assertHost('createMultipartUpload') 71 | 72 | const metadata = {} 73 | 74 | Object.keys(file.meta).forEach(key => { 75 | if (file.meta[key] != null) { 76 | metadata[key] = file.meta[key].toString() 77 | } 78 | }) 79 | 80 | const uppyDomain = definition['form:uppyDomain']._ 81 | const uppyDomainUrl = new URL(uppyDomain) 82 | 83 | return this.client.post('s3/multipart', { 84 | filename: uppyDomainUrl.pathname.substr(1) + file.name, 85 | type: file.type, 86 | metadata, 87 | }).then(assertServerError) 88 | }, 89 | companionUrl: this.definition['form:uppyCompanion']?._, 90 | }) 91 | 92 | uppy.on('file-removed', async (file, reason) => { 93 | if (reason === 'removed-by-user') { 94 | 95 | const parentIsGroup = this.parent.constructor.name === 'Container' && this.definition['form:type'] 96 | const values = parentIsGroup ? this.parent.parentValues[this.parent.mainBinding] : this.parentValues[this.mainBinding] 97 | values?.splice(file.meta.index, 1) 98 | 99 | await this.form.renderer.render() 100 | 101 | this.form.dispatchEvent(new CustomEvent('file-deleted', { 102 | detail: { file } 103 | })) 104 | } 105 | }) 106 | 107 | uppy.on('file-added', async (file) => { 108 | if (!file.meta.relativePath && file.remote?.body?.url) { 109 | file.meta.relativePath = file.remote.body.url 110 | } 111 | 112 | const uppyDomain = this.definition['form:uppyDomain']._ 113 | 114 | if (file.meta?.relativePath && !file.meta.relativePath.includes(uppyDomain)) { 115 | if (!('index' in file.meta)) { 116 | await this.addFileToJsonLd(file) 117 | } 118 | } 119 | 120 | this.refreshPreviews() 121 | }) 122 | 123 | uppy.on('upload-success', async (file, response) => { 124 | const uppyDomain = this.definition['form:uppyDomain']._ 125 | file.meta.relativePath = uppyDomain + response.body.location 126 | await this.addFileToJsonLd(file) 127 | await this.form.renderer.render() 128 | this.form.dispatchEvent(new CustomEvent('file-added', { 129 | detail: { file, response } 130 | })) 131 | }) 132 | 133 | uppy.rdfFormElement = element 134 | return uppy 135 | } 136 | 137 | async addFileToJsonLd (file) { 138 | const uppy = await this.getUppy(this.definition['@id'] + Language.l10nLanguage) 139 | 140 | uppy.setFileState(file.id, { 141 | progress: { uploadComplete: true, uploadStarted: true } 142 | }) 143 | 144 | const parentIsGroup = this.parent.constructor.name === 'Container' && this.definition['form:type']?._ 145 | const values = parentIsGroup ? this.parent.parentValues[this.parent.mainBinding] : this.parentValues[this.mainBinding] 146 | 147 | file.meta.index = values.length 148 | await this.addItem() 149 | values[file.meta.index]['@' + this.jsonldKey] = file.meta.relativePath 150 | } 151 | 152 | async wrapper (innerTemplates: Array = [], isDisplayOnly = false) { 153 | if (isDisplayOnly) return super.wrapper(innerTemplates) 154 | 155 | const uppy = await this.getUppy(this.definition['@id'] + Language.l10nLanguage) 156 | 157 | const template = html` 158 |
{ 159 | const inner = uppy.rdfFormElement.querySelector('.uppy-Dashboard-inner') 160 | if (inner) { 161 | inner.style.width = 'auto' 162 | inner.style.height = 'auto' 163 | 164 | const addMore = inner.querySelector('.uppy-DashboardContent-addMore') 165 | 166 | addMore?.classList.add('button') 167 | addMore?.classList.add('primary') 168 | addMore?.classList.remove('uppy-DashboardContent-addMore') 169 | 170 | const plusIcon = addMore?.querySelector('.uppy-c-icon') 171 | plusIcon?.classList.add('icon') 172 | plusIcon?.classList.remove('uppy-c-icon') 173 | 174 | const back = inner.querySelector('.uppy-DashboardContent-back') 175 | back?.classList.remove('.uppy-DashboardContent-back') 176 | back?.classList.add('button') 177 | back?.classList.add('primary') 178 | 179 | } 180 | }} onclick=${[this.clickEvents.bind(this), { capture: true }]}>${uppy.rdfFormElement}
` 181 | return super.wrapper([template, ...innerTemplates]) 182 | } 183 | 184 | addButton () { 185 | return null 186 | } 187 | 188 | async item () { 189 | const uppy = await this.getUppy(this.definition['@id'] + Language.l10nLanguage) 190 | 191 | if (!instances.has(this) && this.value?._) { 192 | const url = new URL(this.value._) 193 | const name = url.pathname.substr(1) 194 | 195 | try { 196 | const fileId = uppy.addFile({ 197 | name, 198 | meta: { 199 | relativePath: this.value._, 200 | index: this.index 201 | }, 202 | data: '', 203 | isRemote: true, 204 | }) 205 | 206 | uppy.setFileState(fileId, { 207 | progress: { uploadComplete: true, uploadStarted: true } 208 | }) 209 | 210 | instances.add(this) 211 | } 212 | catch (exception) { 213 | console.log(exception) 214 | } 215 | } 216 | 217 | await this.refreshPreviews() 218 | return html`` 219 | } 220 | 221 | async refreshPreviews () { 222 | // const focalPointEnabled = this.definition['form:focalPoint']?.length > 0 223 | 224 | const uppy = await this.getUppy(this.definition['@id'] + Language.l10nLanguage) 225 | 226 | uppy.getFiles().forEach((file) => { 227 | if (!failedPreviews.has(file.meta.relativePath)) { 228 | const image = new Image() 229 | image.onload = () => uppy.setFileState(file.id, { preview: file.meta.relativePath }) 230 | image.onerror = () => failedPreviews.add(file.meta.relativePath) 231 | image.src = file.meta.relativePath 232 | } 233 | 234 | setTimeout(() => { 235 | const images = [...uppy.rdfFormElement.querySelectorAll('.uppy-Dashboard-Item-previewImg')] 236 | for (const image of images) this.attachImageEvents(image) 237 | }, 800) 238 | 239 | }) 240 | } 241 | 242 | } -------------------------------------------------------------------------------- /src/elements/WYSIWYG.ts: -------------------------------------------------------------------------------- 1 | import { Textarea } from './Textarea' 2 | import { init } from "../vendor/pell"; 3 | import { html } from 'uhtml/async' 4 | 5 | const modes = new Map() 6 | 7 | export class WYSIWYG extends Textarea { 8 | 9 | protected editor 10 | 11 | item () { 12 | const id = this.definition['@id'] + (this.index ?? 0) 13 | const mode = modes.get(id) ?? 'wysiwyg' 14 | 15 | let textValue = this.value?.['@value'] ?? '' 16 | if (typeof textValue === 'string') textValue.trim() 17 | 18 | if (!this.editor) { 19 | const element: HTMLDivElement = document.createElement('div') 20 | element.classList.add('wysiwyg-wrapper') 21 | this.editor = init({ 22 | element: element, 23 | actions: [ 24 | 'bold', 25 | 'italic', 26 | 'ulist', 27 | 'heading1', 28 | 'heading2', 29 | 'paragraph', 30 | 'link', 31 | { 32 | icon: '✎', 33 | title: 'Switch to HTML', 34 | result: async () => { 35 | modes.set(id, 'html') 36 | await this.render() 37 | } 38 | }, 39 | 40 | ], 41 | defaultParagraphSeparator: 'p', 42 | onChange: async html => { 43 | if (html) { 44 | if (!this.value) await this.addItem() 45 | this.value['@value'] = html 46 | } 47 | else { 48 | this.removeItem() 49 | } 50 | this.dispatchChange() 51 | }, 52 | }) 53 | 54 | /** @ts-ignore */ 55 | element.content.innerHTML = textValue 56 | } 57 | 58 | return mode === 'wysiwyg' ? this.editor : html` 59 | 64 | ${super.item()} 65 | ` 66 | } 67 | 68 | valueDisplay () { 69 | return html`
element.innerHTML = this.value?._}>
` 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/elements/Wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ElementBase } from './ElementBase' 2 | import { html } from 'uhtml/async' 3 | 4 | export class Wrapper extends ElementBase { 5 | 6 | constructor (...args) { 7 | super(...args) 8 | } 9 | 10 | input () { return html`` } 11 | 12 | } -------------------------------------------------------------------------------- /src/helpers/applyProxy.ts: -------------------------------------------------------------------------------- 1 | export const applyProxy = (url: string, proxy: string | null = null) => { 2 | url = url.replace('http:', location.protocol) 3 | if (proxy && !url.startsWith('/') && !url.startsWith('blob')) url = proxy + url 4 | return url 5 | } -------------------------------------------------------------------------------- /src/helpers/attributesDiff.ts: -------------------------------------------------------------------------------- 1 | export const attributesDiff = (attributes, callback: null | Function = null) => node => { 2 | for (const key of Object.keys(attributes)) { 3 | if (attributes[key]) { 4 | const attributeValue = Array.isArray(attributes[key]) ? attributes[key].join(' ') : attributes[key] 5 | if (typeof attributeValue !== 'string' || attributeValue.trim()) node.setAttribute(key, attributeValue) 6 | } 7 | else { 8 | node.removeAttribute(key) 9 | } 10 | } 11 | 12 | if (callback) { 13 | callback(node) 14 | callback = null 15 | } 16 | }; -------------------------------------------------------------------------------- /src/helpers/containerProxy.ts: -------------------------------------------------------------------------------- 1 | export const containerProxy = (data, mainBinding) => { 2 | return new Proxy(data, { 3 | get: function (_target, prop) { 4 | if (prop === '_proxyType') return 'containerProxy' 5 | if (!data[mainBinding]) return false 6 | return Reflect.get(data[mainBinding], prop) 7 | }, 8 | 9 | has: function (_target, prop) { 10 | if (!data[mainBinding]) return false 11 | return Reflect.has(data[mainBinding], prop) 12 | }, 13 | 14 | set: function (_target, prop, value) { 15 | if (!data[mainBinding]) { 16 | data[mainBinding] = [{'@list': []}] 17 | return true 18 | } 19 | else { 20 | return Reflect.set(data[mainBinding], prop, value) 21 | } 22 | } 23 | }) 24 | } -------------------------------------------------------------------------------- /src/helpers/createPixelArray.ts: -------------------------------------------------------------------------------- 1 | export function createPixelArray(pixels, quality = 0) { 2 | const pixelArray = []; 3 | 4 | for (let i = 0, offset, r, g, b, a; i < pixels.length + 1; i = i + quality) { 5 | offset = i * 4; 6 | r = pixels[offset + 0]; 7 | g = pixels[offset + 1]; 8 | b = pixels[offset + 2]; 9 | a = pixels[offset + 3]; 10 | 11 | // If pixel is mostly opaque and not white 12 | if (typeof a === 'undefined' || a >= 125) { 13 | if (!(r > 250 && g > 250 && b > 250)) { 14 | /** @ts-ignore */ 15 | pixelArray.push([r, g, b]); 16 | } 17 | } 18 | } 19 | return pixelArray; 20 | } -------------------------------------------------------------------------------- /src/helpers/dbpediaSuggestions.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../core/Language' 2 | 3 | /** 4 | * @param searchTerm 5 | */ 6 | export function dbpediaSuggestions (searchTerm: string) { 7 | // Add the following if you want to filter by dbpedia class: ?o dbo:ingredient ?uri . 8 | let querySearchTerm = searchTerm.trim() 9 | 10 | // The * does not work on dbpedia. 11 | 12 | const query = ` 13 | 14 | PREFIX rdfs: 15 | PREFIX dbo: 16 | PREFIX bif: 17 | 18 | SELECT DISTINCT ?uri ?label ?image { 19 | 20 | ?uri rdfs:label ?label . 21 | ?uri dbo:thumbnail ?image . 22 | ?label bif:contains '"${querySearchTerm}"' . 23 | ${Language.uiLanguage ? `filter langMatches(lang(?label), "${Language.uiLanguage}")` : ''} 24 | } 25 | 26 | LIMIT 10` 27 | 28 | return { query, source: { type: 'sparql', value: 'https://dbpedia.org/sparql' }} 29 | } -------------------------------------------------------------------------------- /src/helpers/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait, immediate = false) { 2 | var timeout; 3 | return function(this: any) { 4 | var context = this, args = arguments; 5 | var later = function() { 6 | timeout = null; 7 | if (!immediate) func.apply(context, args); 8 | }; 9 | var callNow = immediate && !timeout; 10 | clearTimeout(timeout); 11 | timeout = setTimeout(later, wait); 12 | if (callNow) func.apply(context, args); 13 | }; 14 | } -------------------------------------------------------------------------------- /src/helpers/expand.ts: -------------------------------------------------------------------------------- 1 | export const expand = (binding, context) => { 2 | const bindingSplit = binding.split(':') 3 | if (context[bindingSplit[0]]) { 4 | binding = context[bindingSplit[0]] + bindingSplit[1] 5 | } 6 | 7 | return binding 8 | } -------------------------------------------------------------------------------- /src/helpers/fa.ts: -------------------------------------------------------------------------------- 1 | import { Hole } from 'uhtml/async' 2 | import { icon } from '../vendor/fontawesome-svg-core.js' 3 | 4 | class FaIcon extends Hole { 5 | constructor(icon) { 6 | super('svg', [icon], []); 7 | } 8 | } 9 | export function fa (iconInput) { 10 | return new FaIcon(icon(iconInput).html[0]) 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/flatMapProxy.ts: -------------------------------------------------------------------------------- 1 | export const flatMapProxy = (data, binding) => { 2 | const flat = data.flatMap(item => item[binding]) 3 | 4 | return new Proxy(data, { 5 | get(_target, prop, receiver) { 6 | if (prop === '_proxyType') return 'flatMapProxy' 7 | return Reflect.get(flat, prop, receiver) 8 | }, 9 | 10 | has(_target, prop) { 11 | return Reflect.has(flat, prop) 12 | }, 13 | 14 | deleteProperty() { 15 | return true 16 | } 17 | }) 18 | } -------------------------------------------------------------------------------- /src/helpers/getImage.ts: -------------------------------------------------------------------------------- 1 | const cachedImageObjects = new WeakMap() 2 | 3 | export const getImage = (imageUrl): Promise => { 4 | return new Promise(resolve => { 5 | imageUrl = imageUrl.replace('http:', location.protocol) 6 | if (!cachedImageObjects.get(imageUrl)) { 7 | const image = document.createElement('img') 8 | image.crossOrigin = 'anonymous' 9 | image.onload = () => resolve(image) 10 | image.src = imageUrl 11 | } 12 | else { 13 | resolve(cachedImageObjects.get(imageUrl)) 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/getImageColor.ts: -------------------------------------------------------------------------------- 1 | import quantize from 'quantize' 2 | import { getImage } from './getImage' 3 | import { createPixelArray } from './createPixelArray' 4 | 5 | export const getImageColor = async (imageUrl) => { 6 | 7 | const image: HTMLImageElement = await getImage(imageUrl) 8 | 9 | const canvas = document.createElement('canvas') as HTMLCanvasElement 10 | const context = canvas.getContext('2d') 11 | if (!context) throw new Error('Could not get context') 12 | context.drawImage(image, 0, 0); 13 | 14 | const pixels = context.getImageData(0, 0, image.width, image.height) 15 | const pixelsData = createPixelArray(pixels.data, 8) 16 | const colorMap = quantize(pixelsData, 6) 17 | const colors = colorMap.palette() 18 | const color = `rgb(${colors[0].join(',')})` 19 | 20 | return { color } 21 | } -------------------------------------------------------------------------------- /src/helpers/getImageDimensionsByUrl.ts: -------------------------------------------------------------------------------- 1 | import { getImage } from './getImage' 2 | 3 | export const getImageDimensionsByUrl = async (imageUrl) => { 4 | 5 | const image: HTMLImageElement = await getImage(imageUrl) 6 | 7 | return { 8 | width: image.width, 9 | height: image.height 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/helpers/getUriMeta.ts: -------------------------------------------------------------------------------- 1 | const metas = new Map() 2 | import { lastPart } from './lastPart' 3 | import { Language } from '../core/Language' 4 | import { applyProxy } from './applyProxy' 5 | import { newEngine } from '../core/Comunica' 6 | import { streamToString } from './streamToString' 7 | 8 | export const getUriMeta = async (uri: string, proxy: string | null = null) => { 9 | if (!metas.get(uri + Language.uiLanguage)) { 10 | 11 | // DBPedia is broken. 12 | let improvedFetchUri 13 | let improvedCanonicalUri 14 | if (uri.includes('://dbpedia.org/resource/')) { 15 | improvedFetchUri = uri.replace('://dbpedia.org/resource/', '://dbpedia.org/data/') + '.jsonld' 16 | improvedCanonicalUri = uri.replace('https://', 'http://') 17 | } 18 | 19 | const engine = await newEngine() 20 | 21 | const response = await engine.query(`DESCRIBE <${improvedCanonicalUri ?? uri}>`, { 22 | sources: [ applyProxy(improvedFetchUri ?? uri, proxy) ] 23 | }) 24 | 25 | const { data } = await engine.resultToString(response, 'application/ld+json'); 26 | const resultString = await streamToString(data); 27 | 28 | let [json] = JSON.parse(resultString) 29 | 30 | if (!json) json = {} 31 | 32 | const meta = { 33 | label: null, 34 | thumbnail: null, 35 | } 36 | 37 | const labelLastParts = ['name', 'username', 'label'] 38 | const imageLastParts = ['thumbnail', 'depiction', 'image', 'img'] 39 | 40 | for (const labelLastPart of labelLastParts) { 41 | for (const [predicate, value] of Object.entries(json)) { 42 | if (!meta.label && labelLastPart === lastPart(predicate)) { 43 | const valueInPreferredLanguage = (value as Array).find(item => item['@language'] === Language.uiLanguage) 44 | const valueInUndeterminedLanguage = (value as Array).find(item => item['@language'] === 'und') 45 | /** @ts-ignore */ 46 | meta.label = valueInPreferredLanguage?.['@value'] ?? valueInUndeterminedLanguage?.['@value'] ?? value?.[0]?.['@value'] 47 | } 48 | 49 | if ((meta.label ?? '').substr(0, 2) === '_:') meta.label = null 50 | } 51 | } 52 | 53 | for (const imageLastPart of imageLastParts) { 54 | for (const [predicate, value] of Object.entries(json)) { 55 | if (!meta.thumbnail && imageLastPart === lastPart(predicate)) { 56 | const valueInPreferredLanguage = (value as Array).find(item => item['@language'] === Language.uiLanguage) 57 | const valueInUndeterminedLanguage = (value as Array).find(item => item['@language'] === 'und') 58 | meta.thumbnail = valueInPreferredLanguage?.['@value'] ?? valueInPreferredLanguage?.['@id'] ?? 59 | valueInUndeterminedLanguage?.['@value'] ?? valueInUndeterminedLanguage?.['@id'] ?? 60 | /** @ts-ignore */ 61 | value?.[0]?.['@value'] ?? value?.[0]?.['@id'] 62 | 63 | /** @ts-ignore */ 64 | if (meta.thumbnail?.substr(0, 2) === '_:') meta.thumbnail = false 65 | 66 | /** @ts-ignore */ 67 | if (!meta.thumbnail && value?.[0]?.['https://schema.org/url']?.[0]?.['@value']) { 68 | /** @ts-ignore */ 69 | meta.thumbnail = value[0]['https://schema.org/url'][0]['@value'] 70 | } 71 | } 72 | } 73 | } 74 | 75 | /** @ts-ignore */ 76 | if (!meta.label) meta.label = false 77 | /** @ts-ignore */ 78 | if (!meta.thumbnail) meta.thumbnail = false 79 | 80 | metas.set(uri + Language.uiLanguage, meta) 81 | } 82 | 83 | return metas.get(uri + Language.uiLanguage) 84 | } -------------------------------------------------------------------------------- /src/helpers/icons.ts: -------------------------------------------------------------------------------- 1 | // 'https://unpkg.com/@fortawesome/free-solid-svg-icons?module' 2 | 3 | export const faPencilAlt = { 4 | prefix:'fas', 5 | iconName:'pencil-alt', 6 | icon:[512,512,[],"f303","M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"] 7 | }; 8 | 9 | export const faCheck = { 10 | prefix:'fas', 11 | iconName:'check', 12 | icon:[512,512,[],"f00c","M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"] 13 | }; 14 | 15 | export const faReply = { 16 | prefix:'fas', 17 | iconName:'reply', 18 | icon:[512,512,[],"f3e5","M8.309 189.836L184.313 37.851C199.719 24.546 224 35.347 224 56.015v80.053c160.629 1.839 288 34.032 288 186.258 0 61.441-39.581 122.309-83.333 154.132-13.653 9.931-33.111-2.533-28.077-18.631 45.344-145.012-21.507-183.51-176.59-185.742V360c0 20.7-24.3 31.453-39.687 18.164l-176.004-152c-11.071-9.562-11.086-26.753 0-36.328z"] 19 | }; 20 | 21 | export const faTimes = { 22 | prefix:'fas', 23 | iconName:'times', 24 | icon:[352,512,[],"f00d","M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"] 25 | }; 26 | 27 | export const faPlus = { 28 | prefix:'fas', 29 | iconName:'plus', 30 | icon:[448,512,[],"f067","M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"] 31 | }; 32 | 33 | export const faLanguage = { 34 | prefix:'fas', 35 | iconName:'language', 36 | icon:[640,512,[],"f1ab","M152.1 236.2c-3.5-12.1-7.8-33.2-7.8-33.2h-.5s-4.3 21.1-7.8 33.2l-11.1 37.5H163zM616 96H336v320h280c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24zm-24 120c0 6.6-5.4 12-12 12h-11.4c-6.9 23.6-21.7 47.4-42.7 69.9 8.4 6.4 17.1 12.5 26.1 18 5.5 3.4 7.3 10.5 4.1 16.2l-7.9 13.9c-3.4 5.9-10.9 7.8-16.7 4.3-12.6-7.8-24.5-16.1-35.4-24.9-10.9 8.7-22.7 17.1-35.4 24.9-5.8 3.5-13.3 1.6-16.7-4.3l-7.9-13.9c-3.2-5.6-1.4-12.8 4.2-16.2 9.3-5.7 18-11.7 26.1-18-7.9-8.4-14.9-17-21-25.7-4-5.7-2.2-13.6 3.7-17.1l6.5-3.9 7.3-4.3c5.4-3.2 12.4-1.7 16 3.4 5 7 10.8 14 17.4 20.9 13.5-14.2 23.8-28.9 30-43.2H412c-6.6 0-12-5.4-12-12v-16c0-6.6 5.4-12 12-12h64v-16c0-6.6 5.4-12 12-12h16c6.6 0 12 5.4 12 12v16h64c6.6 0 12 5.4 12 12zM0 120v272c0 13.3 10.7 24 24 24h280V96H24c-13.3 0-24 10.7-24 24zm58.9 216.1L116.4 167c1.7-4.9 6.2-8.1 11.4-8.1h32.5c5.1 0 9.7 3.3 11.4 8.1l57.5 169.1c2.6 7.8-3.1 15.9-11.4 15.9h-22.9a12 12 0 0 1-11.5-8.6l-9.4-31.9h-60.2l-9.1 31.8c-1.5 5.1-6.2 8.7-11.5 8.7H70.3c-8.2 0-14-8.1-11.4-15.9z"] 37 | }; 38 | 39 | export const faGlobe = { 40 | prefix:'fas', 41 | iconName:'globe', 42 | icon:[496,512,[],"f0ac","M336.5 160C322 70.7 287.8 8 248 8s-74 62.7-88.5 152h177zM152 256c0 22.2 1.2 43.5 3.3 64h185.3c2.1-20.5 3.3-41.8 3.3-64s-1.2-43.5-3.3-64H155.3c-2.1 20.5-3.3 41.8-3.3 64zm324.7-96c-28.6-67.9-86.5-120.4-158-141.6 24.4 33.8 41.2 84.7 50 141.6h108zM177.2 18.4C105.8 39.6 47.8 92.1 19.3 160h108c8.7-56.9 25.5-107.8 49.9-141.6zM487.4 192H372.7c2.1 21 3.3 42.5 3.3 64s-1.2 43-3.3 64h114.6c5.5-20.5 8.6-41.8 8.6-64s-3.1-43.5-8.5-64zM120 256c0-21.5 1.2-43 3.3-64H8.6C3.2 212.5 0 233.8 0 256s3.2 43.5 8.6 64h114.6c-2-21-3.2-42.5-3.2-64zm39.5 96c14.5 89.3 48.7 152 88.5 152s74-62.7 88.5-152h-177zm159.3 141.6c71.4-21.2 129.4-73.7 158-141.6h-108c-8.8 56.9-25.6 107.8-50 141.6zM19.3 352c28.6 67.9 86.5 120.4 158 141.6-24.4-33.8-41.2-84.7-50-141.6h-108z"] 43 | }; 44 | -------------------------------------------------------------------------------- /src/helpers/importGlobalScript.ts: -------------------------------------------------------------------------------- 1 | const imported = new Set() 2 | 3 | export const importGlobalScript = async (url: string, name: string) => { 4 | if (imported.has(name) || window[name]) return Promise.resolve(window[name]) 5 | 6 | return new Promise((resolve) => { 7 | const script = document.createElement('script') 8 | script.src = url 9 | script.onload = () => { 10 | imported.add(name) 11 | resolve(window[name]) 12 | } 13 | document.head.appendChild(script) 14 | }) 15 | } -------------------------------------------------------------------------------- /src/helpers/isFetchable.ts: -------------------------------------------------------------------------------- 1 | export const isFetchable = (string) => { 2 | return string.startsWith('http') || string.startsWith('blob') || string.substr(0, 1) === '/' 3 | } -------------------------------------------------------------------------------- /src/helpers/kebabize.ts: -------------------------------------------------------------------------------- 1 | export const kebabize = str => { 2 | if (str.split('').every(letter => letter.toUpperCase() === letter)) return str.toLowerCase() 3 | return str.split('').map((letter, index) => { 4 | return letter.toUpperCase() === letter 5 | ? `${index !== 0 ? '-' : ''}${letter.toLowerCase()}` 6 | : letter; 7 | }).join(''); 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/lastPart.ts: -------------------------------------------------------------------------------- 1 | export const lastPart = (text) => { 2 | return text.split(/\:|\/|\,|\#/).pop() 3 | } -------------------------------------------------------------------------------- /src/helpers/onlyUnique.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * May be used for filtering array's. 3 | * @param value 4 | * @param index 5 | * @param self 6 | */ 7 | export function onlyUnique(value, index, self) { 8 | return self.indexOf(value) === index && value; 9 | } -------------------------------------------------------------------------------- /src/helpers/searchSuggestionsSparqlQuery.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../core/Language' 2 | 3 | /** 4 | * 5 | * @param query 6 | * @param source 7 | * @param searchTerm 8 | */ 9 | export function searchSuggestionsSparqlQuery (query = '', source: string | any = null, searchTerm: string = '') { 10 | if (searchTerm === '' || (searchTerm.length < 4)) return {} 11 | let querySearchTerm = searchTerm.trim() 12 | 13 | if (source) { 14 | source = { type: 'sparql', value: source } 15 | } 16 | 17 | if (!source) source = { type: 'sparql', value: 'https://dbpedia.org/sparql' } 18 | 19 | // if (source?.type === 'sparql' && querySearchTerm.length > 4) querySearchTerm += '*' 20 | 21 | if (!query) { 22 | query = ` 23 | PREFIX rdfs: 24 | 25 | SELECT DISTINCT ?uri ?label { 26 | ?uri rdfs:label ?label . 27 | FILTER(contains(?label, """SEARCH_TERM""")) 28 | } 29 | 30 | LIMIT 10` 31 | } 32 | 33 | if (typeof source === 'string') { 34 | source = (source as string).replace(/LANGUAGE/g, Language.uiLanguage) 35 | source = (source as string).replace(/SEARCH_TERM/g, querySearchTerm) 36 | } 37 | 38 | query = query.replace(/LANGUAGE/g, Language.uiLanguage) 39 | query = query.replace(/SEARCH_TERM/g, querySearchTerm) 40 | 41 | return { query, source } 42 | } -------------------------------------------------------------------------------- /src/helpers/sparqlQueryToList.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../core/Language' 2 | import { newEngine } from '../core/Comunica' 3 | import { ProxyHandlerStatic } from '../vendor/ProxyHandlerStatic-browser' 4 | /** 5 | * @param query 6 | * @param source 7 | * @param comunica 8 | */ 9 | export async function sparqlQueryToList (query, source, proxy: string | undefined = undefined) { 10 | // TODO maybe use tokens that will less likely collide. 11 | query = query.toString().replace(/LANGUAGE/g, Language.uiLanguage) 12 | if (typeof source === 'object' && source instanceof String) source = source.toString() 13 | 14 | if (typeof source === 'string') source.replace('http:', location.protocol) 15 | 16 | const options : { sources: any, httpProxyHandler: any } = { sources: [source], httpProxyHandler: null } 17 | if (proxy) options.httpProxyHandler = new ProxyHandlerStatic(proxy) 18 | const engine = newEngine() 19 | const result = await engine.query(query, options); 20 | 21 | /** @ts-ignore */ 22 | const bindings = await result.bindings() 23 | 24 | const items: Map = new Map() 25 | 26 | for (const binding of bindings) { 27 | let label = binding.get('?label')?.id ?? binding.get('?label')?.value 28 | const valueAndLanguage = label.split('@') 29 | 30 | if (label[0] === '"' && label[label.length - 1] === '"') { 31 | label = label.substr(1, label.length - 2) 32 | } 33 | 34 | if (valueAndLanguage.length > 1) { 35 | label = {} 36 | label[valueAndLanguage[1].trim('"')] = valueAndLanguage[0].slice(1, -1) 37 | } 38 | 39 | const uri = binding.get('?uri')?.value 40 | let image = binding.get('?image')?.value 41 | let group = binding.get('?group')?.value 42 | 43 | if (!items.get(uri)) { 44 | items.set(uri, { label, uri, image, group }) 45 | } 46 | else { 47 | const existingItem = items.get(uri) 48 | if (typeof existingItem.label === 'string' && typeof label === 'string') { 49 | existingItem.label = label 50 | } 51 | else { 52 | Object.assign(existingItem.label, label) 53 | } 54 | items.set(uri, existingItem) 55 | } 56 | } 57 | 58 | return [...items.values()] 59 | } 60 | -------------------------------------------------------------------------------- /src/helpers/streamToString.ts: -------------------------------------------------------------------------------- 1 | export const streamToString = (stream: NodeJS.ReadableStream): any => { 2 | return new Promise((resolve) => { 3 | let output = '' 4 | stream.on('data', (part) => output += part) 5 | stream.on('end', () => { 6 | resolve(output) 7 | }) 8 | }) 9 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { FormDefinition } from './core/FormDefinition' 2 | import { RdfFormData } from './core/RdfFormData' 3 | import { Registry } from './core/Registry' 4 | import { Renderer } from './core/Renderer' 5 | import { Language, LanguageService, t } from './core/Language' 6 | import { CoreComponent } from './types/CoreComponent' 7 | import { expandProxiesInConsole } from './core/Debug' 8 | export { JsonLdProxy } from './core/JsonLdProxy' 9 | export { languages } from './languages.js' 10 | export { ElementBase } from './elements/ElementBase' 11 | export { t } from './core/Language' 12 | 13 | export class RdfForm extends HTMLElement implements CoreComponent { 14 | public language: LanguageService 15 | public formDefinition: FormDefinition 16 | public formData: RdfFormData 17 | public registry: Registry 18 | public renderer: Renderer 19 | public proxy: string | null 20 | public ready: boolean = false 21 | public shadow: any 22 | public t: any 23 | 24 | constructor () { 25 | super() 26 | } 27 | 28 | static get observedAttributes() { return [ 29 | 'ui-languages', 30 | 'selected-l10n-language', 31 | 'selected-language' 32 | ]; } 33 | 34 | async disconnectedCallback () { 35 | this.dispatchEvent(new CustomEvent('destroy')) 36 | } 37 | 38 | async attributeChangedCallback(name, _oldValue, newValue) { 39 | if (!this.ready) return 40 | 41 | if (name === 'form' && newValue) { 42 | this.formDefinition = new FormDefinition(this) 43 | await this.formDefinition.init() 44 | } 45 | 46 | if (name === 'data' && newValue) { 47 | this.formData = new RdfFormData(this) 48 | await this.formData.init() 49 | } 50 | 51 | if (name === 'selected-l10n-language') { 52 | this.language.l10nLanguage = newValue 53 | await this.language.init(this) 54 | } 55 | 56 | this.renderer.render() 57 | } 58 | 59 | async connectedCallback () { 60 | if (this.shadow) return 61 | this.shadow = this.attachShadow({ mode: 'open' }) 62 | this.formDefinition = new FormDefinition(this) 63 | this.formData = new RdfFormData(this) 64 | this.registry = new Registry(this) 65 | this.renderer = new Renderer(this) 66 | this.language = Language 67 | this.language.addEventListener('indexing-languages', (event: Event) => this.dispatchEvent(new CustomEvent('indexing-languages', { 68 | detail: (event as CustomEvent).detail 69 | }))) 70 | this.language.addEventListener('l10n-change', (event: Event) => this.dispatchEvent(new CustomEvent('l10n-change', { 71 | detail: (event as CustomEvent).detail 72 | }))) 73 | await this.language.init(this) 74 | this.t = t 75 | this.proxy = this.getAttribute('proxy') 76 | 77 | 78 | if (this.getAttribute('debug') !== null) expandProxiesInConsole() 79 | 80 | const components = [ 81 | this.formDefinition, 82 | this.formData, 83 | this.registry, 84 | this.language, 85 | this.renderer 86 | ] 87 | 88 | for (const component of components) { 89 | component.addEventListener('ready', () => { 90 | if (components.every(component => component.ready) && !this.ready) { 91 | this.ready = true 92 | this.renderer.render() 93 | this.dispatchEvent(new CustomEvent('ready', { 94 | detail: { 95 | proxy: this.formData.proxy, 96 | expanded: this.formData.proxy.$, 97 | } 98 | })) 99 | } 100 | }, { once: true }) 101 | } 102 | } 103 | } 104 | 105 | customElements.define('rdf-form', RdfForm); 106 | -------------------------------------------------------------------------------- /src/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Checkbox } from './elements/Checkbox' 2 | import { Color } from './elements/Color' 3 | import { Container } from './elements/Container' 4 | import { Date } from './elements/Date' 5 | import { Details } from './elements/Details' 6 | import { Dropdown } from './elements/Dropdown' 7 | import { Group } from './elements/Group' 8 | import { LanguagePicker } from './elements/LanguagePicker' 9 | import { Mail } from './elements/Mail' 10 | import { Number } from './elements/Number' 11 | import { Reference } from './elements/Reference' 12 | import { String } from './elements/String' 13 | import { Textarea } from './elements/Textarea' 14 | import { Unknown } from './elements/Unknown' 15 | import { Url } from './elements/Url' 16 | import { UrlImage } from './elements/UrlImage' 17 | import { UrlUppy } from './elements/UrlUppy' 18 | import { WYSIWYG } from './elements/WYSIWYG' 19 | import { Wrapper } from './elements/Wrapper' 20 | 21 | export default { 22 | 'checkbox': Checkbox, 23 | 'color': Color, 24 | 'container': Container, 25 | 'date': Date, 26 | 'details': Details, 27 | 'dropdown': Dropdown, 28 | 'group': Group, 29 | 'language-picker': LanguagePicker, 30 | 'mail': Mail, 31 | 'number': Number, 32 | 'reference': Reference, 33 | 'string': String, 34 | 'textarea': Textarea, 35 | 'unknown': Unknown, 36 | 'url': Url, 37 | 'url-image': UrlImage, 38 | 'url-uppy': UrlUppy, 39 | 'wysiwyg': WYSIWYG, 40 | 'wrapper': Wrapper 41 | } 42 | -------------------------------------------------------------------------------- /src/scss/_layout.scss: -------------------------------------------------------------------------------- 1 | :host form { 2 | .form-element[name="main"] { 3 | width: calc(100% - 440px); 4 | float: left; 5 | margin-right: 40px; 6 | } 7 | 8 | .form-element[name="sidebar"] { 9 | width: 400px; 10 | float: right; 11 | } 12 | 13 | .actions { 14 | flex-direction: row-reverse; 15 | float: left; 16 | clear: both; 17 | margin-bottom: 40px; 18 | width: 100%; 19 | border-top: 1px solid var(--color-gray); 20 | padding: 20px 0; 21 | position: sticky; 22 | bottom: 0; 23 | background: white; 24 | margin-top: 20px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/components/_actions.scss: -------------------------------------------------------------------------------- 1 | .actions { 2 | display: flex; 3 | gap : 10px; 4 | position: relative; 5 | z-index: 10000; 6 | 7 | &.top { 8 | > .language-switcher { 9 | flex: 0 0 130px; 10 | margin-left: auto; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/components/_base.scss: -------------------------------------------------------------------------------- 1 | input:not(.uppy-Url-input), textarea, select { 2 | flex: 1 1 auto; 3 | padding: 8px; 4 | border: 1px solid var(--color-gray); 5 | border-radius: var(--radius); 6 | } 7 | 8 | textarea { 9 | resize: vertical; 10 | } 11 | 12 | input[readonly] { 13 | background: none; 14 | border: none; 15 | position: relative; 16 | top: 1px; 17 | outline: none; 18 | padding: 0; 19 | line-height: 30px;; 20 | } 21 | 22 | :host > * { 23 | margin-bottom: 25px; 24 | } 25 | 26 | * { 27 | box-sizing: border-box; 28 | } 29 | 30 | .svg-inline--fa { 31 | &.fa-w-8 { width: 8px; } 32 | &.fa-w-10 { width: 10px; } 33 | &.fa-w-11 { width: 11px; } 34 | &.fa-w-14 { width: 14px; } 35 | &.fa-w-16 { width: 16px; } 36 | &.fa-w-20 { width: 20px; } 37 | } -------------------------------------------------------------------------------- /src/scss/components/_button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 10px; 3 | border-radius: var(--radius); 4 | background: var(--color-gray); 5 | cursor: pointer; 6 | border: none; 7 | position: relative; 8 | overflow: hidden; 9 | white-space: nowrap; 10 | display: flex; 11 | gap: 10px; 12 | 13 | &.big { 14 | padding: 20px 40px; 15 | font-size: 18px; 16 | font-weight: bold; 17 | } 18 | 19 | &.primary { 20 | background: var(--color-primary); 21 | } 22 | 23 | &.secondary { 24 | background: var(--color-secondary); 25 | } 26 | 27 | &.danger { 28 | background: var(--color-danger); 29 | color: white; 30 | 31 | svg path { 32 | fill: white; 33 | } 34 | } 35 | 36 | svg { 37 | display: block; 38 | pointer-events: none; 39 | } 40 | 41 | &.is-working { 42 | position: relative; 43 | background: var(--color-primary-darker); 44 | color: white; 45 | 46 | &:after { 47 | content: ""; 48 | position: absolute; 49 | top: 0; left: 0; bottom: 0; right: 0; 50 | background-image: linear-gradient( 51 | -45deg, 52 | rgba(255, 255, 255, .2) 25%, 53 | transparent 25%, 54 | transparent 50%, 55 | rgba(255, 255, 255, .2) 50%, 56 | rgba(255, 255, 255, .2) 75%, 57 | transparent 75%, 58 | transparent 59 | ); 60 | z-index: 1; 61 | background-size: 37px; 62 | animation: move 2s linear infinite; 63 | overflow: hidden; 64 | } 65 | } 66 | 67 | &:after { 68 | content: ''; 69 | display: block; 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | width: 100%; 74 | height: 100%; 75 | background-color: black; 76 | opacity: 0; 77 | pointer-events: none; 78 | transition: opacity .2s ease-in-out; 79 | } 80 | 81 | &:hover { 82 | &:after { 83 | opacity: .2; 84 | } 85 | } 86 | } 87 | 88 | @keyframes move { 89 | 0% { 90 | background-position: 0 0; 91 | } 92 | 100% { 93 | background-position: 50px 50px; 94 | } 95 | } 96 | 97 | .icon-button { 98 | cursor: pointer; 99 | 100 | svg { 101 | position: relative; 102 | top: 3px; 103 | } 104 | } -------------------------------------------------------------------------------- /src/scss/components/_checkbox-label.scss: -------------------------------------------------------------------------------- 1 | .checkbox-label { 2 | flex: 1 1 100%; 3 | cursor: pointer; 4 | 5 | > input { 6 | margin-left: 0; 7 | } 8 | } -------------------------------------------------------------------------------- /src/scss/components/_form-element.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | font-weight: bold; 3 | padding-bottom: 10px; 4 | display: flex; 5 | align-items: flex-end; 6 | 7 | small { 8 | color: var(--color-gray-medium); 9 | font-weight: normal; 10 | } 11 | } 12 | 13 | .label-required-star { 14 | margin-left: 5px; 15 | } 16 | 17 | .form-element { 18 | display: flex; 19 | position: relative; 20 | flex-direction: column; 21 | margin-bottom: 10px; 22 | flex: 1 1 auto; 23 | 24 | .description { 25 | font-size: 14px; 26 | margin-bottom: 10px; 27 | margin-inline: 5px; 28 | line-height: 22px; 29 | font-style: italic; 30 | color: var(--color-gray-medium); 31 | } 32 | 33 | > .label { 34 | height: 46px; 35 | } 36 | 37 | textarea:invalid, 38 | input:invalid { 39 | box-shadow: none; 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/scss/components/_items.scss: -------------------------------------------------------------------------------- 1 | .items { 2 | padding: 5px; 3 | margin-bottom: 10px; 4 | background: var(--background-color); 5 | border-radius: var(--radius); 6 | } 7 | 8 | .item { 9 | display: flex; 10 | gap: 10px; 11 | flex-wrap: wrap; 12 | padding: 5px; 13 | position: relative; 14 | align-items: flex-start; 15 | 16 | &:not(:last-child) { 17 | border-bottom: 1px dashed var(--color-gray); 18 | } 19 | 20 | &:after { 21 | content: ''; 22 | display: block; 23 | position: absolute; 24 | top: 5px; 25 | left: 3px; 26 | width: calc(100% - 6px); 27 | height: calc(100% - 10px); 28 | pointer-events: none; 29 | } 30 | 31 | &[loading="true"] { 32 | &:after { 33 | background: linear-gradient( 34 | -45deg, 35 | rgba(200, 200, 200, .2) 25%, 36 | transparent 25%, 37 | transparent 50%, 38 | rgba(200, 200, 200, .2) 50%, 39 | rgba(200, 200, 200, .2) 75%, 40 | transparent 75%, 41 | transparent 42 | ); 43 | z-index: 1; 44 | background-size: 50px 50px; 45 | animation: move 2s linear infinite; 46 | } 47 | } 48 | 49 | > .button { 50 | height: 35px; 51 | width: 35px; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | } 56 | 57 | .sub-item { 58 | flex: 1 1 10%; 59 | 60 | select { 61 | width: 100%; 62 | } 63 | 64 | input { 65 | box-sizing: content-box; 66 | width: calc(100% - 20px); 67 | } 68 | } 69 | } 70 | 71 | .item-footer { 72 | flex: 0 0 100%; 73 | } 74 | 75 | @keyframes move { 76 | 0% { 77 | background-position: 0 0; 78 | } 79 | 100% { 80 | background-position: 50px 50px; 81 | } 82 | } 83 | 84 | .form-element.no-label > .items { 85 | padding: 0; 86 | margin-inline: -5px; 87 | } -------------------------------------------------------------------------------- /src/scss/components/_pell.scss: -------------------------------------------------------------------------------- 1 | $pell-actionbar-color: #FFF !default; 2 | $pell-border-color: rgba(10, 10, 10, 0.1) !default; 3 | $pell-border-style: solid !default; 4 | $pell-border-width: 1px !default; 5 | $pell-button-height: 30px !default; 6 | $pell-button-selected-color: #F0F0F0 !default; 7 | $pell-button-width: 30px !default; 8 | $pell-content-height: 300px !default; 9 | $pell-content-padding: 10px !default; 10 | 11 | .pell { 12 | border: $pell-border-width $pell-border-style $pell-border-color; 13 | box-sizing: border-box; 14 | } 15 | 16 | .pell-content { 17 | box-sizing: border-box; 18 | height: $pell-content-height; 19 | outline: 0; 20 | overflow-y: auto; 21 | padding: $pell-content-padding; 22 | } 23 | 24 | .pell-actionbar { 25 | background-color: $pell-actionbar-color; 26 | border-bottom: $pell-border-width $pell-border-style $pell-border-color; 27 | } 28 | 29 | .pell-button { 30 | background-color: transparent; 31 | border: none; 32 | cursor: pointer; 33 | height: $pell-button-height; 34 | outline: 0; 35 | width: $pell-button-width; 36 | vertical-align: bottom; 37 | } 38 | 39 | .pell-button-selected { 40 | background-color: $pell-button-selected-color; 41 | } 42 | -------------------------------------------------------------------------------- /src/scss/components/_rdf-form.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | --radius: 4px; 3 | --background-color: #eef6f4; 4 | --background-color-secondary: #dbdbdb; 5 | --color-gray-light: #ecebeb; 6 | --color-gray: #c3c3c3; 7 | --color-gray-dark: #827575; 8 | --color-gray-blue: #f3f5f9; 9 | --color-gray-medium: #908989; 10 | --color-primary: #99e9ab; 11 | --color-primary-darker: #45a35b; 12 | --color-secondary: #b0c6b4; 13 | --color-danger: #cd4d4d; 14 | --color-text: #102406; 15 | 16 | font-family: "Helvetica Neue", sans-serif; 17 | margin: 0 auto; 18 | display: block; 19 | color: var(--color-text); 20 | } 21 | -------------------------------------------------------------------------------- /src/scss/components/_search-suggestions.scss: -------------------------------------------------------------------------------- 1 | .search-suggestions { 2 | flex: 0 0 100%; 3 | padding: 0; 4 | background: white; 5 | margin: 0; 6 | margin-top: 10px; 7 | list-style: none; 8 | border: 1px solid var(--color-gray); 9 | border-radius: var(--radius); 10 | overflow: hidden; 11 | 12 | .search-suggestion { 13 | display: flex; 14 | align-items: center; 15 | 16 | .image { 17 | width: 40px; 18 | height: 40px; 19 | display: block; 20 | margin-right: 10px; 21 | background: var(--color-gray); 22 | 23 | img { 24 | width: 40px; 25 | height: 40px; 26 | object-fit: cover; 27 | object-position: center; 28 | display: block; 29 | } 30 | } 31 | 32 | .title { 33 | padding: 10px; 34 | } 35 | 36 | &:not(.no-results):hover { 37 | cursor: pointer; 38 | background: var(--background-color); 39 | } 40 | 41 | &.no-results { 42 | cursor: default; 43 | } 44 | } 45 | 46 | .search-suggestion + .search-suggestion { 47 | border-top: 1px dashed var(--color-gray); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/scss/components/_select.scss: -------------------------------------------------------------------------------- 1 | select { 2 | appearance: none; 3 | cursor: pointer; 4 | background: white; 5 | 6 | &:not([multiple]) { 7 | background-image: url('data:image/svg+xml;utf8,'); 8 | background-repeat: no-repeat; 9 | background-size: 14px; 10 | background-position: calc(100% - 10px) center; 11 | padding-right: 30px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/components/_slim-select.scss: -------------------------------------------------------------------------------- 1 | .ss-main { 2 | position: relative; 3 | display: inline-block; 4 | user-select: none; 5 | color: var(--color-text); 6 | width: 100%; 7 | 8 | .ss-single-selected { 9 | display: flex; 10 | cursor: pointer; 11 | width: 100%; 12 | padding: 10px; 13 | border: 1px solid var(--color-gray); 14 | background-color: white; 15 | outline: 0; 16 | box-sizing: border-box; 17 | transition: background-color .2s; 18 | 19 | &.ss-disabled { 20 | background-color: var(--color-gray-light); 21 | cursor: not-allowed; 22 | } 23 | 24 | &.ss-open-above { 25 | border-top-left-radius: 0; 26 | border-top-right-radius: 0; 27 | } 28 | &.ss-open-below { 29 | border-bottom-left-radius: 0; 30 | border-bottom-right-radius: 0; 31 | } 32 | 33 | .placeholder { 34 | display: flex; 35 | flex: 1 1 100%; 36 | align-items: center; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | white-space: nowrap; 40 | text-align: left; 41 | width: calc(100% - 30px); 42 | line-height: 1em; 43 | -webkit-user-select: none; 44 | -moz-user-select: none; 45 | -ms-user-select: none; 46 | user-select: none; 47 | 48 | * { 49 | display: flex; 50 | align-items: center; 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | width: auto; 55 | } 56 | 57 | .ss-disabled { 58 | color: var(--color-gray-light); 59 | } 60 | } 61 | 62 | .ss-deselect { 63 | display: flex; 64 | align-items: center; 65 | justify-content: flex-end; 66 | flex: 0 1 auto; 67 | margin: 0 6px 0 6px; 68 | font-weight: bold; 69 | 70 | &.ss-hide { 71 | display: none; 72 | } 73 | } 74 | 75 | .ss-arrow { 76 | display: flex; 77 | align-items: center; 78 | justify-content: flex-end; 79 | flex: 0 1 auto; 80 | margin: 0 6px 0 6px; 81 | 82 | span { 83 | border: solid var(--color-text); 84 | border-width: 0 2px 2px 0; 85 | display: inline-block; 86 | padding: 3px; 87 | transition: transform .2s, margin .2s; 88 | 89 | &.arrow-up { 90 | transform: rotate(-135deg); 91 | margin: 3px 0 0 0; 92 | } 93 | &.arrow-down { 94 | transform: rotate(45deg); 95 | margin: -3px 0 0 0; 96 | } 97 | } 98 | } 99 | } 100 | 101 | .ss-multi-selected { 102 | display: flex; 103 | min-height: 33px; 104 | flex-direction: row; 105 | border: solid 1px var(--color-gray); 106 | background: white; 107 | border-radius: var(--radius); 108 | 109 | &.ss-disabled { 110 | background-color: var(--color-gray-light); 111 | cursor: not-allowed; 112 | 113 | .ss-values { 114 | .ss-disabled { 115 | color: var(--color-text); 116 | } 117 | 118 | .ss-value { 119 | .ss-value-delete { 120 | cursor: not-allowed; 121 | } 122 | } 123 | } 124 | } 125 | 126 | &.ss-open-above { 127 | border-top-left-radius: 0; 128 | border-top-right-radius: 0; 129 | } 130 | &.ss-open-below { 131 | border-bottom-left-radius: 0; 132 | border-bottom-right-radius: 0; 133 | } 134 | 135 | .ss-values { 136 | display: flex; 137 | flex-wrap: wrap; 138 | justify-content: flex-start; 139 | flex: 1 1 100%; 140 | padding: 4px; 141 | width: calc(100% - 30px); 142 | 143 | .ss-disabled { 144 | display: flex; 145 | padding: 4px 5px; 146 | font-size: 16px; 147 | margin: 2px 0; 148 | line-height: 1em; 149 | align-items: center; 150 | width: 100%; 151 | color: var(--color-gray-light); 152 | overflow: hidden; 153 | text-overflow: ellipsis; 154 | white-space: nowrap; 155 | } 156 | 157 | .ss-value { 158 | display: flex; 159 | user-select: none; 160 | align-items: center; 161 | font-size: 12px; 162 | padding: 3px 6px; 163 | margin: 2px 5px 2px 0; 164 | color: var(--text); 165 | background-color: var(--color-gray); 166 | border-radius: var(--radius); 167 | 168 | .ss-value-delete { 169 | margin: 0 0 0 5px; 170 | cursor: pointer; 171 | } 172 | } 173 | } 174 | 175 | .ss-add { 176 | display: flex; 177 | flex: 0 1 3px; 178 | align-items: center; 179 | margin-right: 16px; 180 | 181 | .ss-plus { 182 | display: flex; 183 | justify-content: center; 184 | align-items: center; 185 | background: var(--color-gray-medium); 186 | position: relative; 187 | height: 15px; 188 | width: 3px; 189 | transition: transform .2s; 190 | 191 | &:after { 192 | background: var(--color-gray-medium); 193 | content: ""; 194 | position: absolute; 195 | height: 3px; 196 | width: 15px; 197 | left: -6px; 198 | top: 6px; 199 | } 200 | 201 | &.ss-cross { 202 | transform: rotate(45deg); 203 | } 204 | } 205 | } 206 | 207 | } 208 | } 209 | .ss-content { 210 | position: absolute; 211 | width: 100%; 212 | margin: -1px 0 0 0; 213 | box-sizing: border-box; 214 | border: solid 1px var(--color-gray); 215 | z-index: 1010; 216 | background-color: white; 217 | transform-origin: center top; 218 | transition: transform .2s, opacity .2s; 219 | opacity: 0; 220 | transform: scaleY(0); 221 | 222 | &.ss-open { 223 | display: block; 224 | opacity: 1; 225 | transform: scaleY(1); 226 | } 227 | 228 | .ss-search { 229 | display: flex; 230 | flex-direction: row; 231 | 232 | &.ss-hide { 233 | height: 0; 234 | opacity: 0; 235 | padding: 0; 236 | margin: 0; 237 | 238 | input { 239 | height: 0; 240 | opacity: 0; 241 | padding: 0; 242 | margin: 0; 243 | } 244 | } 245 | 246 | input { 247 | display: inline-flex; 248 | font-size: inherit; 249 | line-height: inherit; 250 | flex: 1 1 auto; 251 | width: 100%; 252 | min-width: 0; 253 | height: 30px; 254 | padding: 6px 8px; 255 | margin: 0; 256 | border: none; 257 | background-color: white; 258 | outline: 0; 259 | text-align: left; 260 | box-sizing: border-box; 261 | -webkit-box-sizing: border-box; 262 | -webkit-appearance: textfield; 263 | 264 | &:focus { 265 | box-shadow: none; 266 | } 267 | 268 | &::placeholder { 269 | vertical-align: middle; 270 | } 271 | } 272 | 273 | .ss-addable { 274 | display: inline-flex; 275 | justify-content: center; 276 | align-items: center; 277 | cursor: pointer; 278 | font-size: 22px; 279 | font-weight: bold; 280 | flex: 0 0 30px; 281 | height: 30px; 282 | color: var(--color-gray-medium); 283 | margin: 0 3px 0 12px; 284 | border-radius: var(--radius); 285 | box-sizing: border-box; 286 | } 287 | } 288 | 289 | .ss-addable { 290 | padding-top: 0; 291 | } 292 | 293 | .ss-list { 294 | max-height: 200px; 295 | overflow-x: hidden; 296 | overflow-y: auto; 297 | text-align: left; 298 | 299 | .ss-optgroup { 300 | .ss-optgroup-label { 301 | padding: 6px 10px 6px 10px; 302 | font-weight: bold; 303 | } 304 | 305 | .ss-option { 306 | padding: 6px 6px 6px 25px; 307 | } 308 | } 309 | 310 | .ss-optgroup-label-selectable { 311 | cursor: pointer; 312 | 313 | &:hover { 314 | color: var(--text); 315 | background-color: var(--color-gray); 316 | } 317 | } 318 | 319 | .ss-option { 320 | padding: 6px 10px 6px 10px; 321 | cursor: pointer; 322 | user-select: none; 323 | 324 | * { 325 | display: inline-block; 326 | } 327 | 328 | &:hover, &.ss-highlighted { 329 | color: var(--text); 330 | background-color: var(--color-gray); 331 | } 332 | 333 | &.ss-disabled { 334 | cursor: not-allowed; 335 | color: var(--color-gray-light); 336 | background-color: white; 337 | } 338 | 339 | &:not(.ss-disabled).ss-option-selected { 340 | color: var(--text); 341 | background-color: rgba(var(--color-gray), .1); 342 | } 343 | 344 | &.ss-hide { display: none; } 345 | 346 | .ss-search-highlight { 347 | background-color: var(--color-gray); 348 | } 349 | } 350 | } 351 | } 352 | 353 | .ss-value-delete { 354 | svg { 355 | position: relative; 356 | top: 2px; 357 | margin-top: -4px; 358 | width: 8px !important; 359 | 360 | path { 361 | fill: var(--color-text); 362 | } 363 | } 364 | } 365 | 366 | -------------------------------------------------------------------------------- /src/scss/components/_uppy.scss: -------------------------------------------------------------------------------- 1 | .uppy-Dashboard-inner { 2 | width: auto !important; 3 | border: none !important; 4 | background-color: transparent !important; 5 | min-height: 280px; 6 | } 7 | 8 | .uppy-Dashboard-files { 9 | scrollbar-width: none; 10 | 11 | ::-webkit-scrollbar { 12 | display: none; 13 | } 14 | 15 | > div { 16 | min-height : 220px !important; 17 | 18 | > div { 19 | position: relative !important; 20 | } 21 | } 22 | } 23 | 24 | .uppy-DashboardContent-bar { 25 | background-color: transparent !important; 26 | } 27 | 28 | .uppy-DashboardContent-bar { 29 | border: none !important; 30 | } 31 | 32 | .uppy-Root, .uppy-Root > * { 33 | font-family: inherit !important; 34 | } 35 | 36 | .uppy-Dashboard-AddFilesPanel { 37 | background: var(--background-color) !important; 38 | background-color: var(--background-color) !important; 39 | } 40 | 41 | .uppy-Dashboard-Item, 42 | .uppy-Dashboard-Item-preview { 43 | height: auto !important; 44 | user-drag: none; 45 | user-select: none; 46 | } 47 | 48 | .uppy-Dashboard-Item-previewIconWrap { 49 | margin: 20px; 50 | } 51 | 52 | .uppy-Dashboard-Item-previewInnerWrap { 53 | background-color: black !important; 54 | } 55 | 56 | .uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg { 57 | height: auto; 58 | object-fit: none; 59 | width: auto; 60 | } -------------------------------------------------------------------------------- /src/scss/display-only.scss: -------------------------------------------------------------------------------- 1 | .label small, 2 | .ss-add, 3 | .ss-value-delete, 4 | .icon-button { 5 | display: none !important; 6 | } 7 | 8 | .ss-multi-selected { 9 | pointer-events: none; 10 | } 11 | 12 | .ss-value { 13 | pointer-events: all; 14 | } 15 | 16 | .items, 17 | .item, 18 | .label { 19 | display: inline !important; 20 | background: none !important; 21 | padding: 0 !important; 22 | margin: 0 !important; 23 | } 24 | 25 | .item:not(:last-child) { 26 | border: none !important; 27 | } 28 | 29 | .form-element { 30 | display: block !important; 31 | } 32 | 33 | .reference-label { 34 | border: none !important; 35 | display: inline-flex !important; 36 | height: auto !important; 37 | padding-inline-end: 0 !important; 38 | 39 | .image { 40 | width: 16px !important; 41 | height: 16px !important; 42 | margin-right: 4px !important; 43 | } 44 | 45 | .reference-text { 46 | margin-left: 0 !important; 47 | } 48 | 49 | a { 50 | margin-left: 0 !important; 51 | } 52 | } 53 | 54 | .form-element:not([type="language-picker"]) { 55 | margin-bottom: 4px !important; 56 | } 57 | 58 | .form-element[type="language-picker"] { 59 | margin-bottom: 30px !important; 60 | } 61 | 62 | details summary::-webkit-details-marker { 63 | display:none; 64 | } 65 | 66 | details { 67 | margin-top: 14px !important; 68 | 69 | > .items { 70 | display: block !important; 71 | // padding-inline-start: 10px !important; 72 | } 73 | 74 | & > summary { 75 | list-style: none; 76 | pointer-events: none; 77 | margin-bottom: 6px !important; 78 | font-style: italic; 79 | 80 | .label { 81 | font-weight: 100 !important; 82 | } 83 | } 84 | } 85 | 86 | .form-element[type="url-image"] .items { 87 | display: flex !important; 88 | margin-top: 10px !important; 89 | 90 | img { 91 | border: 1px solid var(--color-gray); 92 | margin-inline-end: 30px; 93 | } 94 | } 95 | 96 | .form-element[type="language-picker"] .item .ss-values .ss-value.active, 97 | .form-element[type="language-picker"] .item .ss-multi-selected::after { 98 | right: 0; 99 | } 100 | 101 | .form-element[type="language-picker"] .item .ss-values { 102 | padding-inline-end: 0 !important; 103 | 104 | .ss-value:last-child { 105 | margin-inline-end: 0 !important; 106 | } 107 | } -------------------------------------------------------------------------------- /src/scss/elements/_checkbox.scss: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 60px; 5 | height: 35px; 6 | border-radius: var(--radius); 7 | 8 | input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .slider { 15 | position: absolute; 16 | cursor: pointer; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-color: var(--color-gray-light); 22 | -webkit-transition: .4s; 23 | transition: .4s; 24 | border-radius: var(--radius); 25 | } 26 | 27 | .slider:before { 28 | position: absolute; 29 | content: ""; 30 | height: 20px; 31 | width: 20px; 32 | border-radius: var(--radius); 33 | left: 8px; 34 | bottom: 6px; 35 | background-color: white; 36 | -webkit-transition: .4s; 37 | transition: .4s; 38 | border: 1px solid var(--color-gray); 39 | } 40 | 41 | input:checked + .slider { 42 | background-color: var(--color-secondary); 43 | } 44 | 45 | input:focus + .slider { 46 | box-shadow: 0 0 1px var(--color-secondary); 47 | } 48 | 49 | input:checked + .slider:before { 50 | transform: translateX(23px); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/scss/elements/_color.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="color"] { 2 | input[type="color"] { 3 | height: 60px; 4 | max-width: 60px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/scss/elements/_container.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="container"] { 2 | > .items { 3 | padding: 0; 4 | background: none; 5 | margin-bottom: 0; 6 | 7 | > .item { 8 | // padding: 0; 9 | margin-bottom: 0; 10 | 11 | .form-element { 12 | flex: 1 1 30%; 13 | margin-bottom: 0; 14 | } 15 | } 16 | } 17 | } 18 | 19 | .form-element.column > .items > .item { 20 | flex-direction: column; 21 | 22 | > .form-element { 23 | width: 100%; 24 | } 25 | } -------------------------------------------------------------------------------- /src/scss/elements/_details.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="details"] { 2 | summary { 3 | cursor: pointer; 4 | 5 | > .label { 6 | display: inline-flex; 7 | pointer-events: none; 8 | height: 46px; 9 | } 10 | } 11 | } 12 | 13 | [name="sidebar"] .form-element[type="details"] { 14 | > .items { 15 | background-color: var(--color-gray-blue); 16 | --background-color: #e8eaf0; 17 | --color-primary: #b0bcd9; 18 | --color-secondary: #cdd3e2; 19 | } 20 | } -------------------------------------------------------------------------------- /src/scss/elements/_duration.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="duration"] { 2 | .granularity { 3 | display: flex; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/scss/elements/_group.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="group"] { 2 | display: flex; 3 | 4 | > .items { 5 | > .item:not(:first-child) { 6 | > .form-element { 7 | > .label { 8 | display: none; 9 | } 10 | } 11 | } 12 | > .item { 13 | flex-wrap: nowrap; 14 | 15 | > .button { 16 | margin-bottom: 6px; 17 | align-self: flex-end; 18 | } 19 | 20 | > .form-element { 21 | flex: 1 1 100px; 22 | margin-bottom: 0; 23 | min-width: 80px; 24 | 25 | .label { 26 | height: auto; 27 | color: var(--text); 28 | font-style: italic; 29 | padding-top: 7px; 30 | padding-bottom: 5px; 31 | font-size: 14px; 32 | } 33 | 34 | > .items { 35 | padding: 0; 36 | margin-bottom: 0; 37 | 38 | > .item { 39 | padding-left: 0; 40 | padding-right: 0; 41 | 42 | input, select { 43 | min-width: 80px; 44 | } 45 | } 46 | } 47 | 48 | &[type="reference"] { 49 | min-width: 300px; 50 | 51 | .item { 52 | width: 100%; 53 | 54 | .button.edit { 55 | margin-left: auto; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/scss/elements/_language-picker.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="language-picker"] { 2 | background: none; 3 | border: 0; 4 | z-index: 9; 5 | 6 | .items, .item { 7 | padding: 0; 8 | margin: 0; 9 | border: 0; 10 | background: none; 11 | } 12 | 13 | .language-switcher { 14 | flex: 1 1 100px; 15 | margin-top: 3px; 16 | display: flex; 17 | 18 | svg { 19 | position: relative; 20 | top: 2px; 21 | margin-right: 10px; 22 | 23 | path { 24 | fill: var(--color-gray-medium); 25 | } 26 | } 27 | } 28 | 29 | .item { 30 | .ss-main { 31 | flex: 1 1 80%; 32 | } 33 | 34 | .ss-multi-selected { 35 | overflow-x: auto; 36 | overflow-y: hidden; 37 | scrollbar-width: none; 38 | border-radius: 0 !important; 39 | 40 | &::-webkit-scrollbar { 41 | display: none; 42 | } 43 | 44 | &:before, 45 | &:after { 46 | content: ''; 47 | display: block; 48 | width: 100px; 49 | pointer-events: none; 50 | height: calc(100% - 1px); 51 | position: absolute; 52 | bottom: 1px; 53 | z-index: 2; 54 | opacity: 1; 55 | transition: opacity .1s ease-in-out; 56 | } 57 | 58 | &:before { 59 | left: 0; 60 | background-image: linear-gradient( 61 | -90deg, 62 | rgba(255, 255, 255, 0) 0%, 63 | rgba(255, 255, 255, 1) 100% 64 | ); 65 | } 66 | 67 | &:after { 68 | right: 39px; 69 | background-image: linear-gradient( 70 | 90deg, 71 | rgba(255, 255, 255, 0) 0%, 72 | rgba(255, 255, 255, 1) 100% 73 | ); 74 | } 75 | 76 | &.hide-left-shadow:before { 77 | opacity: 0; 78 | } 79 | 80 | &.hide-right-shadow:after { 81 | opacity: 0; 82 | } 83 | 84 | } 85 | 86 | .ss-values { 87 | padding: 0; 88 | flex-wrap: nowrap; 89 | white-space: nowrap; 90 | width: auto; 91 | padding-inline-end: 40px; 92 | 93 | &:after { 94 | content: ''; 95 | display: block; 96 | position: absolute; 97 | bottom: 0; 98 | width: 100%; 99 | height: 1px; 100 | border-bottom: 1px solid var(--color-gray); 101 | } 102 | 103 | .ss-value { 104 | &:not(.active) .ss-value-text { 105 | color: var(--color-gray-dark); 106 | } 107 | 108 | &.active { 109 | position: sticky; 110 | left: 0; 111 | right: 40px; 112 | z-index: 3; 113 | } 114 | } 115 | 116 | > .ss-disabled { 117 | cursor: pointer; 118 | color: var(--text); 119 | padding: 15px 0; 120 | } 121 | } 122 | 123 | .ss-add { 124 | cursor: pointer; 125 | flex: 0 1 30px; 126 | position: relative; 127 | padding-inline-start: 30px; 128 | padding-inline-end: 10px; 129 | width: 40px; 130 | display: flex; 131 | height: 100%; 132 | position: absolute; 133 | margin-right: 0; 134 | background: white; 135 | border-bottom: 1px solid var(--color-gray); 136 | right: 0px; 137 | z-index: 4; 138 | 139 | .ss-plus { 140 | position: absolute; 141 | left: 50%; 142 | } 143 | 144 | } 145 | 146 | .ss-content.ss-open { 147 | margin-top: 20px; 148 | border-radius: var(--radius); 149 | } 150 | 151 | .ss-multi-selected { 152 | border: 0; 153 | } 154 | 155 | .ss-value { 156 | position: relative; 157 | margin-bottom: -1px; 158 | border-bottom-left-radius: 0; 159 | border-bottom-right-radius: 0; 160 | padding: 14px; 161 | cursor: pointer; 162 | font-size: 16px; 163 | border: 1px solid var(--color-gray); 164 | background: var(--color-gray-light); 165 | 166 | &.active { 167 | background: white; 168 | border-bottom-color: white; 169 | } 170 | 171 | .ss-value-delete svg { 172 | width: 10px !important; 173 | margin-left: 4px; 174 | path { 175 | fill: var(--color-gray-medium); 176 | } 177 | } 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/scss/elements/_password.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="password"] { 2 | .column { 3 | flex: 1 1 40%; 4 | 5 | > * { 6 | width: 100%; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/scss/elements/_reference.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="reference"] { 2 | .items { 3 | display: flex; 4 | flex-wrap: wrap; 5 | } 6 | 7 | .inner { 8 | display: flex; 9 | width: 100%; 10 | } 11 | 12 | .standard-options { 13 | left: 5px; 14 | right: 5px; 15 | border-top-left-radius: 0; 16 | border-top-right-radius: 0; 17 | z-index: 10; 18 | width: 100%; 19 | margin-top: 10px; 20 | top: 40px; 21 | background-color: white; 22 | display: flex; 23 | flex-direction: column; 24 | 25 | > .standard-option { 26 | border: 1px solid var(--color-gray); 27 | padding: 8px; 28 | border-radius: var(--radius); 29 | cursor: pointer; 30 | transition: opacity .2s ease-in-out; 31 | 32 | &:hover { 33 | background-color: var(--color-primary); 34 | } 35 | } 36 | } 37 | 38 | .search-suggestions { 39 | top: 29px; 40 | left: 5px; 41 | right: 5px; 42 | border-top-left-radius: 0; 43 | border-top-right-radius: 0; 44 | position: absolute; 45 | z-index: 10; 46 | } 47 | 48 | .item { 49 | align-items: center; 50 | gap: 0; 51 | 52 | input { 53 | border-top-right-radius: 0; 54 | border-bottom-right-radius: 0; 55 | outline: none; 56 | } 57 | 58 | &:not(:last-child) { 59 | border-bottom: none; 60 | } 61 | 62 | &[has-suggestions] { 63 | .button { 64 | border-bottom-right-radius: 0; 65 | } 66 | 67 | input { 68 | border-bottom-left-radius: 0; 69 | } 70 | } 71 | 72 | .button { 73 | &:not(:last-child):not(.remove) { 74 | border-top-right-radius: 0; 75 | border-bottom-right-radius: 0; 76 | } 77 | } 78 | 79 | * + .button { 80 | border-top-left-radius: 0; 81 | border-bottom-left-radius: 0; 82 | } 83 | } 84 | 85 | .reference-loading { 86 | padding-left: 10px; 87 | font-size: 12px; 88 | } 89 | 90 | .reference-label { 91 | background: white; 92 | border-top-left-radius: var(--radius); 93 | border-bottom-left-radius: var(--radius); 94 | border: 1px solid var(--color-gray); 95 | padding-right: 10px; 96 | 97 | display: flex; 98 | flex: 1 1 auto; 99 | width: auto; 100 | align-items: center; 101 | height: 35px; 102 | 103 | a, span { 104 | margin-left: 10px; 105 | } 106 | 107 | &[type="text"] { 108 | padding-left: 10px; 109 | } 110 | 111 | .image { 112 | width: 33px; 113 | height: 33px; 114 | border-top-left-radius: var(--radius); 115 | border-bottom-left-radius: var(--radius); 116 | background-color: var(--color-gray); 117 | overflow: hidden; 118 | 119 | img { 120 | object-fit: cover; 121 | object-position: center; 122 | width: 100%; 123 | height: 100%; 124 | } 125 | } 126 | 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/scss/elements/_url-image.scss: -------------------------------------------------------------------------------- 1 | .form-element[type="url-uppy"], 2 | .form-element[type="url-image"] { 3 | img { 4 | max-width: 100%; 5 | max-height: 300px; 6 | user-select: none; 7 | -webkit-user-drag: none; 8 | } 9 | 10 | .top { 11 | display: flex; 12 | width: 100%; 13 | gap: 10px; 14 | } 15 | 16 | .image-wrapper { 17 | display: inline-flex; 18 | position: relative; 19 | } 20 | 21 | .focal-point-description { 22 | background: var(--color-primary); 23 | padding: 5px; 24 | } 25 | 26 | .focal-point { 27 | pointer-events: none; 28 | position: absolute; 29 | // border-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgdmlld0JveD0iMCAwIDcuOSA3LjkiIHZlcnNpb249IjEuMSI+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTI4OS4wNjI0OCkiPgo8cGF0aCBkPSJNMi44IDI4OS4ySDEuMmMtMC42IDAtMSAwLjQtMSAxdjEuN203LjcgMHYtMS43YzAtMC41LTAuNS0xLTEtMUg1LjFtLTUgNXYxLjdjMCAwLjUgMC41IDEgMSAxaDEuNm0yLjMgMGgxLjZjMC42IDAgMS0wLjQgMS0xdi0xLjciIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS13aWR0aDowLjM7c3Ryb2tlOndoaXRlIi8+PC9nPjwvc3ZnPg==") 13 / 35px / 0 space; 30 | // mix-blend-mode: difference; 31 | // filter: saturate(100%) contrast(200%); 32 | z-index: 1; 33 | 34 | &:before, 35 | &:after { 36 | content: ''; 37 | display: block; 38 | width: 40px; 39 | height: 0px; 40 | 41 | // border-top: 2px solid #66ff00; 42 | 43 | position: absolute; 44 | top: 50%; 45 | left: 50%; 46 | transform: translate(-50%, -50%); 47 | } 48 | 49 | &:after { 50 | transform: translate(-50%, -50%) rotate(90deg); 51 | } 52 | } 53 | 54 | .item { 55 | flex-direction: column; 56 | } 57 | 58 | input { 59 | width: 100%; 60 | } 61 | 62 | .image-background { 63 | position: absolute; 64 | top: 50%; 65 | left: 50%; 66 | transform: translate(-50%, -50%); 67 | opacity: .3; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scss/elements/_wysiwyg.scss: -------------------------------------------------------------------------------- 1 | .wysiwyg-wrapper { 2 | width: 100%; 3 | } 4 | 5 | .form-element[type="wysiwyg"] { 6 | .switch-editor { 7 | margin: 6px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/scss/rdf-form.scss: -------------------------------------------------------------------------------- 1 | @import './components/actions'; 2 | @import './components/base'; 3 | @import './components/button'; 4 | @import './components/form-element'; 5 | @import './components/items'; 6 | @import './components/rdf-form'; 7 | @import './components/search-suggestions'; 8 | @import './components/select'; 9 | @import './components/slim-select'; 10 | @import './components/pell'; 11 | @import './components/checkbox-label'; 12 | @import './components/uppy'; 13 | 14 | @import './elements/duration'; 15 | @import './elements/group'; 16 | @import './elements/reference'; 17 | @import './elements/checkbox'; 18 | @import './elements/url-image'; 19 | @import './elements/color'; 20 | @import './elements/container'; 21 | @import './elements/details'; 22 | @import './elements/language-picker'; 23 | @import './elements/wysiwyg'; 24 | @import './elements/password'; 25 | 26 | @import './layout'; 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/types/CoreComponent.ts: -------------------------------------------------------------------------------- 1 | export interface CoreComponent { 2 | ready: boolean 3 | } -------------------------------------------------------------------------------- /src/types/ElementInstance.ts: -------------------------------------------------------------------------------- 1 | import { render, html } from 'uhtml/async'; 2 | 3 | export interface ElementInstance { 4 | label: () => typeof html, 5 | wrapper: (innerTemplates: Array) => typeof html, 6 | wrapperDisplay: (innerTemplates: Array) => typeof html, 7 | item: (childTemplates: Array) => typeof html, 8 | itemDisplay: (childTemplates: Array) => typeof html, 9 | } -------------------------------------------------------------------------------- /src/types/ExpandedJsonLdObject.ts: -------------------------------------------------------------------------------- 1 | export type ExpandedJsonLdObject = { 2 | '@type': Array 3 | } -------------------------------------------------------------------------------- /src/types/Field.ts: -------------------------------------------------------------------------------- 1 | export interface Field { 2 | 3 | } -------------------------------------------------------------------------------- /src/types/Form.ts: -------------------------------------------------------------------------------- 1 | export type Form = { 2 | 3 | } -------------------------------------------------------------------------------- /src/vendor/ProxyHandlerStatic-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A proxy handler that prefixes all URLs with a given string. 3 | */ 4 | export class ProxyHandlerStatic { 5 | constructor(prefixUrl) { 6 | this.prefixUrl = prefixUrl; 7 | } 8 | async getProxy(request) { 9 | return { 10 | init: request.init, 11 | input: this.modifyInput(request.input), 12 | }; 13 | } 14 | modifyInput(input) { 15 | if (typeof input === 'string') { 16 | return this.prefixUrl + input; 17 | } 18 | return new Request(this.prefixUrl + input.url, input); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/vendor/pell.js: -------------------------------------------------------------------------------- 1 | const defaultParagraphSeparatorString = 'defaultParagraphSeparator' 2 | const formatBlock = 'formatBlock' 3 | const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener) 4 | const appendChild = (parent, child) => parent.appendChild(child) 5 | const createElement = tag => document.createElement(tag) 6 | const queryCommandState = command => document.queryCommandState(command) 7 | const queryCommandValue = command => document.queryCommandValue(command) 8 | 9 | export const exec = (command, value = null) => document.execCommand(command, false, value) 10 | 11 | const defaultActions = { 12 | bold: { 13 | icon: 'B', 14 | title: 'Bold', 15 | state: () => queryCommandState('bold'), 16 | result: () => exec('bold') 17 | }, 18 | italic: { 19 | icon: 'I', 20 | title: 'Italic', 21 | state: () => queryCommandState('italic'), 22 | result: () => exec('italic') 23 | }, 24 | underline: { 25 | icon: 'U', 26 | title: 'Underline', 27 | state: () => queryCommandState('underline'), 28 | result: () => exec('underline') 29 | }, 30 | strikethrough: { 31 | icon: 'S', 32 | title: 'Strike-through', 33 | state: () => queryCommandState('strikeThrough'), 34 | result: () => exec('strikeThrough') 35 | }, 36 | heading1: { 37 | icon: 'H1', 38 | title: 'Heading 1', 39 | result: () => exec(formatBlock, '

') 40 | }, 41 | heading2: { 42 | icon: 'H2', 43 | title: 'Heading 2', 44 | result: () => exec(formatBlock, '

') 45 | }, 46 | paragraph: { 47 | icon: '¶', 48 | title: 'Paragraph', 49 | result: () => exec(formatBlock, '

') 50 | }, 51 | quote: { 52 | icon: '“ ”', 53 | title: 'Quote', 54 | result: () => exec(formatBlock, '

') 55 | }, 56 | olist: { 57 | icon: '#', 58 | title: 'Ordered List', 59 | result: () => exec('insertOrderedList') 60 | }, 61 | ulist: { 62 | icon: '•', 63 | title: 'Unordered List', 64 | result: () => exec('insertUnorderedList') 65 | }, 66 | code: { 67 | icon: '</>', 68 | title: 'Code', 69 | result: () => exec(formatBlock, '
')
 70 |   },
 71 |   line: {
 72 |     icon: '―',
 73 |     title: 'Horizontal Line',
 74 |     result: () => exec('insertHorizontalRule')
 75 |   },
 76 |   link: {
 77 |     icon: '🔗',
 78 |     title: 'Link',
 79 |     result: () => {
 80 |       const url = window.prompt('Enter the link URL')
 81 |       if (url) exec('createLink', url)
 82 |     }
 83 |   },
 84 |   image: {
 85 |     icon: '📷',
 86 |     title: 'Image',
 87 |     result: () => {
 88 |       const url = window.prompt('Enter the image URL')
 89 |       if (url) exec('insertImage', url)
 90 |     }
 91 |   }
 92 | }
 93 | 
 94 | const defaultClasses = {
 95 |   actionbar: 'pell-actionbar',
 96 |   button: 'pell-button',
 97 |   content: 'pell-content',
 98 |   selected: 'pell-button-selected'
 99 | }
100 | 
101 | export const init = settings => {
102 |   const actions = settings.actions
103 |     ? (
104 |       settings.actions.map(action => {
105 |         if (typeof action === 'string') return defaultActions[action]
106 |         else if (defaultActions[action.name]) return { ...defaultActions[action.name], ...action }
107 |         return action
108 |       })
109 |     )
110 |     : Object.keys(defaultActions).map(action => defaultActions[action])
111 | 
112 |   const classes = { ...defaultClasses, ...settings.classes }
113 | 
114 |   const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div'
115 | 
116 |   const actionbar = createElement('div')
117 |   actionbar.className = classes.actionbar
118 |   appendChild(settings.element, actionbar)
119 | 
120 |   const content = settings.element.content = createElement('div')
121 |   content.contentEditable = true
122 |   content.className = classes.content
123 |   content.oninput = ({ target: { firstChild } }) => {
124 |     if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`)
125 |     else if (content.innerHTML === '
') content.innerHTML = '' 126 | settings.onChange(content.innerHTML) 127 | } 128 | content.onkeydown = event => { 129 | if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') { 130 | setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0) 131 | } 132 | } 133 | appendChild(settings.element, content) 134 | 135 | actions.forEach(action => { 136 | const button = createElement('button') 137 | button.className = classes.button 138 | button.innerHTML = action.icon 139 | button.title = action.title 140 | button.setAttribute('type', 'button') 141 | button.onclick = () => action.result() && content.focus() 142 | 143 | if (action.state) { 144 | const handler = () => button.classList[action.state() ? 'add' : 'remove'](classes.selected) 145 | addEventListener(content, 'keyup', handler) 146 | addEventListener(content, 'mouseup', handler) 147 | addEventListener(button, 'click', handler) 148 | } 149 | 150 | appendChild(actionbar, button) 151 | }) 152 | 153 | if (settings.styleWithCSS) exec('styleWithCSS') 154 | exec(defaultParagraphSeparatorString, defaultParagraphSeparator) 155 | 156 | return settings.element 157 | } 158 | 159 | export default { exec, init } 160 | -------------------------------------------------------------------------------- /test/blah.test.ts: -------------------------------------------------------------------------------- 1 | describe('blah', () => { 2 | it('works', () => { 3 | expect(1).toEqual(1); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "lib": ["dom", "ESNext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "strictPropertyInitialization": false, 20 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | // use Node's module resolution algorithm, instead of the legacy TS one 24 | "moduleResolution": "node", 25 | // interop between ESM and CJS modules. Recommended by TS 26 | "esModuleInterop": true, 27 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 28 | "skipLibCheck": true, 29 | // error out if import and file system have a casing mismatch. Recommended by TS 30 | "forceConsistentCasingInFileNames": true, 31 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 32 | "noEmit": true, 33 | 34 | "target": "ESNext", 35 | "noImplicitAny": false, 36 | "allowJs": true, 37 | "checkJs": false, 38 | "removeComments": true, 39 | // "emitDeclarationOnly": true, 40 | // "allowSyntheticDefaultImports": true, 41 | 42 | // "declarationMap": true, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const scss = require('rollup-plugin-scss'); 2 | const fg = require('fast-glob'); 3 | 4 | function scssWatchPlugin(options = {}) { 5 | return { 6 | name: 'scss-watch', 7 | async transform(code, id) { 8 | if (!id.includes('/rdf-form.scss')) return 9 | 10 | const files = await fg('./src/scss/**/*'); 11 | const dependencies = files.filter(file => file !== './scss/rdf-form.scss') 12 | .map(dep => dep.replace('src/scss/', '')) 13 | 14 | return { code, dependencies, map: { mappings: '' } } 15 | } 16 | }; 17 | } 18 | 19 | 20 | module.exports = { 21 | rollup: function (config, options) { 22 | config.plugins.push(scss({ output: false })); 23 | config.plugins.push(scssWatchPlugin()) 24 | return config; 25 | }, 26 | }; --------------------------------------------------------------------------------