├── .gitignore ├── .jshintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── json-silo ├── data └── .gitignore ├── lib ├── databasemanager.js ├── esmapdbmanager.js ├── imagesmanager.js ├── jsonsilo.js ├── routes │ ├── responsehandler.js │ ├── store.js │ └── stories.js └── storiesmanager.js ├── package.json └── web └── json-silo ├── images ├── json-silo.png ├── pareto-anywhere-logo-nav.png └── reelyactive-logo-square-nav.png ├── index.html ├── js ├── bootstrappyactive.bundle.min.js ├── bootstrappyactive.bundle.min.js.map ├── color-modes.js ├── cormorant.js ├── cuttlefish-story.js ├── fontawesome-reelyactive.min.js ├── fontawesome-reelyactive.min.js.LICENSE.txt ├── jsonsilo.js └── stories.js ├── stories └── index.html └── style ├── bootstrappyactive.min.css └── bootstrappyactive.min.css.map /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "funcscope": true, 5 | "freeze": true, 6 | "maxdepth": 3, 7 | "maxstatements": 15, 8 | "eqnull": true, 9 | "node": true 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | conduct@reelyactive.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute 2 | ================= 3 | 4 | First and foremost: _thank you for your interest in contributing to this open source software project!_ 5 | 6 | The many ways in which you can contribute include: 7 | - [reporting an issue](#reporting-an-issue) 8 | - [fixing an issue](#fixing-an-issue) 9 | - [adding a feature](#adding-a-feature) 10 | - [financial support](#financial-support) 11 | - [love and encouragement](#love-and-encouragement) 12 | 13 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md) 14 | Kindly observe the standard [code of conduct](CODE_OF_CONDUCT.md) however you choose to contribute. 15 | 16 | 17 | Reporting an issue 18 | ------------------ 19 | 20 | We invite contributors to __create an issue on GitHub__ whenever a significant bug or impedement to the existing functionality of the project is encountered. Kindly include sufficient detail to easily replicate the issue as this will maximise the chances of a swift resolution. 21 | 22 | 23 | Fixing an issue 24 | --------------- 25 | 26 | We invite contributors who are comfortable fixing any significant and obvious issue they discover to __create a pull request__ on the appropriate project branch. 27 | 28 | :information_source: Kindly observe reelyActive's [Node.js style guide](https://github.com/reelyactive/node-style-guide) and/or [Web style guide](https://github.com/reelyactive/web-style-guide) as applicable. 29 | 30 | 31 | Adding a feature 32 | ---------------- 33 | 34 | We invite _new_ contributors to __fork the project__, add new features, and then [contact us](https://www.reelyactive.com/contact/) to share their work. 35 | 36 | We invite _veteran_ contributors to __create a pull request__ on the appropriate branch for each new feature they develop. 37 | 38 | :information_source: Kindly observe reelyActive's [Node.js style guide](https://github.com/reelyactive/node-style-guide) and/or [Web style guide](https://github.com/reelyactive/web-style-guide) as applicable. 39 | 40 | 41 | Financial support 42 | ----------------- 43 | 44 | Open source development of this and other reelyActive software packages is supported primarily by [annual subscriptions](https://www.reelyactive.com/pricing/) from clients, partners and members of our community. If you're in a position to contribute financially, please consider doing so. 45 | 46 | 47 | Love and encouragement 48 | ---------------------- 49 | 50 | This open source project exists because of a perceived community need and the tireless efforts of its contributors. Consider encouraging them by: 51 | - :star: starring the project on GitHub 52 | - :busts_in_silhouette: sharing the project with friends and colleagues to raise awareness 53 | - :heart: surprising the community and its contributors with random acts of kindness 54 | 55 | 56 | These contribution guidelines were drafted by [the reelyActive team](https://reelyactive.com/team/) and we invite you to adapt them for your own projects under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2024 reelyactive 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | json-silo 2 | ========= 3 | 4 | 5 | A lightweight digital twins store and API for the IoT 6 | ----------------------------------------------------- 7 | 8 | The __json-silo__ is a digital twins store and an accessory module of [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware for [context-aware physical spaces](https://www.reelyactive.com/context-aware-physical-spaces/). 9 | 10 | ![Overview of json-silo](https://reelyactive.github.io/json-silo/images/overview.png) 11 | 12 | Specifically, the __json-silo__ stores "stories" which are machine-readable representations of people, products, places, etc. in the form of JSON-LD and Schema.org. Additionally, it provides a simple mechanism for file storage and retrieval, for any accompanying metadata such as images. The __json-silo__ can run standalone, even on resource-constrained devices. 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | npm install json-silo 19 | 20 | 21 | Hello json-silo! 22 | ---------------- 23 | 24 | npm start 25 | 26 | Browse to [localhost:3001/json-silo/](http://localhost:3001/json-silo/) and observe the json-silo logo. Interact with the json-silo via its REST API. 27 | 28 | 29 | REST API 30 | -------- 31 | 32 | The __json-silo__'s REST API includes the following two base routes: 33 | - /stories _for retrieving/specifying digital twins_ 34 | - /store _for storing and retrieving metadata such as /images_ 35 | 36 | 37 | ### POST /stories 38 | 39 | Create a story. The _id_ of the created story is provided in the response. 40 | 41 | #### Example request 42 | 43 | | Method | Route | Content-Type | 44 | |:-------|:---------|:-----------------| 45 | | POST | /stories | application/json | 46 | 47 | { 48 | "@context": { 49 | "schema": "https://schema.org/" 50 | }, 51 | "@graph": [ 52 | { 53 | "@id": "person", 54 | "@type": "schema:Person", 55 | "schema:givenName": "barnowl" 56 | } 57 | ] 58 | } 59 | 60 | #### Example response 61 | 62 | { 63 | "_meta": { 64 | "message": "ok", 65 | "statusCode": 200 66 | }, 67 | "_links": { 68 | "self": { 69 | "href": "http://localhost:3000/stories" 70 | } 71 | }, 72 | "stories": { 73 | "barnowl": { 74 | "@context": { 75 | "schema": "https://schema.org/" 76 | }, 77 | "@graph": [ 78 | { 79 | "@id": "person", 80 | "@type": "schema:Person", 81 | "schema:givenName": "barnowl" 82 | } 83 | ] 84 | } 85 | } 86 | } 87 | 88 | 89 | ### GET /stories/{id} 90 | 91 | Retrieve the story with the given _id_. 92 | 93 | #### Example request 94 | 95 | | Method | Route | Content-Type | 96 | |:-------|:-----------------|:-----------------| 97 | | GET | /stories/barnowl | application/json | 98 | 99 | #### Example response 100 | 101 | { 102 | "_meta": { 103 | "message": "ok", 104 | "statusCode": 200 105 | }, 106 | "_links": { 107 | "self": { 108 | "href": "http://localhost:3000/stories/barnowl" 109 | } 110 | }, 111 | "stories": { 112 | "barnowl": { 113 | "@context": { 114 | "schema": "https://schema.org/" 115 | }, 116 | "@graph": [ 117 | { 118 | "@id": "person", 119 | "@type": "schema:Person", 120 | "schema:givenName": "barnowl" 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | 127 | 128 | ### POST /store/images 129 | 130 | Upload an image to the store. The _id_ (modified filename) of the uploaded image is provided in the response. The image is modified using [sharp](https://github.com/lovell/sharp) to conform to standard dimensions before it is stored on the filesystem. 131 | 132 | #### Example request 133 | 134 | | Method | Route | Content-Type | 135 | |:-------|:--------------|:--------------------| 136 | | POST | /store/images | multipart/form-data | 137 | 138 | 139 | ### GET /store/images/{id} 140 | 141 | Retrieve the image with the given _id_ (filename). 142 | 143 | #### Example request 144 | 145 | | Method | Route | Content-Type | 146 | |:-------|:---------------------------|:--------------------| 147 | | POST | /store/images/12345678.jpg | multipart/form-data | 148 | 149 | 150 | What's in a name? 151 | ----------------- 152 | 153 | The __json-silo__ is exactly that: a data silo for [JSON](https://en.wikipedia.org/wiki/JSON)! Simple enough, right? So why does it have a grain silo with a hockey mask for a mascot? 154 | 155 | At reelyActive we've always been outspoken about the need for an open Internet of Things as opposed to a bunch of siloed applications. In 2013, on social media we recycled the ["More cowbell" meme](https://en.wikipedia.org/wiki/More_cowbell) with an image of Will Ferrell banging on a grain silo with the caption ["The Internet of Things does not need More Silo"](https://reelyactive.github.io/images/moreSilo.jpg). When it came time to create a mascot for the __json-silo__, we decided to start with that grain silo. 156 | 157 | Now, how do you visually represent JSON in association with a grain silo? Sure, we could have slapped the official JSON logo on that silo, but where's the fun in that? Instead, for those of us who grew up in the eighties, hearing "JSON" out of context (pun intended) evokes the image of [Jason Voorhees](https://en.wikipedia.org/wiki/Jason_Voorhees) from the Friday the 13th series of films, specifically the iconic hockey goaltender mask he wore. Not only does that "Jason" mask make for a silly visual pun, it also gives a nod to our hometown heritage, where [Jacques Plante](https://en.wikipedia.org/wiki/Jacques_Plante) of the Montreal Canadiens was the first goaltender to wear such a mask full-time, which would later become standard practice. We'd be pleased to see the use of personal data lockers become standard practice too. 158 | 159 | ![json-silo logo](https://reelyactive.github.io/json-silo/images/json-silo-bubble.png) 160 | 161 | 162 | What's next? 163 | ------------ 164 | 165 | __json-silo__ v1.0.0 was released in August 2019, superseding all earlier versions, the latest of which remains available in the [release-0.5 branch](https://github.com/reelyactive/json-silo/tree/release-0.5) and as [json-silo@0.5.2 on npm](https://www.npmjs.com/package/json-silo/v/0.5.2). 166 | 167 | __json-silo__ v1.1.0 migrates to [ESMapDB](https://github.com/reelyactive/esmapdb) from [NeDB](https://github.com/louischatriot/nedb). If upgrading from a previous version, any stored stories will need to be recreated. 168 | 169 | 170 | Contributing 171 | ------------ 172 | 173 | Discover [how to contribute](CONTRIBUTING.md) to this open source project which upholds a standard [code of conduct](CODE_OF_CONDUCT.md). 174 | 175 | 176 | Security 177 | -------- 178 | 179 | Consult our [security policy](SECURITY.md) for best practices using this open source software and to report vulnerabilities. 180 | 181 | 182 | License 183 | ------- 184 | 185 | MIT License 186 | 187 | Copyright (c) 2014-2024 [reelyActive](https://www.reelyactive.com) 188 | 189 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 190 | 191 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 192 | 193 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 194 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 195 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 196 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 197 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 198 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 199 | THE SOFTWARE. 200 | 201 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Using reelyActive open source software securely 2 | =============================================== 3 | 4 | If you're reading this, it is likely because you take open source software security seriously. _Thank you!_ 5 | 6 | 7 | Keep up to date 8 | --------------- 9 | 10 | Unless otherwise specified, it is recommended to regularly update to the most recent version of this software package, and to review the security test results, including any [Dependabot alerts](https://docs.github.com/code-security/dependabot/dependabot-alerts) listed under the Security tab of this GitHub repository, ideally automating this process and including an alerting feature. 11 | 12 | 13 | Disclose a vulnerability 14 | ------------------------ 15 | 16 | Should you discover a novel security issue or vulnerability, kindly __report your findings privately__, either to __security@reelyactive.com__, or with the __Report a vulnerability__ feature under the Security tab of this GitHub repository. 17 | 18 | Kindly provide sufficient detail to effectively address, if not resolve, the issue, as well as a means of contact should additional detail be required, _and so that we may return our thanks!_ 19 | 20 | 21 | These security guidelines were drafted by [the reelyActive team](https://www.reelyactive.com/team/) and we invite you to adapt them for your own projects under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/). -------------------------------------------------------------------------------- /bin/json-silo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const http = require('http'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const JSONSilo = require('../lib/jsonsilo.js'); 7 | 8 | 9 | const PORT = process.env.PORT || 3001; 10 | 11 | 12 | let app = express(); 13 | app.use(bodyParser.json({ limit: "50mb", extended: true })); 14 | 15 | let server = http.createServer(app); 16 | server.listen(PORT, function() { 17 | console.log('json-silo instance is listening on port', PORT); 18 | }); 19 | 20 | const options = { app: app }; 21 | 22 | let stories = new JSONSilo(options); -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # Include the empty data folder in the repo 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /lib/databasemanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const ESMapDBManager = require('./esmapdbmanager'); 8 | 9 | 10 | /** 11 | * DatabaseManager Class 12 | * Manages the database(s) in which the data is stored, abstracting away the 13 | * implementation details. 14 | */ 15 | class DatabaseManager { 16 | 17 | /** 18 | * DatabaseManager constructor 19 | * @param {Object} options The options as a JSON object. 20 | * @constructor 21 | */ 22 | constructor(options) { 23 | options = options || {}; 24 | 25 | this.database = new ESMapDBManager(options); 26 | } 27 | 28 | /** 29 | * Delete the pair associated with the given key from database. 30 | * @param {String} key The key to look up. 31 | * @param {function} callback Function to call on completion. 32 | */ 33 | delete(key, callback) { 34 | this.database.delete(key, callback); 35 | } 36 | 37 | /** 38 | * Get the value associated with the given key from database. 39 | * @param {String} key The key to look up. 40 | * @param {function} callback Function to call on completion. 41 | */ 42 | get(key, callback) { 43 | this.database.get(key, callback); 44 | } 45 | 46 | /** 47 | * Determine if the given key exists in the database. 48 | * @param {String} key The key to look up. 49 | * @param {function} callback Function to call on completion. 50 | */ 51 | has(key, callback) { 52 | this.database.has(key, callback); 53 | } 54 | 55 | /** 56 | * Set the value associated with the given key in the database. 57 | * @param {String} key The key to look up. 58 | * @param {function} callback Function to call on completion. 59 | */ 60 | set(key, value, callback) { 61 | this.database.set(key, value, callback); 62 | } 63 | 64 | /** 65 | * Find all key/value pairs in the database that match the given query. 66 | * @param {Object} query The query parameters. 67 | * @param {function} callback Function to call on completion. 68 | */ 69 | find(query, callback) { 70 | this.database.find(query, callback); 71 | } 72 | 73 | } 74 | 75 | 76 | module.exports = DatabaseManager; -------------------------------------------------------------------------------- /lib/esmapdbmanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2015-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const ESMapDB = require('esmapdb'); 8 | 9 | 10 | const DEFAULT_DATA_FOLDER = 'data'; 11 | const DEFAULT_ASSOCIATION_DB = 'stories'; 12 | 13 | 14 | /** 15 | * ESMapDBManager Class 16 | * Manages an ESMapDB database instance. 17 | */ 18 | class ESMapDBManager { 19 | 20 | /** 21 | * EsMapDBManager constructor 22 | * @param {Object} options The options as a JSON object. 23 | * @constructor 24 | */ 25 | constructor(options) { 26 | options = options || {}; 27 | 28 | let persistentLocation = DEFAULT_DATA_FOLDER + '/' + DEFAULT_ASSOCIATION_DB; 29 | this.database = new ESMapDB({ createInMemory: true, 30 | createPersistent: true, 31 | persistentLocation: persistentLocation, 32 | persistentValueEncoding: 'json' }); 33 | } 34 | 35 | /** 36 | * Delete the pair associated with the given key from database. 37 | * @param {String} key The key to look up. 38 | * @param {function} callback Function to call on completion. 39 | */ 40 | delete(key, callback) { 41 | this.database.delete(key, callback); 42 | } 43 | 44 | /** 45 | * Get the value associated with the given key from database. 46 | * @param {String} key The key to look up. 47 | * @param {function} callback Function to call on completion. 48 | */ 49 | get(key, callback) { 50 | this.database.get(key, callback); 51 | } 52 | 53 | /** 54 | * Determine if the given key exists in the database. 55 | * @param {String} key The key to look up. 56 | * @param {function} callback Function to call on completion. 57 | */ 58 | has(key, callback) { 59 | this.database.has(key, callback); 60 | } 61 | 62 | /** 63 | * Set the value associated with the given key in the database. 64 | * @param {String} key The key to look up. 65 | * @param {function} callback Function to call on completion. 66 | */ 67 | set(key, value, callback) { 68 | this.database.set(key, value, callback); 69 | } 70 | 71 | /** 72 | * Find all key/value pairs in the database that match the given query. 73 | * @param {Object} query The query parameters. 74 | * @param {function} callback Function to call on completion. 75 | */ 76 | find(query, callback) { 77 | let result = new Map(); 78 | 79 | this.database.forEach((value, key) => { 80 | // TODO: handle query rather than assuming 'get all' 81 | result.set(key, value); 82 | }); 83 | 84 | callback(undefined, result); 85 | 86 | return result; 87 | } 88 | 89 | } 90 | 91 | 92 | module.exports = ESMapDBManager; 93 | -------------------------------------------------------------------------------- /lib/imagesmanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const crypto = require('crypto'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const sharp = require('sharp'); 11 | 12 | 13 | const DEFAULT_IMAGES_FOLDER = 'data/images'; 14 | const DEFAULT_IMAGE_WIDTH_PIXELS = 480; 15 | const DEFAULT_IMAGE_HEIGHT_PIXELS = 480; 16 | const DEFAULT_IDENTIFIER_LENGTH = 4; 17 | const HTTP_STATUS_OK = 200; 18 | const HTTP_STATUS_CREATED = 201; 19 | const HTTP_STATUS_BAD_REQUEST = 400; 20 | const HTTP_STATUS_NOT_FOUND = 404; 21 | const HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415; 22 | 23 | 24 | /** 25 | * ImagesManager Class 26 | * Manages the storage and retrieval of images. 27 | */ 28 | class ImagesManager { 29 | 30 | /** 31 | * ImagesManager constructor 32 | * @param {Object} options The options as a JSON object. 33 | * @constructor 34 | */ 35 | constructor(options) { 36 | let self = this; 37 | options = options || {}; 38 | 39 | self.identifierLength = options.identifierLength || 40 | DEFAULT_IDENTIFIER_LENGTH; 41 | 42 | fs.mkdir(DEFAULT_IMAGES_FOLDER, { recursive: true }, (err) => { 43 | if(err) { 44 | console.log('json-silo could not create/access images directory:', 45 | DEFAULT_IMAGES_FOLDER, ' Image store will not function.'); 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Create a new image 52 | * @param {Object} req The HTTP request. 53 | * @param {Object} res The HTTP result. 54 | * @param {callback} callback Function to call on completion. 55 | */ 56 | create(req, res, callback) { 57 | let self = this; 58 | 59 | if(req.file === undefined) { 60 | return callback(HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE); 61 | } 62 | else { 63 | let fileName = crypto.randomBytes(self.identifierLength).toString('hex') 64 | + path.extname(req.file.originalname); 65 | let filePath = path.resolve(DEFAULT_IMAGES_FOLDER + '/' + fileName); 66 | 67 | sharp(req.file.buffer) 68 | .resize(DEFAULT_IMAGE_WIDTH_PIXELS, DEFAULT_IMAGE_HEIGHT_PIXELS) 69 | .toFile(filePath, (err, info) => { 70 | if(err) { 71 | return callback(HTTP_STATUS_BAD_REQUEST); 72 | } 73 | 74 | let images = {}; 75 | images[fileName] = info; 76 | 77 | return callback(HTTP_STATUS_CREATED, { images: images }); 78 | }); 79 | } 80 | } 81 | 82 | /** 83 | * Retrieve an existing image 84 | * @param {String} fileName The filename of the image. 85 | * @param {callback} callback Function to call on completion. 86 | */ 87 | retrieve(fileName, callback) { 88 | let filePath = path.resolve(DEFAULT_IMAGES_FOLDER + '/' + fileName); 89 | 90 | fs.access(filePath, fs.F_OK, (err) => { 91 | if(err) { 92 | return callback(HTTP_STATUS_NOT_FOUND); 93 | } 94 | return callback(HTTP_STATUS_OK, filePath); 95 | }); 96 | } 97 | 98 | } 99 | 100 | 101 | module.exports = ImagesManager; 102 | -------------------------------------------------------------------------------- /lib/jsonsilo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const express = require('express'); 8 | const path = require('path'); 9 | const DatabaseManager = require('./databasemanager'); 10 | const StoriesManager = require('./storiesmanager'); 11 | const ImagesManager = require('./imagesmanager'); 12 | 13 | 14 | /** 15 | * JSONSilo Class 16 | * Data silo for digital twins in context-aware physical spaces. 17 | */ 18 | class JSONSilo { 19 | 20 | /** 21 | * JSONSilo constructor 22 | * @param {Object} options The configuration options. 23 | * @constructor 24 | */ 25 | constructor(options) { 26 | let self = this; 27 | options = options || {}; 28 | 29 | if(options.app) { 30 | configureExpress(options.app, self); 31 | } 32 | 33 | this.database = new DatabaseManager(options); 34 | this.stories = new StoriesManager(options, self.database); 35 | this.images = new ImagesManager(options); 36 | 37 | console.log('reelyActive json-silo instance is hosting digital twins in an open IoT'); 38 | } 39 | 40 | } 41 | 42 | 43 | /** 44 | * Configure the routes of the API. 45 | * @param {Express} app The Express app. 46 | * @param {JSONSilo} instance The JSON Silo instance. 47 | */ 48 | function configureExpress(app, instance) { 49 | app.use(function(req, res, next) { 50 | req.jsonsilo = instance; 51 | next(); 52 | }); 53 | app.use('/stories', require('./routes/stories')); 54 | app.use('/store', require('./routes/store')); 55 | app.use('/', express.static(path.resolve(__dirname + '/../web'))); 56 | } 57 | 58 | 59 | module.exports = JSONSilo; 60 | -------------------------------------------------------------------------------- /lib/routes/responsehandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2019 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | // TODO: move to a library 7 | 8 | 9 | const MESSAGE_OK = "ok"; 10 | const MESSAGE_CREATED = "created"; 11 | const MESSAGE_NOTFOUND = "notFound"; 12 | const MESSAGE_BADREQUEST = "badRequest"; 13 | const MESSAGE_NOTIMPLEMENTED = "notImplemented"; 14 | const MESSAGE_SERVICEUNAVAILABLE = "serviceUnavailable"; 15 | const CODE_OK = 200; 16 | const CODE_CREATED = 201; 17 | const CODE_NOTFOUND = 404; 18 | const CODE_BADREQUEST = 400; 19 | const CODE_NOTIMPLEMENTED = 501; 20 | const CODE_SERVICEUNAVAILABLE = 503; 21 | 22 | 23 | /** 24 | * Prepare the JSON for an API query response. 25 | * @param {Object} req The HTTP request. 26 | * @param {Number} status Integer status code. 27 | * @param {Object} data The data to include in the response. 28 | */ 29 | function prepareResponse(req, status, data) { 30 | let rootUrl = req.protocol + '://' + req.get('host'); 31 | let queryPath = req.originalUrl; 32 | let response = {}; 33 | 34 | prepareMeta(response, status); 35 | prepareLinks(response, rootUrl, queryPath); 36 | prepareData(response, data); 37 | 38 | return response; 39 | } 40 | 41 | 42 | /** 43 | * Prepares and adds the _meta to the given API query response. 44 | * @param {Object} response JSON representation of the response. 45 | * @param {Number} status Integer status code. 46 | */ 47 | function prepareMeta(response, status) { 48 | switch(status) { 49 | case CODE_OK: 50 | response._meta = { "message": MESSAGE_OK, 51 | "statusCode": CODE_OK }; 52 | break; 53 | case CODE_CREATED: 54 | response._meta = { "message": MESSAGE_CREATED, 55 | "statusCode": CODE_CREATED }; 56 | break; 57 | case CODE_NOTFOUND: 58 | response._meta = { "message": MESSAGE_NOTFOUND, 59 | "statusCode": CODE_NOTFOUND }; 60 | break; 61 | case CODE_NOTIMPLEMENTED: 62 | response._meta = { "message": MESSAGE_NOTIMPLEMENTED, 63 | "statusCode": CODE_NOTIMPLEMENTED }; 64 | break; 65 | case CODE_SERVICEUNAVAILABLE: 66 | response._meta = { "message": MESSAGE_SERVICEUNAVAILABLE, 67 | "statusCode": CODE_SERVICEUNAVAILABLE }; 68 | break; 69 | default: 70 | response._meta = { "message": MESSAGE_BADREQUEST, 71 | "statusCode": CODE_BADREQUEST }; 72 | } 73 | } 74 | 75 | 76 | /** 77 | * Prepares and adds the _links to the given API query response. 78 | * @param {Object} response JSON representation of the response. 79 | * @param {String} rootUrl The root URL of the original query. 80 | * @param {String} queryPath The query path of the original query. 81 | */ 82 | function prepareLinks(response, rootUrl, queryPath) { 83 | response._links = {}; 84 | response._links.self = { "href": rootUrl + queryPath }; 85 | } 86 | 87 | 88 | /** 89 | * Prepares and adds the data to the given API query response. 90 | * @param {Object} response JSON representation of the response. 91 | * @param {Object} data The data to add to the response. 92 | */ 93 | function prepareData(response, data) { 94 | if(!data) { 95 | return; 96 | } 97 | 98 | for(let property in data) { 99 | let item = data[property]; 100 | 101 | if(Array.isArray(item)) { 102 | response[property] = {}; 103 | item.forEach(function(element) { 104 | if(element.hasOwnProperty('_id')) { 105 | let id = element._id; 106 | delete element._id; 107 | response[property][id] = element; 108 | } 109 | else { 110 | // TODO: handle elegantly the lack of _id 111 | } 112 | }); 113 | } 114 | else { 115 | response[property] = item; 116 | } 117 | } 118 | } 119 | 120 | 121 | module.exports.prepareResponse = prepareResponse; 122 | -------------------------------------------------------------------------------- /lib/routes/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2019-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const express = require('express'); 8 | const multer = require('multer'); 9 | const path = require('path'); 10 | const responseHandler = require('./responsehandler'); 11 | 12 | 13 | const SUPPORTED_IMAGE_FILE_TYPES = /jpeg|jpg|png|gif/; 14 | const IMAGE_FILE_NAME = 'image'; 15 | 16 | 17 | let router = express.Router(); 18 | let upload = multer({ fileFilter: fileFilter }); 19 | 20 | 21 | router.route('/images') 22 | .post(upload.single(IMAGE_FILE_NAME), function(req, res) { 23 | createImage(req, res); 24 | }); 25 | 26 | router.route('/images/:id') 27 | .get(function(req, res) { 28 | retrieveImage(req, res); 29 | }); 30 | 31 | 32 | /** 33 | * Create the given image. 34 | * @param {Object} req The HTTP request. 35 | * @param {Object} res The HTTP result. 36 | */ 37 | function createImage(req, res) { 38 | let images = req.jsonsilo.images; 39 | 40 | images.create(req, res, function(status, data) { 41 | let response = responseHandler.prepareResponse(req, status, data); 42 | res.status(status).json(response); 43 | }); 44 | } 45 | 46 | 47 | /** 48 | * Retrieve the given image. 49 | * @param {Object} req The HTTP request. 50 | * @param {Object} res The HTTP result. 51 | */ 52 | function retrieveImage(req, res) { 53 | let images = req.jsonsilo.images; 54 | let imageFileName = req.params.id; 55 | 56 | images.retrieve(imageFileName, function(status, data) { 57 | if(data) { 58 | res.sendFile(data); 59 | } 60 | else { 61 | res.status(status).send(); 62 | } 63 | }); 64 | } 65 | 66 | 67 | /** 68 | * Reject image files with incompatible mimetype and/or extname. 69 | * @param {Object} req The HTTP request. 70 | * @param {Object} file The multipart file. 71 | * @param {function} callback Function to call on completion. 72 | */ 73 | function fileFilter(req, file, callback) { 74 | let isValidExtname = SUPPORTED_IMAGE_FILE_TYPES 75 | .test(path.extname(file.originalname) 76 | .toLowerCase()); 77 | let isValidMimetype = SUPPORTED_IMAGE_FILE_TYPES.test(file.mimetype); 78 | 79 | if(isValidMimetype && isValidExtname) { 80 | return callback(null, true); 81 | } 82 | else { 83 | return callback(null, false); 84 | } 85 | } 86 | 87 | 88 | module.exports = router; 89 | -------------------------------------------------------------------------------- /lib/routes/stories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2019 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const express = require('express'); 8 | const path = require('path'); 9 | const responseHandler = require('./responsehandler'); 10 | 11 | 12 | let router = express.Router(); 13 | 14 | router.route('/') 15 | .post(function(req, res) { 16 | createStory(req, res); 17 | }); 18 | 19 | router.route('/:id') 20 | .get(function(req, res) { 21 | retrieveStory(req, res); 22 | }); 23 | 24 | 25 | /** 26 | * Create the given story 27 | * @param {Object} req The HTTP request. 28 | * @param {Object} res The HTTP result. 29 | */ 30 | function createStory(req, res) { 31 | let story = req.body; 32 | let stories = req.jsonsilo.stories; 33 | stories.create(story, function(status, data) { 34 | let response = responseHandler.prepareResponse(req, status, data); 35 | res.status(status).json(response); 36 | }); 37 | } 38 | 39 | 40 | /** 41 | * Retrieve the given story 42 | * @param {Object} req The HTTP request. 43 | * @param {Object} res The HTTP result. 44 | */ 45 | function retrieveStory(req, res) { 46 | switch(req.accepts(['json','html'])) { 47 | case 'html': 48 | res.sendFile(path.resolve(__dirname + '/../../web/json-silo/stories/index.html')); 49 | break; 50 | default: 51 | let id = req.params.id; 52 | let stories = req.jsonsilo.stories; 53 | stories.retrieve(id, function(status, data) { 54 | let response = responseHandler.prepareResponse(req, status, data); 55 | res.status(status).json(response); 56 | }); 57 | } 58 | } 59 | 60 | 61 | module.exports = router; 62 | -------------------------------------------------------------------------------- /lib/storiesmanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | const crypto = require('crypto'); 8 | 9 | 10 | const DEFAULT_IDENTIFIER_LENGTH = 4; 11 | const HTTP_STATUS_OK = 200; 12 | const HTTP_STATUS_CREATED = 201; 13 | const HTTP_STATUS_BAD_REQUEST = 400; 14 | const HTTP_STATUS_NOT_FOUND = 404; 15 | 16 | 17 | /** 18 | * StoriesManager Class 19 | * Manages the storage and retrieval of JSON stories. 20 | */ 21 | class StoriesManager { 22 | 23 | /** 24 | * StoriesManager constructor 25 | * @param {Object} options The options as a JSON object. 26 | * @param {DatabaseManager} database The database manager. 27 | * @constructor 28 | */ 29 | constructor(options, database) { 30 | let self = this; 31 | options = options || {}; 32 | self.database = database; 33 | self.identifierLength = options.identifierLength || 34 | DEFAULT_IDENTIFIER_LENGTH; 35 | } 36 | 37 | /** 38 | * Create a new story 39 | * @param {String} story The story to create in the database 40 | * @param {callback} callback Function to call on completion 41 | */ 42 | create(story, callback) { 43 | let self = this; 44 | let key = crypto.randomBytes(this.identifierLength).toString('hex'); 45 | 46 | this.database.has(key, function(err, isPresent) { 47 | if(err) { 48 | return callback(HTTP_STATUS_BAD_REQUEST); 49 | } 50 | else if(isPresent) { 51 | return create(story, callback); 52 | } 53 | else { 54 | self.database.set(key, story, function(err) { 55 | if(err) { 56 | return callback(HTTP_STATUS_BAD_REQUEST); 57 | } 58 | else { 59 | let data = { stories: {} }; 60 | data.stories[key] = story; 61 | return callback(HTTP_STATUS_CREATED, data); 62 | } 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Retrieve an existing story 70 | * @param {String} id The id of the story 71 | * @param {callback} callback Function to call on completion 72 | */ 73 | retrieve(id, callback) { 74 | this.database.get(id, function(err, story) { 75 | if(err) { 76 | return callback(HTTP_STATUS_BAD_REQUEST); 77 | } 78 | else if(story === undefined) { 79 | return callback(HTTP_STATUS_NOT_FOUND); 80 | } 81 | else { 82 | let data = { stories: {} }; 83 | data.stories[id] = story; 84 | return callback(HTTP_STATUS_OK, data); 85 | } 86 | }); 87 | } 88 | 89 | } 90 | 91 | 92 | module.exports = StoriesManager; 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-silo", 3 | "homepage": "https://www.reelyactive.com", 4 | "author": { 5 | "name": "reelyActive", 6 | "email": "info@reelyactive.com" 7 | }, 8 | "description": "A data silo for digital twins in context-aware physical spaces. We believe in an open Internet of Things.", 9 | "keywords": [ 10 | "reelyActive", 11 | "IoT", 12 | "Personal Data Locker", 13 | "digital twins", 14 | "Schema.org", 15 | "JSON-LD" 16 | ], 17 | "version": "1.1.5", 18 | "engines": { 19 | "node": ">=10.0.0" 20 | }, 21 | "main": "./lib/jsonsilo.js", 22 | "scripts": { 23 | "start": "node bin/json-silo" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/reelyactive/json-silo.git" 28 | }, 29 | "dependencies": { 30 | "esmapdb": "^0.3.0", 31 | "express": "^4.19.2", 32 | "multer": "^1.4.5-lts.1", 33 | "sharp": "^0.33.4" 34 | }, 35 | "devDependencies": {}, 36 | "bugs": { 37 | "url": "https://github.com/reelyactive/json-silo/issues" 38 | }, 39 | "contributors": [ 40 | { 41 | "name": "Jeffrey Dungen" 42 | }, 43 | { 44 | "name": "George Koulouris" 45 | }, 46 | { 47 | "name": "Furaha Damien" 48 | } 49 | ], 50 | "license": "MIT" 51 | } 52 | -------------------------------------------------------------------------------- /web/json-silo/images/json-silo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reelyactive/json-silo/1ece5c5202e4b5e9a7798b20def56c22648b583c/web/json-silo/images/json-silo.png -------------------------------------------------------------------------------- /web/json-silo/images/pareto-anywhere-logo-nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reelyactive/json-silo/1ece5c5202e4b5e9a7798b20def56c22648b583c/web/json-silo/images/pareto-anywhere-logo-nav.png -------------------------------------------------------------------------------- /web/json-silo/images/reelyactive-logo-square-nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reelyactive/json-silo/1ece5c5202e4b5e9a7798b20def56c22648b583c/web/json-silo/images/reelyactive-logo-square-nav.png -------------------------------------------------------------------------------- /web/json-silo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | json-silo by reelyActive 12 | 13 | 14 | 15 | 132 | 133 |
134 |
135 |
136 | 138 |
139 |
140 |
141 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /web/json-silo/js/bootstrappyactive.bundle.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); 7 | //# sourceMappingURL=bootstrap.bundle.min.js.map -------------------------------------------------------------------------------- /web/json-silo/js/color-modes.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. 5 | */ 6 | 7 | (() => { 8 | 'use strict' 9 | 10 | const getStoredTheme = () => localStorage.getItem('theme') 11 | const setStoredTheme = theme => localStorage.setItem('theme', theme) 12 | 13 | const getPreferredTheme = () => { 14 | const storedTheme = getStoredTheme() 15 | if (storedTheme) { 16 | return storedTheme 17 | } 18 | 19 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 20 | } 21 | 22 | const setTheme = theme => { 23 | if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark') 25 | } else { 26 | document.documentElement.setAttribute('data-bs-theme', theme) 27 | } 28 | } 29 | 30 | setTheme(getPreferredTheme()) 31 | 32 | const showActiveTheme = (theme, focus = false) => { 33 | const themeSwitcher = document.querySelector('#bd-theme') 34 | 35 | if (!themeSwitcher) { 36 | return 37 | } 38 | 39 | const themeSwitcherText = document.querySelector('#bd-theme-text') 40 | const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) 41 | 42 | document.querySelectorAll('[data-bs-theme-value]').forEach(element => { 43 | element.classList.remove('active') 44 | element.setAttribute('aria-pressed', 'false') 45 | }) 46 | 47 | btnToActive.classList.add('active') 48 | btnToActive.setAttribute('aria-pressed', 'true') 49 | } 50 | 51 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 52 | const storedTheme = getStoredTheme() 53 | if (storedTheme !== 'light' && storedTheme !== 'dark') { 54 | setTheme(getPreferredTheme()) 55 | } 56 | }) 57 | 58 | window.addEventListener('DOMContentLoaded', () => { 59 | showActiveTheme(getPreferredTheme()) 60 | 61 | document.querySelectorAll('[data-bs-theme-value]') 62 | .forEach(toggle => { 63 | toggle.addEventListener('click', () => { 64 | const theme = toggle.getAttribute('data-bs-theme-value') 65 | setStoredTheme(theme) 66 | setTheme(theme) 67 | showActiveTheme(theme, true) 68 | }) 69 | }) 70 | }) 71 | })() 72 | -------------------------------------------------------------------------------- /web/json-silo/js/cormorant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2016-2023 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | let cormorant = (function() { 8 | 9 | // Internal constants 10 | const STATUS_OK = 200; 11 | const SIGNATURE_SEPARATOR = '/'; 12 | 13 | // Internal variables 14 | let associations = new Map(); 15 | let stories = new Map(); 16 | let digitalTwins = new Map(); 17 | 18 | // Extract the JSON-LD, if present, from the given HTML 19 | function extractFromHtml(html) { 20 | let tagIndex = html.search(/()/); 21 | if(tagIndex < 0) { 22 | return null; 23 | } 24 | let startIndex = html.indexOf('>', tagIndex) + 1; 25 | let stopIndex = html.indexOf('', startIndex); 26 | 27 | try { 28 | return parseAsStory(JSON.parse(html.substring(startIndex, stopIndex))); 29 | } 30 | catch(error) { 31 | return null; 32 | } 33 | } 34 | 35 | // Parse the given JSON as a standardised story 36 | function parseAsStory(data) { 37 | // Handle standard reelyActive API response case 38 | if(data.hasOwnProperty('stories')) { 39 | let storyId = Object.keys(data.stories)[0]; 40 | let story = data.stories[storyId]; 41 | 42 | return story; 43 | } 44 | 45 | return data; 46 | } 47 | 48 | // Perform a HTTP GET on the given URL with the given accept headers 49 | function retrieve(url, acceptHeaders, callback) { 50 | fetch(url, { headers: { "Accept": acceptHeaders } }) 51 | .then((response) => { 52 | if(!response.ok) { throw new Error('GET returned ' + response.status); } 53 | let contentType = response.headers.get('Content-Type'); 54 | if(contentType.startsWith('application/json')) { 55 | return response.json(); 56 | } 57 | return response.text(); 58 | }) 59 | .then((result) => { return callback(result); }) 60 | .catch((error) => { return callback(null); }); 61 | } 62 | 63 | // Get the associations for the given device signature 64 | function retrieveAssociations(serverUrl, deviceSignature, options, callback) { 65 | options = options || {}; 66 | let url = serverUrl + '/associations/' + deviceSignature; 67 | 68 | retrieve(url, 'application/json', (data) => { 69 | let deviceAssociations = null; 70 | let isStoryBeingRetrieved = false; 71 | let isJsonData = (data !== null) && (typeof data === 'object'); 72 | 73 | if(isJsonData) { 74 | let returnedDeviceId = null; 75 | if(data.hasOwnProperty('associations')) { // chickadee v1.x 76 | returnedDeviceSignature = Object.keys(data.associations)[0]; 77 | deviceAssociations = data.associations[returnedDeviceSignature]; 78 | } 79 | else if(data.hasOwnProperty('devices')) { // chickadee v0.x 80 | returnedDeviceSignature = Object.keys(data.devices)[0]; 81 | deviceAssociations = data.devices[returnedDeviceSignature]; 82 | } 83 | associations.set(deviceSignature, deviceAssociations); 84 | associations.set(returnedDeviceSignature, deviceAssociations); 85 | 86 | if(options.isStoryToBeRetrieved && deviceAssociations.url) { 87 | isStoryBeingRetrieved = true; 88 | retrieveStory(deviceAssociations.url, options, 89 | (story, isRetrievedFromMemory) => { 90 | return callback(deviceAssociations, story, isRetrievedFromMemory); 91 | }); 92 | } 93 | } 94 | 95 | if(!isStoryBeingRetrieved) { 96 | return callback(deviceAssociations); 97 | } 98 | }); 99 | } 100 | 101 | // Get the story for the given URL 102 | function retrieveStory(storyUrl, options, callback) { 103 | options = options || {}; 104 | 105 | if(stories.has(storyUrl) && !options.isStoryToBeRefetched) { 106 | return callback(stories.get(storyUrl), true); 107 | } 108 | 109 | retrieve(storyUrl, 'application/json, text/plain', (data) => { 110 | if(!data) { return callback(null, false); } 111 | 112 | let isJsonData = (typeof data === 'object'); 113 | story = isJsonData ? parseAsStory(data) : extractFromHtml(data); 114 | if(story) { stories.set(storyUrl, story); } 115 | 116 | return callback(story, false); 117 | }); 118 | } 119 | 120 | // Get the digital twin for the given device 121 | function retrieveDigitalTwin(deviceSignature, device, options, callback) { 122 | options = options || {}; 123 | 124 | if(digitalTwins.has(deviceSignature)) { 125 | // TODO: update digital twin timestamp, refresh if necessary? 126 | return callback(digitalTwins.get(deviceSignature), true); 127 | } 128 | 129 | if(!associations.has(deviceSignature) && options.associationsServerUrl) { 130 | retrieveAssociations(options.associationsServerUrl, deviceSignature, 131 | { isStoryToBeRetrieved: true }, 132 | (deviceAssociations, story) => { 133 | updateDigitalTwin(deviceSignature, story); 134 | return callback(digitalTwins.get(deviceSignature), false); 135 | }); 136 | } 137 | else if(device) { 138 | if(device.url) { 139 | retrieveStory(device.url, {}, (story) => { 140 | updateDigitalTwin(deviceSignature, story); 141 | // TODO: device.statid.uri 142 | return callback(digitalTwins.get(deviceSignature), false); 143 | }); 144 | } 145 | else if(device.statid && device.statid.uri) { 146 | retrieveStory(device.statid.uri, {}, (story) => { 147 | updateDigitalTwin(deviceSignature, story); 148 | return callback(digitalTwins.get(deviceSignature), false); 149 | }); 150 | } 151 | } 152 | else { 153 | return callback(null); 154 | } 155 | } 156 | 157 | // Update the digital twin of the given device using the given story 158 | function updateDigitalTwin(deviceSignature, story) { 159 | if(!story) { return; } 160 | 161 | let storyCovers = []; 162 | 163 | if(story.hasOwnProperty('@graph') && Array.isArray(story['@graph'])) { 164 | story['@graph'].forEach(storyElement => { 165 | let storyCover = determineStoryCover(storyElement); 166 | if(storyCover) { storyCovers.push(storyCover); } 167 | }); 168 | digitalTwins.set(deviceSignature, { story: story, 169 | storyCovers: storyCovers }); 170 | } 171 | else { 172 | // TODO: transform to graph (flattened) representation 173 | } 174 | } 175 | 176 | // Determine the title and image for the storyCover 177 | function determineStoryCover(element) { 178 | let title; 179 | let imageUrl; 180 | 181 | if(element.hasOwnProperty("schema:name")) { 182 | title = element["schema:name"]; 183 | } 184 | else if(element.hasOwnProperty("schema:givenName") || 185 | element.hasOwnProperty("schema:familyName")) { 186 | title = (element["schema:givenName"] || '') + ' ' + 187 | (element["schema:familyName"] || ''); 188 | } 189 | 190 | if(element.hasOwnProperty("schema:image")) { 191 | imageUrl = element["schema:image"]; 192 | } 193 | else if(element.hasOwnProperty("schema:logo")) { 194 | imageUrl = element["schema:logo"]; 195 | } 196 | 197 | if(title || imageUrl) { 198 | return { title: title || '', imageUrl: imageUrl } 199 | } 200 | 201 | return null; 202 | } 203 | 204 | // Expose the following functions and variables 205 | return { 206 | retrieveAssociations: retrieveAssociations, 207 | retrieveStory: retrieveStory, 208 | retrieveDigitalTwin, retrieveDigitalTwin, 209 | associations: associations, 210 | stories: stories, 211 | digitalTwins: digitalTwins 212 | } 213 | 214 | }()); 215 | -------------------------------------------------------------------------------- /web/json-silo/js/cuttlefish-story.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2016-2021 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | let cuttlefishStory = (function() { 8 | 9 | // Internal constants 10 | 11 | // Render a story 12 | function render(story, target, options) { 13 | let element = determinePrimaryElement(story); 14 | let title = determineElementTitle(element); 15 | let imageUrl = determineElementImageUrl(element); 16 | let isRenderableStory = (title || imageUrl); 17 | let card = new DocumentFragment(); 18 | 19 | if(isRenderableStory) { 20 | let cardComponents = []; 21 | 22 | if(imageUrl) { 23 | let img = createElement('img', 'card-img-top'); 24 | img.setAttribute('src', imageUrl); 25 | cardComponents.push(img); 26 | } 27 | 28 | if(title) { 29 | let cardTitle = createElement('h5', 'card-title', title); 30 | let cardBody = createElement('div', 'card-body', cardTitle); 31 | cardComponents.push(cardBody); 32 | } 33 | 34 | card = createElement('div', 'card', cardComponents); 35 | } 36 | 37 | if(target) { 38 | target.replaceChildren(card); 39 | } 40 | 41 | return card; 42 | } 43 | 44 | // Determine the primary story element, or empty object if none 45 | function determinePrimaryElement(story) { 46 | if(story && story.hasOwnProperty('@graph') && 47 | Array.isArray(story['@graph'])) { 48 | return story['@graph'][0]; 49 | } 50 | 51 | return {}; 52 | } 53 | 54 | // Determine the title of the story 55 | function determineTitle(story) { 56 | let element = determinePrimaryElement(story); 57 | 58 | return determineElementTitle(element); 59 | } 60 | 61 | // Determine the title of the element 62 | function determineElementTitle(element) { 63 | if(element.hasOwnProperty("schema:name")) { 64 | return element["schema:name"]; 65 | } 66 | else if(element.hasOwnProperty("schema:givenName") || 67 | element.hasOwnProperty("schema:familyName")) { 68 | return (element["schema:givenName"] || '') + ' ' + 69 | (element["schema:familyName"] || ''); 70 | } 71 | else { 72 | return null; 73 | } 74 | } 75 | 76 | // Determine the image URL of the story 77 | function determineImageUrl(story) { 78 | let element = determinePrimaryElement(story); 79 | 80 | return determineElementImageUrl(element); 81 | } 82 | 83 | // Determine the image URL of the element 84 | function determineElementImageUrl(element) { 85 | if(element.hasOwnProperty("schema:image")) { 86 | return element["schema:image"]; 87 | } 88 | else if(element.hasOwnProperty("schema:logo")) { 89 | return element["schema:logo"]; 90 | } 91 | 92 | return null; 93 | } 94 | 95 | // Create an element as specified, appending optional content as child(ren) 96 | function createElement(elementName, classNames, content) { 97 | let element = document.createElement(elementName); 98 | 99 | if(classNames) { 100 | element.setAttribute('class', classNames); 101 | } 102 | 103 | if((content instanceof Element) || (content instanceof DocumentFragment)) { 104 | element.appendChild(content); 105 | } 106 | else if(Array.isArray(content)) { 107 | content.forEach(function(item) { 108 | if((item instanceof Element) || (item instanceof DocumentFragment)) { 109 | element.appendChild(item); 110 | } 111 | else { 112 | element.appendChild(document.createTextNode(item)); 113 | } 114 | }); 115 | } 116 | else if(content) { 117 | element.appendChild(document.createTextNode(content)); 118 | } 119 | 120 | return element; 121 | } 122 | 123 | // Expose the following functions and variables 124 | return { 125 | render: render, 126 | determineImageUrl: determineImageUrl, 127 | determineTitle: determineTitle 128 | } 129 | 130 | }()); -------------------------------------------------------------------------------- /web/json-silo/js/fontawesome-reelyactive.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | -------------------------------------------------------------------------------- /web/json-silo/js/jsonsilo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2015-2019 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | // Constants 8 | const STORIES_ROUTE = '/stories'; 9 | const IMAGES_ROUTE = '/images'; 10 | const DEFAULT_IMAGE_PROPERTY_NAME = 'image'; 11 | const DEFAULT_STORY = { 12 | "@context": { "schema": "https://schema.org/" }, 13 | "@graph": [] 14 | }; 15 | const DEFAULT_PERSON_ELEMENT = { "@id": "person", "@type": "schema:Person" }; 16 | 17 | 18 | // DOM elements 19 | let personForm = document.querySelector('#personForm'); 20 | let personGivenName = document.querySelector('#personGivenName'); 21 | let personFamilyName = document.querySelector('#personFamilyName'); 22 | let personImageInput = document.querySelector('#personImageInput'); 23 | let storeStory = document.querySelector('#storeStory'); 24 | let storeButton = document.querySelector('#storeButton'); 25 | let accessStory = document.querySelector('#accessStory'); 26 | let copyButton = document.querySelector('#copyButton'); 27 | let visitButton = document.querySelector('#visitButton'); 28 | let anotherButton = document.querySelector('#anotherButton'); 29 | let visualPreview = document.querySelector('#visualPreview'); 30 | let storyPreview = document.querySelector('#storyPreview'); 31 | let storyUrl = document.querySelector('#storyUrl'); 32 | let error = document.querySelector('#error'); 33 | 34 | 35 | // Other variables 36 | let personStory = Object.assign({}, DEFAULT_STORY); 37 | let personElement = Object.assign({}, DEFAULT_PERSON_ELEMENT); 38 | personStory['@graph'].push(personElement); 39 | let personImgSrc; 40 | 41 | 42 | /** 43 | * Uploads an image to the file system 44 | * @param {callback} callback Function to call upon completion 45 | */ 46 | function addImage(callback) { 47 | let formData = new FormData(); 48 | formData.append(DEFAULT_IMAGE_PROPERTY_NAME, personImageInput.files[0]); 49 | 50 | let httpRequest = new XMLHttpRequest(); 51 | httpRequest.onload = function(oevent){ 52 | if(httpRequest.status === 200) { 53 | let response = JSON.parse(httpRequest.responseText); 54 | let imageId = Object.keys(response.images)[0]; 55 | let image = response.images[imageId]; 56 | let url = response._links.self.href + '/' + imageId; 57 | error.textContent = ''; 58 | return callback(url); 59 | } 60 | else if(httpRequest.status === 204) { 61 | error.textContent = 'wrong file format'; 62 | } 63 | else if(httpRequest.status === 422) { 64 | error.textContent = 'We could not detect any file'; 65 | } 66 | else { 67 | //textImage.textContent = 'something went wrong while uploading image'; 68 | } 69 | return callback(); 70 | }; 71 | httpRequest.open('POST', IMAGES_ROUTE, true); 72 | httpRequest.send(formData); 73 | } 74 | 75 | 76 | /** 77 | * Obtains story and sends it to the database 78 | * @param {callback} callback Function to call upon completion 79 | */ 80 | function addStory(callback) { 81 | let httpRequest = new XMLHttpRequest(); 82 | let personStoryString = JSON.stringify(personStory); 83 | httpRequest.onreadystatechange = function(){ 84 | if(httpRequest.readyState === XMLHttpRequest.DONE) { 85 | if(httpRequest.status === 200) { 86 | let response = JSON.parse(httpRequest.responseText); 87 | let storyId = Object.keys(response.stories)[0]; 88 | let story = response.stories[storyId]; 89 | let url = response._links.self.href + '/' + storyId; 90 | storyUrl.value = url; 91 | visitButton.href = url; 92 | } 93 | return callback(); 94 | } 95 | }; 96 | httpRequest.open('POST', STORIES_ROUTE); 97 | httpRequest.setRequestHeader('Content-Type', 'application/json'); 98 | httpRequest.setRequestHeader('Accept', 'application/json'); 99 | httpRequest.send(personStoryString); 100 | } 101 | 102 | 103 | // Update the person element 104 | function updatePersonElement() { 105 | if(personGivenName.value === '') { 106 | delete personElement['schema:givenName']; 107 | } 108 | else { 109 | personElement['schema:givenName'] = personGivenName.value; 110 | } 111 | 112 | if(personFamilyName.value === '') { 113 | delete personElement['schema:familyName']; 114 | } 115 | else { 116 | personElement['schema:familyName'] = personFamilyName.value; 117 | } 118 | 119 | storyPreview.textContent = JSON.stringify(personStory, null, 2); 120 | cuttlefish.render(personStory, visualPreview); 121 | } 122 | 123 | 124 | // Update the person's image source based on the uploaded image 125 | function updatePersonImageSrc() { 126 | let input = this; 127 | if(input.files && input.files[0]) { 128 | let reader = new FileReader(); 129 | 130 | reader.onload = function(e) { 131 | personImgSrc = e.target.result; 132 | personElement['schema:image'] = personImgSrc; 133 | cuttlefish.render(personStory, visualPreview); 134 | } 135 | reader.readAsDataURL(input.files[0]); 136 | } 137 | } 138 | 139 | 140 | // Handle user request to publish story 141 | function publishStory() { 142 | storeStory.hidden = true; 143 | 144 | if(personImgSrc) { 145 | addImage(function(imageUrl) { 146 | if(imageUrl) { 147 | personElement['schema:image'] = imageUrl; 148 | } 149 | else { /* TODO: handle image errors */ } 150 | addStory(function() { 151 | accessStory.hidden = false; 152 | }); 153 | }); 154 | } 155 | else { 156 | addStory(function() { 157 | accessStory.hidden = false; 158 | }); 159 | } 160 | } 161 | 162 | 163 | // Handle user request to copy the story URL 164 | function copyStoryUrl() { 165 | storyUrl.select(); 166 | document.execCommand('copy'); 167 | } 168 | 169 | 170 | // Handle the user request to create another story 171 | function anotherStory() { 172 | accessStory.hidden = true; 173 | storeStory.hidden = false; 174 | } 175 | 176 | 177 | personForm.addEventListener('keyup', updatePersonElement); 178 | personImageInput.addEventListener('change', updatePersonImageSrc); 179 | storeButton.addEventListener('click', publishStory); 180 | copyButton.addEventListener('click', copyStoryUrl); 181 | anotherButton.addEventListener('click', anotherStory); 182 | -------------------------------------------------------------------------------- /web/json-silo/js/stories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright reelyActive 2014-2022 3 | * We believe in an open Internet of Things 4 | */ 5 | 6 | 7 | // Constants 8 | const STORIES_ROUTE = '/stories'; 9 | const MESSAGE_NOT_FOUND = 'Story Not Found [404].'; 10 | 11 | 12 | // DOM elements 13 | let returnButton = document.querySelector('#returnbutton'); 14 | let visualisation = document.querySelector('#visualisation'); 15 | let jsonResponse = document.querySelector('#jsonResponse'); 16 | let loading = document.querySelector('#loading'); 17 | let error = document.querySelector('#error'); 18 | let errorMessage = document.querySelector('#errorMessage'); 19 | 20 | 21 | // Other variables 22 | let queryUrl = window.location.href; 23 | let storiesUrl = window.location.protocol + '//' + window.location.hostname + 24 | ':' + window.location.port + STORIES_ROUTE; 25 | let isRootQuery = false; 26 | 27 | 28 | // Hide "return to /stories" button when already querying /stories 29 | if((window.location.pathname.endsWith(STORIES_ROUTE )) || 30 | (window.location.pathname.endsWith(STORIES_ROUTE + '/'))) { 31 | isRootQuery = true; 32 | returnButton.hidden = true; 33 | } 34 | 35 | // Retrieve and render the story 36 | if(!isRootQuery) { 37 | cormorant.retrieveStory(window.location.href, function(story) { 38 | jsonResponse.textContent = JSON.stringify(story, null, 2); 39 | loading.hidden = true; 40 | 41 | if(story) { 42 | cuttlefishStory.render(story, visualisation); 43 | visualisation.hidden = false; 44 | } 45 | else { 46 | errorMessage.textContent = MESSAGE_NOT_FOUND; 47 | error.hidden = false; 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /web/json-silo/stories/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | json-silo by reelyActive 12 | 13 | 14 | 15 | 132 | 133 |
134 |
135 |
136 | 151 |
152 | 153 |
155 |

156 | 157 | 158 | 159 |   GET /stories in progress... 160 |

161 | 167 | 168 |
169 | 170 |
172 |

173 |             
174 |
175 |

176 | 177 |   /stories 178 | 179 |

180 |
181 |
182 |
183 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | --------------------------------------------------------------------------------