Browse Source

Multiple Admin Interface fixes and some others.

Misc:
- Fixed hadolint workflow, new git cli needs some extra arguments.
- Add ignore paths to all specific on triggers.
- Updated hadolint version.
- Made SMTP_DEBUG read-only, since it can't be changed at runtime.

Admin:
- Migrated from Bootstrap v4 to v5
- Updated jquery to v3.6.0
- Updated Datatables
- Made Javascript strict
- Added a way to show which ENV Vars are overridden.
- Changed the way to provide data for handlebars.
- Fixed date/time check.
- Made support string use details and summary feature of markdown/github.
BlackDex 4 years ago
parent
commit
8615736e84

+ 15 - 2
.github/workflows/build.yml

@@ -2,6 +2,19 @@ name: Build
 
 on:
   push:
+    paths-ignore:
+      - "*.md"
+      - "*.txt"
+      - ".dockerignore"
+      - ".env.template"
+      - ".gitattributes"
+      - ".gitignore"
+      - "azure-pipelines.yml"
+      - "docker/**"
+      - "hooks/**"
+      - "tools/**"
+      - ".github/FUNDING.yml"
+      - ".github/ISSUE_TEMPLATE/**"
   pull_request:
     # Ignore when there are only changes done too one of these paths
     paths-ignore:
@@ -39,13 +52,13 @@ jobs:
             features: [sqlite,mysql,postgresql] # Remember to update the `cargo test` to match the amount of features
             channel: nightly
             os: ubuntu-18.04
-            ext:
+            ext: ""
           # - target-triple: x86_64-unknown-linux-gnu
           #   host-triple: x86_64-unknown-linux-gnu
           #   features: "sqlite,mysql,postgresql"
           #   channel: stable
           #   os: ubuntu-18.04
-          #   ext:
+          #   ext: ""
 
     name: Building ${{ matrix.channel }}-${{ matrix.target-triple }}
     runs-on: ${{ matrix.os }}

+ 6 - 3
.github/workflows/hadolint.yml

@@ -2,6 +2,9 @@ name: Hadolint
 
 on:
   push:
+    # Ignore when there are only changes done too one of these paths
+    paths:
+      - "docker/**"
   pull_request:
     # Ignore when there are only changes done too one of these paths
     paths:
@@ -22,14 +25,14 @@ jobs:
       - name: Download hadolint
         shell: bash
         run: |
-          sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
+          sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
           sudo chmod +x /usr/local/bin/hadolint
         env:
-          HADOLINT_VERSION: 2.3.0
+          HADOLINT_VERSION: 2.5.0
       # End Download hadolint
 
       # Test Dockerfiles
       - name: Run hadolint
         shell: bash
-        run:  git ls-files --exclude='docker/*/Dockerfile*' --ignored | xargs hadolint
+        run:  git ls-files --exclude='docker/*/Dockerfile*' --ignored --cached | xargs hadolint
       # End Test Dockerfiles

+ 9 - 42
src/api/admin.rs

@@ -196,9 +196,7 @@ fn _validate_token(token: &str) -> bool {
 struct AdminTemplateData {
     page_content: String,
     version: Option<&'static str>,
-    users: Option<Vec<Value>>,
-    organizations: Option<Vec<Value>>,
-    diagnostics: Option<Value>,
+    page_data: Option<Value>,
     config: Value,
     can_backup: bool,
     logged_in: bool,
@@ -214,51 +212,19 @@ impl AdminTemplateData {
             can_backup: *CAN_BACKUP,
             logged_in: true,
             urlpath: CONFIG.domain_path(),
-            users: None,
-            organizations: None,
-            diagnostics: None,
+            page_data: None,
         }
     }
 
-    fn users(users: Vec<Value>) -> Self {
+    fn with_data(page_content: &str, page_data: Value) -> Self {
         Self {
-            page_content: String::from("admin/users"),
+            page_content: String::from(page_content),
             version: VERSION,
-            users: Some(users),
+            page_data: Some(page_data),
             config: CONFIG.prepare_json(),
             can_backup: *CAN_BACKUP,
             logged_in: true,
             urlpath: CONFIG.domain_path(),
-            organizations: None,
-            diagnostics: None,
-        }
-    }
-
-    fn organizations(organizations: Vec<Value>) -> Self {
-        Self {
-            page_content: String::from("admin/organizations"),
-            version: VERSION,
-            organizations: Some(organizations),
-            config: CONFIG.prepare_json(),
-            can_backup: *CAN_BACKUP,
-            logged_in: true,
-            urlpath: CONFIG.domain_path(),
-            users: None,
-            diagnostics: None,
-        }
-    }
-
-    fn diagnostics(diagnostics: Value) -> Self {
-        Self {
-            page_content: String::from("admin/diagnostics"),
-            version: VERSION,
-            organizations: None,
-            config: CONFIG.prepare_json(),
-            can_backup: *CAN_BACKUP,
-            logged_in: true,
-            urlpath: CONFIG.domain_path(),
-            users: None,
-            diagnostics: Some(diagnostics),
         }
     }
 
@@ -360,7 +326,7 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
         })
         .collect();
 
-    let text = AdminTemplateData::users(users_json).render()?;
+    let text = AdminTemplateData::with_data("admin/users", json!(users_json)).render()?;
     Ok(Html(text))
 }
 
@@ -466,7 +432,7 @@ fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<St
         })
         .collect();
 
-    let text = AdminTemplateData::organizations(organizations_json).render()?;
+    let text = AdminTemplateData::with_data("admin/organizations", json!(organizations_json)).render()?;
     Ok(Html(text))
 }
 
@@ -592,11 +558,12 @@ fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResu
         "db_type": *DB_TYPE,
         "db_version": get_sql_server_version(&conn),
         "admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
+        "overrides": &CONFIG.get_overrides().join(", "),
         "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
         "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
     });
 
-    let text = AdminTemplateData::diagnostics(diagnostics_json).render()?;
+    let text = AdminTemplateData::with_data("admin/diagnostics", diagnostics_json).render()?;
     Ok(Html(text))
 }
 

+ 2 - 2
src/api/web.rs

@@ -91,8 +91,8 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
         "identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
         "datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
         "datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
-        "jquery-3.5.1.slim.js" => {
-            Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.5.1.slim.js")))
+        "jquery-3.6.0.slim.js" => {
+            Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.0.slim.js")))
         }
         _ => err!(format!("Static file not found: {}", filename)),
     }

+ 25 - 8
src/config.rs

@@ -57,6 +57,8 @@ macro_rules! make_config {
 
             _env: ConfigBuilder,
             _usr: ConfigBuilder,
+
+            _overrides: Vec<String>,
         }
 
         #[derive(Debug, Clone, Default, Deserialize, Serialize)]
@@ -113,8 +115,7 @@ macro_rules! make_config {
 
             /// Merges the values of both builders into a new builder.
             /// If both have the same element, `other` wins.
-            fn merge(&self, other: &Self, show_overrides: bool) -> Self {
-                let mut overrides = Vec::new();
+            fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
                 let mut builder = self.clone();
                 $($(
                     if let v @Some(_) = &other.$name {
@@ -176,9 +177,9 @@ macro_rules! make_config {
             )+)+
 
             pub fn prepare_json(&self) -> serde_json::Value {
-                let (def, cfg) = {
+                let (def, cfg, overriden) = {
                     let inner = &self.inner.read().unwrap();
-                    (inner._env.build(), inner.config.clone())
+                    (inner._env.build(), inner.config.clone(), inner._overrides.clone())
                 };
 
                 fn _get_form_type(rust_type: &str) -> &'static str {
@@ -210,6 +211,7 @@ macro_rules! make_config {
                         "default": def.$name,
                         "type":  _get_form_type(stringify!($ty)),
                         "doc": _get_doc(concat!($($doc),+)),
+                        "overridden": overriden.contains(&stringify!($name).to_uppercase()),
                     }, )+
                     ]}, )+ ])
             }
@@ -224,6 +226,15 @@ macro_rules! make_config {
                     stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action },
                 )+)+ })
             }
+
+            pub fn get_overrides(&self) -> Vec<String> {
+                let overrides = {
+                    let inner = &self.inner.read().unwrap();
+                    inner._overrides.clone()
+                };
+
+                overrides
+            }
         }
     };
 
@@ -505,7 +516,7 @@ make_config! {
         /// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
         helo_name:                     String, true,   option;
         /// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
-        smtp_debug:                    bool,   true,   def,     false;
+        smtp_debug:                    bool,   false,  def,     false;
         /// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
         smtp_accept_invalid_certs:     bool,   true,   def,     false;
         /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
@@ -639,7 +650,8 @@ impl Config {
         let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
 
         // Create merged config, config file overwrites env
-        let builder = _env.merge(&_usr, true);
+        let mut _overrides = Vec::new();
+        let builder = _env.merge(&_usr, true, &mut _overrides);
 
         // Fill any missing with defaults
         let config = builder.build();
@@ -651,6 +663,7 @@ impl Config {
                 config,
                 _env,
                 _usr,
+                _overrides,
             }),
         })
     }
@@ -666,9 +679,10 @@ impl Config {
         let config_str = serde_json::to_string_pretty(&builder)?;
 
         // Prepare the combined config
+        let mut overrides = Vec::new();
         let config = {
             let env = &self.inner.read().unwrap()._env;
-            env.merge(&builder, false).build()
+            env.merge(&builder, false, &mut overrides).build()
         };
         validate_config(&config)?;
 
@@ -677,6 +691,7 @@ impl Config {
             let mut writer = self.inner.write().unwrap();
             writer.config = config;
             writer._usr = builder;
+            writer._overrides = overrides;
         }
 
         //Save to file
@@ -690,7 +705,8 @@ impl Config {
     pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
         let builder = {
             let usr = &self.inner.read().unwrap()._usr;
-            usr.merge(&other, false)
+            let mut _overrides = Vec::new();
+            usr.merge(&other, false, &mut _overrides)
         };
         self.update_config(builder)
     }
@@ -751,6 +767,7 @@ impl Config {
             let mut writer = self.inner.write().unwrap();
             writer.config = config;
             writer._usr = usr;
+            writer._overrides = Vec::new();
         }
 
         Ok(())

+ 3437 - 1554
src/static/scripts/bootstrap-native.js

@@ -1,1671 +1,3554 @@
 /*!
-  * Native JavaScript for Bootstrap v3.0.15 (https://thednp.github.io/bootstrap.native/)
+  * Native JavaScript for Bootstrap v4.0.2 (https://thednp.github.io/bootstrap.native/)
   * Copyright 2015-2021 © dnp_theme
   * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
   */
- (function (global, factory) {
+(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
   typeof define === 'function' && define.amd ? define(factory) :
-  (global = global || self, global.BSN = factory());
+  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
 }(this, (function () { 'use strict';
 
-  var transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
+  const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
 
-  var supportTransition = 'webkitTransition' in document.head.style || 'transition' in document.head.style;
+  const supportTransition = 'webkitTransition' in document.head.style || 'transition' in document.head.style;
 
-  var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration';
+  const transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration';
 
-  var transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty';
+  const transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty';
 
   function getElementTransitionDuration(element) {
-    var computedStyle = getComputedStyle(element),
-        property = computedStyle[transitionProperty],
-        duration = supportTransition && property && property !== 'none'
-                 ? parseFloat(computedStyle[transitionDuration]) : 0;
-    return !isNaN(duration) ? duration * 1000 : 0;
+    const computedStyle = getComputedStyle(element);
+    const propertyValue = computedStyle[transitionProperty];
+    const durationValue = computedStyle[transitionDuration];
+    const durationScale = durationValue.includes('ms') ? 1 : 1000;
+    const duration = supportTransition && propertyValue && propertyValue !== 'none'
+      ? parseFloat(durationValue) * durationScale : 0;
+
+    return !Number.isNaN(duration) ? duration : 0;
   }
 
-  function emulateTransitionEnd(element,handler){
-    var called = 0, duration = getElementTransitionDuration(element);
-    duration ? element.addEventListener( transitionEndEvent, function transitionEndWrapper(e){
-                !called && handler(e), called = 1;
-                element.removeEventListener( transitionEndEvent, transitionEndWrapper);
-              })
-             : setTimeout(function() { !called && handler(), called = 1; }, 17);
+  function emulateTransitionEnd(element, handler) {
+    let called = 0;
+    const endEvent = new Event(transitionEndEvent);
+    const duration = getElementTransitionDuration(element);
+
+    if (duration) {
+      element.addEventListener(transitionEndEvent, function transitionEndWrapper(e) {
+        if (e.target === element) {
+          handler.apply(element, [e]);
+          element.removeEventListener(transitionEndEvent, transitionEndWrapper);
+          called = 1;
+        }
+      });
+      setTimeout(() => {
+        if (!called) element.dispatchEvent(endEvent);
+      }, duration + 17);
+    } else {
+      handler.apply(element, [endEvent]);
+    }
   }
 
   function queryElement(selector, parent) {
-    var lookUp = parent && parent instanceof Element ? parent : document;
+    const lookUp = parent && parent instanceof Element ? parent : document;
     return selector instanceof Element ? selector : lookUp.querySelector(selector);
   }
 
-  function bootstrapCustomEvent(eventName, componentName, eventProperties) {
-    var OriginalCustomEvent = new CustomEvent( eventName + '.bs.' + componentName, {cancelable: true});
-    if (typeof eventProperties !== 'undefined') {
-      Object.keys(eventProperties).forEach(function (key) {
+  function hasClass(element, classNAME) {
+    return element.classList.contains(classNAME);
+  }
+
+  function removeClass(element, classNAME) {
+    element.classList.remove(classNAME);
+  }
+
+  const addEventListener = 'addEventListener';
+
+  const removeEventListener = 'removeEventListener';
+
+  const fadeClass = 'fade';
+
+  const showClass = 'show';
+
+  const dataBsDismiss = 'data-bs-dismiss';
+
+  function bootstrapCustomEvent(namespacedEventType, eventProperties) {
+    const OriginalCustomEvent = new CustomEvent(namespacedEventType, { cancelable: true });
+
+    if (eventProperties instanceof Object) {
+      Object.keys(eventProperties).forEach((key) => {
         Object.defineProperty(OriginalCustomEvent, key, {
-          value: eventProperties[key]
+          value: eventProperties[key],
         });
       });
     }
     return OriginalCustomEvent;
   }
 
-  function dispatchCustomEvent(customEvent){
-    this && this.dispatchEvent(customEvent);
-  }
-
-  function Alert(element) {
-    var self = this,
-      alert,
-      closeCustomEvent = bootstrapCustomEvent('close','alert'),
-      closedCustomEvent = bootstrapCustomEvent('closed','alert');
-    function triggerHandler() {
-      alert.classList.contains('fade') ? emulateTransitionEnd(alert,transitionEndHandler) : transitionEndHandler();
+  function normalizeValue(value) {
+    if (value === 'true') {
+      return true;
     }
-    function toggleEvents(action){
-      action = action ? 'addEventListener' : 'removeEventListener';
-      element[action]('click',clickHandler,false);
+
+    if (value === 'false') {
+      return false;
     }
-    function clickHandler(e) {
-      alert = e && e.target.closest(".alert");
-      element = queryElement('[data-dismiss="alert"]',alert);
-      element && alert && (element === e.target || element.contains(e.target)) && self.close();
+
+    if (!Number.isNaN(+value)) {
+      return +value;
     }
-    function transitionEndHandler() {
-      toggleEvents();
-      alert.parentNode.removeChild(alert);
-      dispatchCustomEvent.call(alert,closedCustomEvent);
+
+    if (value === '' || value === 'null') {
+      return null;
     }
-    self.close = function () {
-      if ( alert && element && alert.classList.contains('show') ) {
-        dispatchCustomEvent.call(alert,closeCustomEvent);
-        if ( closeCustomEvent.defaultPrevented ) { return; }
-        self.dispose();
-        alert.classList.remove('show');
-        triggerHandler();
-      }
-    };
-    self.dispose = function () {
-      toggleEvents();
-      delete element.Alert;
-    };
-    element = queryElement(element);
-    alert = element.closest('.alert');
-    element.Alert && element.Alert.dispose();
-    if ( !element.Alert ) {
-      toggleEvents(1);
-    }
-    self.element = element;
-    element.Alert = self;
-  }
-
-  function Button(element) {
-    var self = this, labels,
-        changeCustomEvent = bootstrapCustomEvent('change', 'button');
-    function toggle(e) {
-      var input,
-          label = e.target.tagName === 'LABEL' ? e.target
-                : e.target.closest('LABEL') ? e.target.closest('LABEL') : null;
-      input = label && label.getElementsByTagName('INPUT')[0];
-      if ( !input ) { return; }
-      dispatchCustomEvent.call(input, changeCustomEvent);
-      dispatchCustomEvent.call(element, changeCustomEvent);
-      if ( input.type === 'checkbox' ) {
-        if ( changeCustomEvent.defaultPrevented ) { return; }
-        if ( !input.checked ) {
-          label.classList.add('active');
-          input.getAttribute('checked');
-          input.setAttribute('checked','checked');
-          input.checked = true;
+
+    // string / function / Element / Object
+    return value;
+  }
+
+  function normalizeOptions(element, defaultOps, inputOps, ns) {
+    const normalOps = {};
+    const dataOps = {};
+    const data = { ...element.dataset };
+
+    Object.keys(data)
+      .forEach((k) => {
+        const key = k.includes(ns)
+          ? k.replace(ns, '').replace(/[A-Z]/, (match) => match.toLowerCase())
+          : k;
+
+        dataOps[key] = normalizeValue(data[k]);
+      });
+
+    Object.keys(inputOps)
+      .forEach((k) => {
+        inputOps[k] = normalizeValue(inputOps[k]);
+      });
+
+    Object.keys(defaultOps)
+      .forEach((k) => {
+        if (k in inputOps) {
+          normalOps[k] = inputOps[k];
+        } else if (k in dataOps) {
+          normalOps[k] = dataOps[k];
         } else {
-          label.classList.remove('active');
-          input.getAttribute('checked');
-          input.removeAttribute('checked');
-          input.checked = false;
-        }
-        if (!element.toggled) {
-          element.toggled = true;
+          normalOps[k] = defaultOps[k];
         }
+      });
+
+    return normalOps;
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Base Component
+  ----------------------------------------------------- */
+
+  class BaseComponent {
+    constructor(name, target, defaults, config) {
+      const self = this;
+      const element = queryElement(target);
+
+      if (element[name]) element[name].dispose();
+      self.element = element;
+
+      if (defaults && Object.keys(defaults).length) {
+        self.options = normalizeOptions(element, defaults, (config || {}), 'bs');
       }
-      if ( input.type === 'radio' && !element.toggled ) {
-        if ( changeCustomEvent.defaultPrevented ) { return; }
-        if ( !input.checked || (e.screenX === 0 && e.screenY == 0) ) {
-          label.classList.add('active');
-          label.classList.add('focus');
-          input.setAttribute('checked','checked');
-          input.checked = true;
-          element.toggled = true;
-          Array.from(labels).map(function (otherLabel){
-            var otherInput = otherLabel.getElementsByTagName('INPUT')[0];
-            if ( otherLabel !== label && otherLabel.classList.contains('active') )  {
-              dispatchCustomEvent.call(otherInput, changeCustomEvent);
-              otherLabel.classList.remove('active');
-              otherInput.removeAttribute('checked');
-              otherInput.checked = false;
-            }
-          });
+      element[name] = self;
+    }
+
+    dispose(name) {
+      const self = this;
+      self.element[name] = null;
+      Object.keys(self).forEach((prop) => { self[prop] = null; });
+    }
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Alert
+  -------------------------------------------- */
+
+  // ALERT PRIVATE GC
+  // ================
+  const alertString = 'alert';
+  const alertComponent = 'Alert';
+  const alertSelector = `.${alertString}`;
+  const alertDismissSelector = `[${dataBsDismiss}="${alertString}"]`;
+
+  // ALERT CUSTOM EVENTS
+  // ===================
+  const closeAlertEvent = bootstrapCustomEvent(`close.bs.${alertString}`);
+  const closedAlertEvent = bootstrapCustomEvent(`closed.bs.${alertString}`);
+
+  // ALERT EVENT HANDLERS
+  // ====================
+  function alertTransitionEnd(self) {
+    const { element, relatedTarget } = self;
+    toggleAlertHandler(self);
+
+    if (relatedTarget) closedAlertEvent.relatedTarget = relatedTarget;
+    element.dispatchEvent(closedAlertEvent);
+
+    self.dispose();
+    element.parentNode.removeChild(element);
+  }
+
+  // ALERT PRIVATE METHOD
+  // ====================
+  function toggleAlertHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    if (self.dismiss) self.dismiss[action]('click', self.close);
+  }
+
+  // ALERT DEFINITION
+  // ================
+  class Alert extends BaseComponent {
+    constructor(target) {
+      super(alertComponent, target);
+      // bind
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+
+      // the dismiss button
+      self.dismiss = queryElement(alertDismissSelector, element);
+      self.relatedTarget = null;
+
+      // add event listener
+      toggleAlertHandler(self, 1);
+    }
+
+    // ALERT PUBLIC METHODS
+    // ====================
+    close(e) {
+      const target = e ? e.target : null;
+      const self = e
+        ? e.target.closest(alertSelector)[alertComponent]
+        : this;
+      const { element } = self;
+
+      if (self && element && hasClass(element, showClass)) {
+        if (target) {
+          closeAlertEvent.relatedTarget = target;
+          self.relatedTarget = target;
         }
+        element.dispatchEvent(closeAlertEvent);
+        if (closeAlertEvent.defaultPrevented) return;
+
+        removeClass(element, showClass);
+
+        if (hasClass(element, fadeClass)) {
+          emulateTransitionEnd(element, () => alertTransitionEnd(self));
+        } else alertTransitionEnd(self);
       }
-      setTimeout( function () { element.toggled = false; }, 50 );
     }
-    function keyHandler(e) {
-      var key = e.which || e.keyCode;
-      key === 32 && e.target === document.activeElement && toggle(e);
+
+    dispose() {
+      toggleAlertHandler(this);
+      super.dispose(alertComponent);
     }
-    function preventScroll(e) {
-      var key = e.which || e.keyCode;
-      key === 32 && e.preventDefault();
+  }
+
+  Alert.init = {
+    component: alertComponent,
+    selector: alertSelector,
+    constructor: Alert,
+  };
+
+  function addClass(element, classNAME) {
+    element.classList.add(classNAME);
+  }
+
+  const activeClass = 'active';
+
+  const dataBsToggle = 'data-bs-toggle';
+
+  /* Native JavaScript for Bootstrap 5 | Button
+  ---------------------------------------------*/
+
+  // BUTTON PRIVATE GC
+  // =================
+  const buttonString = 'button';
+  const buttonComponent = 'Button';
+  const buttonSelector = `[${dataBsToggle}="${buttonString}"]`;
+  const ariaPressed = 'aria-pressed';
+
+  // BUTTON PRIVATE METHOD
+  // =====================
+  function toggleButtonHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    self.element[action]('click', self.toggle);
+  }
+
+  // BUTTON DEFINITION
+  // =================
+  class Button extends BaseComponent {
+    constructor(target) {
+      super(buttonComponent, target);
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+
+      // set initial state
+      self.isActive = hasClass(element, activeClass);
+      element.setAttribute(ariaPressed, !!self.isActive);
+
+      // add event listener
+      toggleButtonHandler(self, 1);
     }
-    function focusToggle(e) {
-      if (e.target.tagName === 'INPUT' ) {
-        var action = e.type === 'focusin' ? 'add' : 'remove';
-        e.target.closest('.btn').classList[action]('focus');
-      }
+
+    // BUTTON PUBLIC METHODS
+    // =====================
+    toggle(e) {
+      if (e) e.preventDefault();
+      const self = e ? this[buttonComponent] : this;
+      const { element } = self;
+
+      if (hasClass(element, 'disabled')) return;
+
+      self.isActive = hasClass(element, activeClass);
+      const { isActive } = self;
+
+      const action = isActive ? removeClass : addClass;
+      const ariaValue = isActive ? 'false' : 'true';
+
+      action(element, activeClass);
+      element.setAttribute(ariaPressed, ariaValue);
     }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      element[action]('click',toggle,false );
-      element[action]('keyup',keyHandler,false), element[action]('keydown',preventScroll,false);
-      element[action]('focusin',focusToggle,false), element[action]('focusout',focusToggle,false);
+
+    dispose() {
+      toggleButtonHandler(this);
+      super.dispose(buttonComponent);
     }
-    self.dispose = function () {
-      toggleEvents();
-      delete element.Button;
-    };
-    element = queryElement(element);
-    element.Button && element.Button.dispose();
-    labels = element.getElementsByClassName('btn');
-    if (!labels.length) { return; }
-    if ( !element.Button ) {
-      toggleEvents(1);
-    }
-    element.toggled = false;
-    element.Button = self;
-    Array.from(labels).map(function (btn){
-      !btn.classList.contains('active')
-        && queryElement('input:checked',btn)
-        && btn.classList.add('active');
-      btn.classList.contains('active')
-        && !queryElement('input:checked',btn)
-        && btn.classList.remove('active');
-    });
   }
 
-  var mouseHoverEvents = ('onmouseleave' in document) ? [ 'mouseenter', 'mouseleave'] : [ 'mouseover', 'mouseout' ];
+  Button.init = {
+    component: buttonComponent,
+    selector: buttonSelector,
+    constructor: Button,
+  };
 
-  var supportPassive = (function () {
-    var result = false;
+  const supportPassive = (() => {
+    let result = false;
     try {
-      var opts = Object.defineProperty({}, 'passive', {
-        get: function() {
+      const opts = Object.defineProperty({}, 'passive', {
+        get() {
           result = true;
-        }
+          return result;
+        },
       });
-      document.addEventListener('DOMContentLoaded', function wrap(){
-        document.removeEventListener('DOMContentLoaded', wrap, opts);
+      document[addEventListener]('DOMContentLoaded', function wrap() {
+        document[removeEventListener]('DOMContentLoaded', wrap, opts);
       }, opts);
-    } catch (e) {}
+    } catch (e) {
+      throw Error('Passive events are not supported');
+    }
+
     return result;
   })();
 
+  // general event options
+
   var passiveHandler = supportPassive ? { passive: true } : false;
 
+  function reflow(element) {
+    return element.offsetHeight;
+  }
+
   function isElementInScrollRange(element) {
-    var bcr = element.getBoundingClientRect(),
-        viewportHeight = window.innerHeight || document.documentElement.clientHeight;
-    return bcr.top <= viewportHeight && bcr.bottom >= 0;
-  }
-
-  function Carousel (element,options) {
-    options = options || {};
-    var self = this,
-      vars, ops,
-      slideCustomEvent, slidCustomEvent,
-      slides, leftArrow, rightArrow, indicator, indicators;
-    function pauseHandler() {
-      if ( ops.interval !==false && !element.classList.contains('paused') ) {
-        element.classList.add('paused');
-        !vars.isSliding && ( clearInterval(vars.timer), vars.timer = null );
+    const bcr = element.getBoundingClientRect();
+    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
+    return bcr.top <= viewportHeight && bcr.bottom >= 0; // bottom && top
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Carousel
+  ----------------------------------------------- */
+
+  // CAROUSEL PRIVATE GC
+  // ===================
+  const carouselString = 'carousel';
+  const carouselComponent = 'Carousel';
+  const carouselSelector = `[data-bs-ride="${carouselString}"]`;
+  const carouselControl = `${carouselString}-control`;
+  const carouselItem = `${carouselString}-item`;
+  const dataBsSlideTo = 'data-bs-slide-to';
+  const pausedClass = 'paused';
+  const defaultCarouselOptions = {
+    pause: 'hover', // 'boolean|string'
+    keyboard: false, // 'boolean'
+    touch: true, // 'boolean'
+    interval: 5000, // 'boolean|number'
+  };
+  let startX = 0;
+  let currentX = 0;
+  let endX = 0;
+
+  // CAROUSEL CUSTOM EVENTS
+  // ======================
+  const carouselSlideEvent = bootstrapCustomEvent(`slide.bs.${carouselString}`);
+  const carouselSlidEvent = bootstrapCustomEvent(`slid.bs.${carouselString}`);
+
+  // CAROUSEL EVENT HANDLERS
+  // =======================
+  function carouselTransitionEndHandler(self) {
+    const {
+      index, direction, element, slides, options, isAnimating,
+    } = self;
+
+    // discontinue disposed instances
+    if (isAnimating && element[carouselComponent]) {
+      const activeItem = getActiveIndex(self);
+      const orientation = direction === 'left' ? 'next' : 'prev';
+      const directionClass = direction === 'left' ? 'start' : 'end';
+      self.isAnimating = false;
+
+      addClass(slides[index], activeClass);
+      removeClass(slides[activeItem], activeClass);
+
+      removeClass(slides[index], `${carouselItem}-${orientation}`);
+      removeClass(slides[index], `${carouselItem}-${directionClass}`);
+      removeClass(slides[activeItem], `${carouselItem}-${directionClass}`);
+
+      element.dispatchEvent(carouselSlidEvent);
+
+      // check for element, might have been disposed
+      if (!document.hidden && options.interval
+        && !hasClass(element, pausedClass)) {
+        self.cycle();
       }
     }
-    function resumeHandler() {
-      if ( ops.interval !== false && element.classList.contains('paused') ) {
-        element.classList.remove('paused');
-        !vars.isSliding && ( clearInterval(vars.timer), vars.timer = null );
-        !vars.isSliding && self.cycle();
+  }
+
+  function carouselPauseHandler(e) {
+    const eventTarget = e.target;
+    const self = eventTarget.closest(carouselSelector)[carouselComponent];
+    const { element, isAnimating } = self;
+
+    if (!hasClass(element, pausedClass)) {
+      addClass(element, pausedClass);
+      if (!isAnimating) {
+        clearInterval(self.timer);
+        self.timer = null;
       }
     }
-    function indicatorHandler(e) {
-      e.preventDefault();
-      if (vars.isSliding) { return; }
-      var eventTarget = e.target;
-      if ( eventTarget && !eventTarget.classList.contains('active') && eventTarget.getAttribute('data-slide-to') ) {
-        vars.index = parseInt( eventTarget.getAttribute('data-slide-to'));
-      } else { return false; }
-      self.slideTo( vars.index );
-    }
-    function controlsHandler(e) {
-      e.preventDefault();
-      if (vars.isSliding) { return; }
-      var eventTarget = e.currentTarget || e.srcElement;
-      if ( eventTarget === rightArrow ) {
-        vars.index++;
-      } else if ( eventTarget === leftArrow ) {
-        vars.index--;
-      }
-      self.slideTo( vars.index );
-    }
-    function keyHandler(ref) {
-      var which = ref.which;
-      if (vars.isSliding) { return; }
-      switch (which) {
-        case 39:
-          vars.index++;
-          break;
-        case 37:
-          vars.index--;
-          break;
-        default: return;
-      }
-      self.slideTo( vars.index );
-    }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      if ( ops.pause && ops.interval ) {
-        element[action]( mouseHoverEvents[0], pauseHandler, false );
-        element[action]( mouseHoverEvents[1], resumeHandler, false );
-        element[action]( 'touchstart', pauseHandler, passiveHandler );
-        element[action]( 'touchend', resumeHandler, passiveHandler );
-      }
-      ops.touch && slides.length > 1 && element[action]( 'touchstart', touchDownHandler, passiveHandler );
-      rightArrow && rightArrow[action]( 'click', controlsHandler,false );
-      leftArrow && leftArrow[action]( 'click', controlsHandler,false );
-      indicator && indicator[action]( 'click', indicatorHandler,false );
-      ops.keyboard && window[action]( 'keydown', keyHandler,false );
-    }
-    function toggleTouchEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      element[action]( 'touchmove', touchMoveHandler, passiveHandler );
-      element[action]( 'touchend', touchEndHandler, passiveHandler );
-    }
-    function touchDownHandler(e) {
-      if ( vars.isTouch ) { return; }
-      vars.touchPosition.startX = e.changedTouches[0].pageX;
-      if ( element.contains(e.target) ) {
-        vars.isTouch = true;
-        toggleTouchEvents(1);
+  }
+
+  function carouselResumeHandler(e) {
+    const eventTarget = e.target;
+    const self = eventTarget.closest(carouselSelector)[carouselComponent];
+    const { isPaused, isAnimating, element } = self;
+
+    if (!isPaused && hasClass(element, pausedClass)) {
+      removeClass(element, pausedClass);
+
+      if (!isAnimating) {
+        clearInterval(self.timer);
+        self.timer = null;
+        self.cycle();
       }
     }
-    function touchMoveHandler(e) {
-      if ( !vars.isTouch ) { e.preventDefault(); return; }
-      vars.touchPosition.currentX = e.changedTouches[0].pageX;
-      if ( e.type === 'touchmove' && e.changedTouches.length > 1 ) {
-        e.preventDefault();
-        return false;
-      }
+  }
+
+  function carouselIndicatorHandler(e) {
+    e.preventDefault();
+    const { target } = e;
+    const self = target.closest(carouselSelector)[carouselComponent];
+
+    if (self.isAnimating) return;
+
+    const newIndex = target.getAttribute(dataBsSlideTo);
+
+    if (target && !hasClass(target, activeClass) // event target is not active
+      && newIndex) { // AND has the specific attribute
+      self.to(+newIndex); // do the slide
     }
-    function touchEndHandler (e) {
-      if ( !vars.isTouch || vars.isSliding ) { return }
-      vars.touchPosition.endX = vars.touchPosition.currentX || e.changedTouches[0].pageX;
-      if ( vars.isTouch ) {
-        if ( (!element.contains(e.target) || !element.contains(e.relatedTarget) )
-            && Math.abs(vars.touchPosition.startX - vars.touchPosition.endX) < 75 ) {
-          return false;
-        } else {
-          if ( vars.touchPosition.currentX < vars.touchPosition.startX ) {
-            vars.index++;
-          } else if ( vars.touchPosition.currentX > vars.touchPosition.startX ) {
-            vars.index--;
-          }
-          vars.isTouch = false;
-          self.slideTo(vars.index);
-        }
-        toggleTouchEvents();
-      }
+  }
+
+  function carouselControlsHandler(e) {
+    e.preventDefault();
+    const that = this;
+    const self = that.closest(carouselSelector)[carouselComponent];
+    const { controls } = self;
+
+    if (controls[1] && that === controls[1]) {
+      self.next();
+    } else if (controls[1] && that === controls[0]) {
+      self.prev();
     }
-    function setActivePage(pageIndex) {
-      Array.from(indicators).map(function (x){x.classList.remove('active');});
-      indicators[pageIndex] && indicators[pageIndex].classList.add('active');
-    }
-    function transitionEndHandler(e){
-      if (vars.touchPosition){
-        var next = vars.index,
-            timeout = e && e.target !== slides[next] ? e.elapsedTime*1000+100 : 20,
-            activeItem = self.getActiveIndex(),
-            orientation = vars.direction === 'left' ? 'next' : 'prev';
-        vars.isSliding && setTimeout(function () {
-          if (vars.touchPosition){
-            vars.isSliding = false;
-            slides[next].classList.add('active');
-            slides[activeItem].classList.remove('active');
-            slides[next].classList.remove(("carousel-item-" + orientation));
-            slides[next].classList.remove(("carousel-item-" + (vars.direction)));
-            slides[activeItem].classList.remove(("carousel-item-" + (vars.direction)));
-            dispatchCustomEvent.call(element, slidCustomEvent);
-            if ( !document.hidden && ops.interval && !element.classList.contains('paused') ) {
-              self.cycle();
-            }
-          }
-        }, timeout);
-      }
+  }
+
+  function carouselKeyHandler({ which }) {
+    const [element] = Array.from(document.querySelectorAll(carouselSelector))
+      .filter((x) => isElementInScrollRange(x));
+
+    if (!element) return;
+    const self = element[carouselComponent];
+
+    switch (which) {
+      case 39:
+        self.next();
+        break;
+      case 37:
+        self.prev();
+        break;
     }
-    self.cycle = function () {
-      if (vars.timer) {
-        clearInterval(vars.timer);
-        vars.timer = null;
-      }
-      vars.timer = setInterval(function () {
-        var idx = vars.index || self.getActiveIndex();
-        isElementInScrollRange(element) && (idx++, self.slideTo( idx ) );
-      }, ops.interval);
-    };
-    self.slideTo = function (next) {
-      if (vars.isSliding) { return; }
-      var activeItem = self.getActiveIndex(), orientation, eventProperties;
-      if ( activeItem === next ) {
-        return;
-      } else if  ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) {
-        vars.direction = 'left';
-      } else if  ( (activeItem > next) || (activeItem === slides.length - 1 && next === 0 ) ) {
-        vars.direction = 'right';
-      }
-      if ( next < 0 ) { next = slides.length - 1; }
-      else if ( next >= slides.length ){ next = 0; }
-      orientation = vars.direction === 'left' ? 'next' : 'prev';
-      eventProperties = { relatedTarget: slides[next], direction: vars.direction, from: activeItem, to: next };
-      slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', eventProperties);
-      slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', eventProperties);
-      dispatchCustomEvent.call(element, slideCustomEvent);
-      if (slideCustomEvent.defaultPrevented) { return; }
-      vars.index = next;
-      vars.isSliding = true;
-      clearInterval(vars.timer);
-      vars.timer = null;
-      setActivePage( next );
-      if ( getElementTransitionDuration(slides[next]) && element.classList.contains('slide') ) {
-        slides[next].classList.add(("carousel-item-" + orientation));
-        slides[next].offsetWidth;
-        slides[next].classList.add(("carousel-item-" + (vars.direction)));
-        slides[activeItem].classList.add(("carousel-item-" + (vars.direction)));
-        emulateTransitionEnd(slides[next], transitionEndHandler);
-      } else {
-        slides[next].classList.add('active');
-        slides[next].offsetWidth;
-        slides[activeItem].classList.remove('active');
-        setTimeout(function () {
-          vars.isSliding = false;
-          if ( ops.interval && element && !element.classList.contains('paused') ) {
-            self.cycle();
-          }
-          dispatchCustomEvent.call(element, slidCustomEvent);
-        }, 100 );
-      }
-    };
-    self.getActiveIndex = function () { return Array.from(slides).indexOf(element.getElementsByClassName('carousel-item active')[0]) || 0; };
-    self.dispose = function () {
-      var itemClasses = ['left','right','prev','next'];
-      Array.from(slides).map(function (slide,idx) {
-        slide.classList.contains('active') && setActivePage( idx );
-        itemClasses.map(function (cls) { return slide.classList.remove(("carousel-item-" + cls)); });
-      });
-      clearInterval(vars.timer);
-      toggleEvents();
-      vars = {};
-      ops = {};
-      delete element.Carousel;
-    };
-    element = queryElement( element );
-    element.Carousel && element.Carousel.dispose();
-    slides = element.getElementsByClassName('carousel-item');
-    leftArrow = element.getElementsByClassName('carousel-control-prev')[0];
-    rightArrow = element.getElementsByClassName('carousel-control-next')[0];
-    indicator = element.getElementsByClassName('carousel-indicators')[0];
-    indicators = indicator && indicator.getElementsByTagName( "LI" ) || [];
-    if (slides.length < 2) { return }
-    var
-      intervalAttribute = element.getAttribute('data-interval'),
-      intervalData = intervalAttribute === 'false' ? 0 : parseInt(intervalAttribute),
-      touchData = element.getAttribute('data-touch') === 'false' ? 0 : 1,
-      pauseData = element.getAttribute('data-pause') === 'hover' || false,
-      keyboardData = element.getAttribute('data-keyboard') === 'true' || false,
-      intervalOption = options.interval,
-      touchOption = options.touch;
-    ops = {};
-    ops.keyboard = options.keyboard === true || keyboardData;
-    ops.pause = (options.pause === 'hover' || pauseData) ? 'hover' : false;
-    ops.touch = touchOption || touchData;
-    ops.interval = typeof intervalOption === 'number' ? intervalOption
-                : intervalOption === false || intervalData === 0 || intervalData === false ? 0
-                : isNaN(intervalData) ? 5000
-                : intervalData;
-    if (self.getActiveIndex()<0) {
-      slides.length && slides[0].classList.add('active');
-      indicators.length && setActivePage(0);
-    }
-    vars = {};
-    vars.direction = 'left';
-    vars.index = 0;
-    vars.timer = null;
-    vars.isSliding = false;
-    vars.isTouch = false;
-    vars.touchPosition = {
-      startX : 0,
-      currentX : 0,
-      endX : 0
-    };
-    toggleEvents(1);
-    if ( ops.interval ){ self.cycle(); }
-    element.Carousel = self;
-  }
-
-  function Collapse(element,options) {
-    options = options || {};
-    var self = this;
-    var accordion = null,
-        collapse = null,
-        activeCollapse,
-        activeElement,
-        showCustomEvent,
-        shownCustomEvent,
-        hideCustomEvent,
-        hiddenCustomEvent;
-    function openAction(collapseElement, toggle) {
-      dispatchCustomEvent.call(collapseElement, showCustomEvent);
-      if ( showCustomEvent.defaultPrevented ) { return; }
-      collapseElement.isAnimating = true;
-      collapseElement.classList.add('collapsing');
-      collapseElement.classList.remove('collapse');
-      collapseElement.style.height = (collapseElement.scrollHeight) + "px";
-      emulateTransitionEnd(collapseElement, function () {
-        collapseElement.isAnimating = false;
-        collapseElement.setAttribute('aria-expanded','true');
-        toggle.setAttribute('aria-expanded','true');
-        collapseElement.classList.remove('collapsing');
-        collapseElement.classList.add('collapse');
-        collapseElement.classList.add('show');
-        collapseElement.style.height = '';
-        dispatchCustomEvent.call(collapseElement, shownCustomEvent);
-      });
+  }
+
+  // CAROUSEL TOUCH HANDLERS
+  // =======================
+  function carouselTouchDownHandler(e) {
+    const element = this;
+    const self = element[carouselComponent];
+
+    if (!self || self.isTouch) { return; }
+
+    startX = e.changedTouches[0].pageX;
+
+    if (element.contains(e.target)) {
+      self.isTouch = true;
+      toggleCarouselTouchHandlers(self, 1);
     }
-    function closeAction(collapseElement, toggle) {
-      dispatchCustomEvent.call(collapseElement, hideCustomEvent);
-      if ( hideCustomEvent.defaultPrevented ) { return; }
-      collapseElement.isAnimating = true;
-      collapseElement.style.height = (collapseElement.scrollHeight) + "px";
-      collapseElement.classList.remove('collapse');
-      collapseElement.classList.remove('show');
-      collapseElement.classList.add('collapsing');
-      collapseElement.offsetWidth;
-      collapseElement.style.height = '0px';
-      emulateTransitionEnd(collapseElement, function () {
-        collapseElement.isAnimating = false;
-        collapseElement.setAttribute('aria-expanded','false');
-        toggle.setAttribute('aria-expanded','false');
-        collapseElement.classList.remove('collapsing');
-        collapseElement.classList.add('collapse');
-        collapseElement.style.height = '';
-        dispatchCustomEvent.call(collapseElement, hiddenCustomEvent);
-      });
+  }
+
+  function carouselTouchMoveHandler(e) {
+    const { changedTouches, type } = e;
+    const self = this[carouselComponent];
+
+    if (!self || !self.isTouch) { return; }
+
+    currentX = changedTouches[0].pageX;
+
+    // cancel touch if more than one changedTouches detected
+    if (type === 'touchmove' && changedTouches.length > 1) {
+      e.preventDefault();
     }
-    self.toggle = function (e) {
-      if (e && e.target.tagName === 'A' || element.tagName === 'A') {e.preventDefault();}
-      if (element.contains(e.target) || e.target === element) {
-        if (!collapse.classList.contains('show')) { self.show(); }
-        else { self.hide(); }
-      }
-    };
-    self.hide = function () {
-      if ( collapse.isAnimating ) { return; }
-      closeAction(collapse,element);
-      element.classList.add('collapsed');
-    };
-    self.show = function () {
-      if ( accordion ) {
-        activeCollapse = accordion.getElementsByClassName("collapse show")[0];
-        activeElement = activeCollapse && (queryElement(("[data-target=\"#" + (activeCollapse.id) + "\"]"),accordion)
-                      || queryElement(("[href=\"#" + (activeCollapse.id) + "\"]"),accordion) );
-      }
-      if ( !collapse.isAnimating ) {
-        if ( activeElement && activeCollapse !== collapse ) {
-          closeAction(activeCollapse,activeElement);
-          activeElement.classList.add('collapsed');
-        }
-        openAction(collapse,element);
-        element.classList.remove('collapsed');
-      }
-    };
-    self.dispose = function () {
-      element.removeEventListener('click',self.toggle,false);
-      delete element.Collapse;
-    };
-      element = queryElement(element);
-      element.Collapse && element.Collapse.dispose();
-      var accordionData = element.getAttribute('data-parent');
-      showCustomEvent = bootstrapCustomEvent('show', 'collapse');
-      shownCustomEvent = bootstrapCustomEvent('shown', 'collapse');
-      hideCustomEvent = bootstrapCustomEvent('hide', 'collapse');
-      hiddenCustomEvent = bootstrapCustomEvent('hidden', 'collapse');
-      collapse = queryElement(options.target || element.getAttribute('data-target') || element.getAttribute('href'));
-      collapse.isAnimating = false;
-      accordion = element.closest(options.parent || accordionData);
-      if ( !element.Collapse ) {
-        element.addEventListener('click',self.toggle,false);
-      }
-      element.Collapse = self;
-  }
-
-  function setFocus (element){
-    element.focus ? element.focus() : element.setActive();
-  }
-
-  function Dropdown(element,option) {
-    var self = this,
-        showCustomEvent,
-        shownCustomEvent,
-        hideCustomEvent,
-        hiddenCustomEvent,
-        relatedTarget = null,
-        parent, menu, menuItems = [],
-        persist;
-    function preventEmptyAnchor(anchor) {
-      (anchor.href && anchor.href.slice(-1) === '#' || anchor.parentNode && anchor.parentNode.href
-        && anchor.parentNode.href.slice(-1) === '#') && this.preventDefault();
-    }
-    function toggleDismiss() {
-      var action = element.open ? 'addEventListener' : 'removeEventListener';
-      document[action]('click',dismissHandler,false);
-      document[action]('keydown',preventScroll,false);
-      document[action]('keyup',keyHandler,false);
-      document[action]('focus',dismissHandler,false);
-    }
-    function dismissHandler(e) {
-      var eventTarget = e.target,
-            hasData = eventTarget && (eventTarget.getAttribute('data-toggle')
-                                  || eventTarget.parentNode && eventTarget.parentNode.getAttribute
-                                  && eventTarget.parentNode.getAttribute('data-toggle'));
-      if ( e.type === 'focus' && (eventTarget === element || eventTarget === menu || menu.contains(eventTarget) ) ) {
+  }
+
+  function carouselTouchEndHandler(e) {
+    const element = this;
+    const self = element[carouselComponent];
+
+    if (!self || !self.isTouch) { return; }
+
+    endX = currentX || e.changedTouches[0].pageX;
+
+    if (self.isTouch) {
+      // the event target is outside the carousel OR carousel doens't include the related target
+      if ((!element.contains(e.target) || !element.contains(e.relatedTarget))
+        && Math.abs(startX - endX) < 75) { // AND swipe distance is less than 75px
+        // when the above conditions are satisfied, no need to continue
         return;
+      } // OR determine next index to slide to
+      if (currentX < startX) {
+        self.index += 1;
+      } else if (currentX > startX) {
+        self.index -= 1;
       }
-      if ( (eventTarget === menu || menu.contains(eventTarget)) && (persist || hasData) ) { return; }
-      else {
-        relatedTarget = eventTarget === element || element.contains(eventTarget) ? element : null;
-        self.hide();
-      }
-      preventEmptyAnchor.call(e,eventTarget);
-    }
-    function clickHandler(e) {
-      relatedTarget = element;
-      self.show();
-      preventEmptyAnchor.call(e,e.target);
-    }
-    function preventScroll(e) {
-      var key = e.which || e.keyCode;
-      if( key === 38 || key === 40 ) { e.preventDefault(); }
-    }
-    function keyHandler(e) {
-      var key = e.which || e.keyCode,
-          activeItem = document.activeElement,
-          isSameElement = activeItem === element,
-          isInsideMenu = menu.contains(activeItem),
-          isMenuItem = activeItem.parentNode === menu || activeItem.parentNode.parentNode === menu,
-          idx = menuItems.indexOf(activeItem);
-      if ( isMenuItem ) {
-        idx = isSameElement ? 0
-                            : key === 38 ? (idx>1?idx-1:0)
-                            : key === 40 ? (idx<menuItems.length-1?idx+1:idx) : idx;
-        menuItems[idx] && setFocus(menuItems[idx]);
-      }
-      if ( (menuItems.length && isMenuItem
-            || !menuItems.length && (isInsideMenu || isSameElement)
-            || !isInsideMenu )
-            && element.open && key === 27
-      ) {
-        self.toggle();
-        relatedTarget = null;
-      }
+
+      self.isTouch = false;
+      self.to(self.index); // do the slide
+
+      toggleCarouselTouchHandlers(self); // remove touch events handlers
     }
-    self.show = function () {
-      showCustomEvent = bootstrapCustomEvent('show', 'dropdown', { relatedTarget: relatedTarget });
-      dispatchCustomEvent.call(parent, showCustomEvent);
-      if ( showCustomEvent.defaultPrevented ) { return; }
-      menu.classList.add('show');
-      parent.classList.add('show');
-      element.setAttribute('aria-expanded',true);
-      element.open = true;
-      element.removeEventListener('click',clickHandler,false);
-      setTimeout(function () {
-        setFocus( menu.getElementsByTagName('INPUT')[0] || element );
-        toggleDismiss();
-        shownCustomEvent = bootstrapCustomEvent('shown', 'dropdown', { relatedTarget: relatedTarget });
-        dispatchCustomEvent.call(parent, shownCustomEvent);
-      },1);
-    };
-    self.hide = function () {
-      hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', { relatedTarget: relatedTarget });
-      dispatchCustomEvent.call(parent, hideCustomEvent);
-      if ( hideCustomEvent.defaultPrevented ) { return; }
-      menu.classList.remove('show');
-      parent.classList.remove('show');
-      element.setAttribute('aria-expanded',false);
-      element.open = false;
-      toggleDismiss();
-      setFocus(element);
-      setTimeout(function () {
-        element.Dropdown && element.addEventListener('click',clickHandler,false);
-      },1);
-      hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', { relatedTarget: relatedTarget });
-      dispatchCustomEvent.call(parent, hiddenCustomEvent);
-    };
-    self.toggle = function () {
-      if (parent.classList.contains('show') && element.open) { self.hide(); }
-      else { self.show(); }
-    };
-    self.dispose = function () {
-      if (parent.classList.contains('show') && element.open) { self.hide(); }
-      element.removeEventListener('click',clickHandler,false);
-      delete element.Dropdown;
-    };
-    element = queryElement(element);
-    element.Dropdown && element.Dropdown.dispose();
-    parent = element.parentNode;
-    menu = queryElement('.dropdown-menu', parent);
-    Array.from(menu.children).map(function (child){
-      child.children.length && (child.children[0].tagName === 'A' && menuItems.push(child.children[0]));
-      child.tagName === 'A' && menuItems.push(child);
-    });
-    if ( !element.Dropdown ) {
-      !('tabindex' in menu) && menu.setAttribute('tabindex', '0');
-      element.addEventListener('click',clickHandler,false);
-    }
-    persist = option === true || element.getAttribute('data-persist') === 'true' || false;
-    element.open = false;
-    element.Dropdown = self;
-  }
-
-  function Modal(element,options) {
-    options = options || {};
-    var self = this, modal,
-      showCustomEvent,
-      shownCustomEvent,
-      hideCustomEvent,
-      hiddenCustomEvent,
-      relatedTarget = null,
-      scrollBarWidth,
-      overlay,
-      overlayDelay,
-      fixedItems,
-      ops = {};
-    function setScrollbar() {
-      var openModal = document.body.classList.contains('modal-open'),
-          bodyPad = parseInt(getComputedStyle(document.body).paddingRight),
-          bodyOverflow = document.documentElement.clientHeight !== document.documentElement.scrollHeight
-                      || document.body.clientHeight !== document.body.scrollHeight,
-          modalOverflow = modal.clientHeight !== modal.scrollHeight;
-      scrollBarWidth = measureScrollbar();
-      modal.style.paddingRight = !modalOverflow && scrollBarWidth ? (scrollBarWidth + "px") : '';
-      document.body.style.paddingRight = modalOverflow || bodyOverflow ? ((bodyPad + (openModal ? 0:scrollBarWidth)) + "px") : '';
-      fixedItems.length && fixedItems.map(function (fixed){
-        var itemPad = getComputedStyle(fixed).paddingRight;
-        fixed.style.paddingRight = modalOverflow || bodyOverflow ? ((parseInt(itemPad) + (openModal?0:scrollBarWidth)) + "px") : ((parseInt(itemPad)) + "px");
-      });
+  }
+
+  // CAROUSEL PRIVATE METHODS
+  // ========================
+  function activateCarouselIndicator(self, pageIndex) { // indicators
+    const { indicators } = self;
+    Array.from(indicators).forEach((x) => removeClass(x, activeClass));
+    if (self.indicators[pageIndex]) addClass(indicators[pageIndex], activeClass);
+  }
+
+  function toggleCarouselTouchHandlers(self, add) {
+    const { element } = self;
+    const action = add ? addEventListener : removeEventListener;
+    element[action]('touchmove', carouselTouchMoveHandler, passiveHandler);
+    element[action]('touchend', carouselTouchEndHandler, passiveHandler);
+  }
+
+  function toggleCarouselHandlers(self, add) {
+    const {
+      element, options, slides, controls, indicator,
+    } = self;
+    const {
+      touch, pause, interval, keyboard,
+    } = options;
+    const action = add ? addEventListener : removeEventListener;
+
+    if (pause && interval) {
+      element[action]('mouseenter', carouselPauseHandler);
+      element[action]('mouseleave', carouselResumeHandler);
+      element[action]('touchstart', carouselPauseHandler, passiveHandler);
+      element[action]('touchend', carouselResumeHandler, passiveHandler);
     }
-    function resetScrollbar() {
-      document.body.style.paddingRight = '';
-      modal.style.paddingRight = '';
-      fixedItems.length && fixedItems.map(function (fixed){
-        fixed.style.paddingRight = '';
-      });
+
+    if (touch && slides.length > 1) {
+      element[action]('touchstart', carouselTouchDownHandler, passiveHandler);
     }
-    function measureScrollbar() {
-      var scrollDiv = document.createElement('div'), widthValue;
-      scrollDiv.className = 'modal-scrollbar-measure';
-      document.body.appendChild(scrollDiv);
-      widthValue = scrollDiv.offsetWidth - scrollDiv.clientWidth;
-      document.body.removeChild(scrollDiv);
-      return widthValue;
-    }
-    function createOverlay() {
-      var newOverlay = document.createElement('div');
-      overlay = queryElement('.modal-backdrop');
-      if ( overlay === null ) {
-        newOverlay.setAttribute('class', 'modal-backdrop' + (ops.animation ? ' fade' : ''));
-        overlay = newOverlay;
-        document.body.appendChild(overlay);
+
+    controls.forEach((arrow) => {
+      if (arrow) arrow[action]('click', carouselControlsHandler);
+    });
+
+    if (indicator) indicator[action]('click', carouselIndicatorHandler);
+    if (keyboard) window[action]('keydown', carouselKeyHandler);
+  }
+
+  function getActiveIndex(self) {
+    const { slides, element } = self;
+    return Array.from(slides)
+      .indexOf(element.getElementsByClassName(`${carouselItem} ${activeClass}`)[0]) || 0;
+  }
+
+  // CAROUSEL DEFINITION
+  // ===================
+  class Carousel extends BaseComponent {
+    constructor(target, config) {
+      super(carouselComponent, target, defaultCarouselOptions, config);
+      // bind
+      const self = this;
+
+      // additional properties
+      self.timer = null;
+      self.direction = 'left';
+      self.isPaused = false;
+      self.isAnimating = false;
+      self.index = 0;
+      self.timer = null;
+      self.isTouch = false;
+
+      // initialization element
+      const { element } = self;
+      // carousel elements
+      // a LIVE collection is prefferable
+      self.slides = element.getElementsByClassName(carouselItem);
+      const { slides } = self;
+
+      // invalidate when not enough items
+      // no need to go further
+      if (slides.length < 2) { return; }
+
+      self.controls = [
+        queryElement(`.${carouselControl}-prev`, element),
+        queryElement(`.${carouselControl}-next`, element),
+      ];
+
+      // a LIVE collection is prefferable
+      self.indicator = queryElement('.carousel-indicators', element);
+      self.indicators = (self.indicator && self.indicator.querySelectorAll(`[${dataBsSlideTo}]`)) || [];
+
+      // set JavaScript and DATA API options
+      const { options } = self;
+
+      // don't use TRUE as interval, it's actually 0, use the default 5000ms better
+      self.options.interval = options.interval === true
+        ? defaultCarouselOptions.interval
+        : options.interval;
+
+      // set first slide active if none
+      if (getActiveIndex(self) < 0) {
+        if (slides.length) addClass(slides[0], activeClass);
+        if (self.indicators.length) activateCarouselIndicator(self, 0);
       }
-      return overlay;
+
+      // attach event handlers
+      toggleCarouselHandlers(self, 1);
+
+      // start to cycle if interval is set
+      if (options.interval) self.cycle();
     }
-    function removeOverlay () {
-      overlay = queryElement('.modal-backdrop');
-      if ( overlay && !document.getElementsByClassName('modal show')[0] ) {
-        document.body.removeChild(overlay); overlay = null;
-      }
-      overlay === null && (document.body.classList.remove('modal-open'), resetScrollbar());
-    }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      window[action]( 'resize', self.update, passiveHandler);
-      modal[action]( 'click',dismissHandler,false);
-      document[action]( 'keydown',keyHandler,false);
-    }
-    function beforeShow() {
-      modal.style.display = 'block';
-      setScrollbar();
-      !document.getElementsByClassName('modal show')[0] && document.body.classList.add('modal-open');
-      modal.classList.add('show');
-      modal.setAttribute('aria-hidden', false);
-      modal.classList.contains('fade') ? emulateTransitionEnd(modal, triggerShow) : triggerShow();
-    }
-    function triggerShow() {
-      setFocus(modal);
-      modal.isAnimating = false;
-      toggleEvents(1);
-      shownCustomEvent = bootstrapCustomEvent('shown', 'modal', { relatedTarget: relatedTarget });
-      dispatchCustomEvent.call(modal, shownCustomEvent);
-    }
-    function triggerHide(force) {
-      modal.style.display = '';
-      element && (setFocus(element));
-      overlay = queryElement('.modal-backdrop');
-      if (force !== 1 && overlay && overlay.classList.contains('show') && !document.getElementsByClassName('modal show')[0]) {
-        overlay.classList.remove('show');
-        emulateTransitionEnd(overlay,removeOverlay);
-      } else {
-        removeOverlay();
-      }
-      toggleEvents();
-      modal.isAnimating = false;
-      hiddenCustomEvent = bootstrapCustomEvent('hidden', 'modal');
-      dispatchCustomEvent.call(modal, hiddenCustomEvent);
-    }
-    function clickHandler(e) {
-      if ( modal.isAnimating ) { return; }
-      var clickTarget = e.target,
-          modalID = "#" + (modal.getAttribute('id')),
-          targetAttrValue = clickTarget.getAttribute('data-target') || clickTarget.getAttribute('href'),
-          elemAttrValue = element.getAttribute('data-target') || element.getAttribute('href');
-      if ( !modal.classList.contains('show')
-          && (clickTarget === element && targetAttrValue === modalID
-          || element.contains(clickTarget) && elemAttrValue === modalID) ) {
-        modal.modalTrigger = element;
-        relatedTarget = element;
-        self.show();
-        e.preventDefault();
+
+    // CAROUSEL PUBLIC METHODS
+    // =======================
+    cycle() {
+      const self = this;
+      const { isPaused, element, options } = self;
+      if (self.timer) {
+        clearInterval(self.timer);
+        self.timer = null;
       }
-    }
-    function keyHandler(ref) {
-      var which = ref.which;
-      if (!modal.isAnimating && ops.keyboard && which == 27 && modal.classList.contains('show') ) {
-        self.hide();
+
+      if (isPaused) {
+        removeClass(element, pausedClass);
+        self.isPaused = !isPaused;
       }
+
+      self.timer = setInterval(() => {
+        if (isElementInScrollRange(element)) {
+          self.index += 1;
+          self.to(self.index);
+        }
+      }, options.interval);
     }
-    function dismissHandler(e) {
-      if ( modal.isAnimating ) { return; }
-      var clickTarget = e.target,
-          hasData = clickTarget.getAttribute('data-dismiss') === 'modal',
-          parentWithData = clickTarget.closest('[data-dismiss="modal"]');
-      if ( modal.classList.contains('show') && ( parentWithData || hasData
-          || clickTarget === modal && ops.backdrop !== 'static' ) ) {
-        self.hide(); relatedTarget = null;
-        e.preventDefault();
+
+    pause() {
+      const self = this;
+      const { element, options, isPaused } = self;
+      if (options.interval && !isPaused) {
+        clearInterval(self.timer);
+        self.timer = null;
+        addClass(element, pausedClass);
+        self.isPaused = !isPaused;
       }
     }
-    self.toggle = function () {
-      if ( modal.classList.contains('show') ) {self.hide();} else {self.show();}
-    };
-    self.show = function () {
-      if (modal.classList.contains('show') && !!modal.isAnimating ) {return}
-      showCustomEvent = bootstrapCustomEvent('show', 'modal', { relatedTarget: relatedTarget });
-      dispatchCustomEvent.call(modal, showCustomEvent);
-      if ( showCustomEvent.defaultPrevented ) { return; }
-      modal.isAnimating = true;
-      var currentOpen = document.getElementsByClassName('modal show')[0];
-      if (currentOpen && currentOpen !== modal) {
-        currentOpen.modalTrigger && currentOpen.modalTrigger.Modal.hide();
-        currentOpen.Modal && currentOpen.Modal.hide();
-      }
-      if ( ops.backdrop ) {
-        overlay = createOverlay();
-      }
-      if ( overlay && !currentOpen && !overlay.classList.contains('show') ) {
-        overlay.offsetWidth;
-        overlayDelay = getElementTransitionDuration(overlay);
-        overlay.classList.add('show');
-      }
-      !currentOpen ? setTimeout( beforeShow, overlay && overlayDelay ? overlayDelay:0 ) : beforeShow();
-    };
-    self.hide = function (force) {
-      if ( !modal.classList.contains('show') ) {return}
-      hideCustomEvent = bootstrapCustomEvent( 'hide', 'modal');
-      dispatchCustomEvent.call(modal, hideCustomEvent);
-      if ( hideCustomEvent.defaultPrevented ) { return; }
-      modal.isAnimating = true;
-      modal.classList.remove('show');
-      modal.setAttribute('aria-hidden', true);
-      modal.classList.contains('fade') && force !== 1 ? emulateTransitionEnd(modal, triggerHide) : triggerHide();
-    };
-    self.setContent = function (content) {
-      queryElement('.modal-content',modal).innerHTML = content;
-    };
-    self.update = function () {
-      if (modal.classList.contains('show')) {
-        setScrollbar();
-      }
-    };
-    self.dispose = function () {
-      self.hide(1);
-      if (element) {element.removeEventListener('click',clickHandler,false); delete element.Modal; }
-      else {delete modal.Modal;}
-    };
-    element = queryElement(element);
-    var checkModal = queryElement( element.getAttribute('data-target') || element.getAttribute('href') );
-    modal = element.classList.contains('modal') ? element : checkModal;
-    fixedItems = Array.from(document.getElementsByClassName('fixed-top'))
-                      .concat(Array.from(document.getElementsByClassName('fixed-bottom')));
-    if ( element.classList.contains('modal') ) { element = null; }
-    element && element.Modal && element.Modal.dispose();
-    modal && modal.Modal && modal.Modal.dispose();
-    ops.keyboard = options.keyboard === false || modal.getAttribute('data-keyboard') === 'false' ? false : true;
-    ops.backdrop = options.backdrop === 'static' || modal.getAttribute('data-backdrop') === 'static' ? 'static' : true;
-    ops.backdrop = options.backdrop === false || modal.getAttribute('data-backdrop') === 'false' ? false : ops.backdrop;
-    ops.animation = modal.classList.contains('fade') ? true : false;
-    ops.content = options.content;
-    modal.isAnimating = false;
-    if ( element && !element.Modal ) {
-      element.addEventListener('click',clickHandler,false);
-    }
-    if ( ops.content ) {
-      self.setContent( ops.content.trim() );
-    }
-    if (element) {
-      modal.modalTrigger = element;
-      element.Modal = self;
-    } else {
-      modal.Modal = self;
-    }
-  }
-
-  var mouseClickEvents = { down: 'mousedown', up: 'mouseup' };
-
-  function getScroll() {
-    return {
-      y : window.pageYOffset || document.documentElement.scrollTop,
-      x : window.pageXOffset || document.documentElement.scrollLeft
-    }
-  }
-
-  function styleTip(link,element,position,parent) {
-    var tipPositions = /\b(top|bottom|left|right)+/,
-        elementDimensions = { w : element.offsetWidth, h: element.offsetHeight },
-        windowWidth = (document.documentElement.clientWidth || document.body.clientWidth),
-        windowHeight = (document.documentElement.clientHeight || document.body.clientHeight),
-        rect = link.getBoundingClientRect(),
-        scroll = parent === document.body ? getScroll() : { x: parent.offsetLeft + parent.scrollLeft, y: parent.offsetTop + parent.scrollTop },
-        linkDimensions = { w: rect.right - rect.left, h: rect.bottom - rect.top },
-        isPopover = element.classList.contains('popover'),
-        arrow = element.getElementsByClassName('arrow')[0],
-        halfTopExceed = rect.top + linkDimensions.h/2 - elementDimensions.h/2 < 0,
-        halfLeftExceed = rect.left + linkDimensions.w/2 - elementDimensions.w/2 < 0,
-        halfRightExceed = rect.left + elementDimensions.w/2 + linkDimensions.w/2 >= windowWidth,
-        halfBottomExceed = rect.top + elementDimensions.h/2 + linkDimensions.h/2 >= windowHeight,
-        topExceed = rect.top - elementDimensions.h < 0,
-        leftExceed = rect.left - elementDimensions.w < 0,
-        bottomExceed = rect.top + elementDimensions.h + linkDimensions.h >= windowHeight,
-        rightExceed = rect.left + elementDimensions.w + linkDimensions.w >= windowWidth;
-    position = (position === 'left' || position === 'right') && leftExceed && rightExceed ? 'top' : position;
-    position = position === 'top' && topExceed ? 'bottom' : position;
-    position = position === 'bottom' && bottomExceed ? 'top' : position;
-    position = position === 'left' && leftExceed ? 'right' : position;
-    position = position === 'right' && rightExceed ? 'left' : position;
-    var topPosition,
-      leftPosition,
-      arrowTop,
-      arrowLeft,
-      arrowWidth,
-      arrowHeight;
-    element.className.indexOf(position) === -1 && (element.className = element.className.replace(tipPositions,position));
-    arrowWidth = arrow.offsetWidth; arrowHeight = arrow.offsetHeight;
-    if ( position === 'left' || position === 'right' ) {
-      if ( position === 'left' ) {
-        leftPosition = rect.left + scroll.x - elementDimensions.w - ( isPopover ? arrowWidth : 0 );
-      } else {
-        leftPosition = rect.left + scroll.x + linkDimensions.w;
-      }
-      if (halfTopExceed) {
-        topPosition = rect.top + scroll.y;
-        arrowTop = linkDimensions.h/2 - arrowWidth;
-      } else if (halfBottomExceed) {
-        topPosition = rect.top + scroll.y - elementDimensions.h + linkDimensions.h;
-        arrowTop = elementDimensions.h - linkDimensions.h/2 - arrowWidth;
-      } else {
-        topPosition = rect.top + scroll.y - elementDimensions.h/2 + linkDimensions.h/2;
-        arrowTop = elementDimensions.h/2 - (isPopover ? arrowHeight*0.9 : arrowHeight/2);
-      }
-    } else if ( position === 'top' || position === 'bottom' ) {
-      if ( position === 'top') {
-        topPosition =  rect.top + scroll.y - elementDimensions.h - ( isPopover ? arrowHeight : 0 );
-      } else {
-        topPosition = rect.top + scroll.y + linkDimensions.h;
-      }
-      if (halfLeftExceed) {
-        leftPosition = 0;
-        arrowLeft = rect.left + linkDimensions.w/2 - arrowWidth;
-      } else if (halfRightExceed) {
-        leftPosition = windowWidth - elementDimensions.w*1.01;
-        arrowLeft = elementDimensions.w - ( windowWidth - rect.left ) + linkDimensions.w/2 - arrowWidth/2;
-      } else {
-        leftPosition = rect.left + scroll.x - elementDimensions.w/2 + linkDimensions.w/2;
-        arrowLeft = elementDimensions.w/2 - ( isPopover ? arrowWidth : arrowWidth/2 );
-      }
+
+    next() {
+      const self = this;
+      if (!self.isAnimating) { self.index += 1; self.to(self.index); }
     }
-    element.style.top = topPosition + 'px';
-    element.style.left = leftPosition + 'px';
-    arrowTop && (arrow.style.top = arrowTop + 'px');
-    arrowLeft && (arrow.style.left = arrowLeft + 'px');
-  }
-
-  function Popover(element,options) {
-    options = options || {};
-    var self = this;
-    var popover = null,
-        timer = 0,
-        isIphone = /(iPhone|iPod|iPad)/.test(navigator.userAgent),
-        titleString,
-        contentString,
-        ops = {};
-    var triggerData,
-        animationData,
-        placementData,
-        dismissibleData,
-        delayData,
-        containerData,
-        closeBtn,
-        showCustomEvent,
-        shownCustomEvent,
-        hideCustomEvent,
-        hiddenCustomEvent,
-        containerElement,
-        containerDataElement,
-        modal,
-        navbarFixedTop,
-        navbarFixedBottom,
-        placementClass;
-    function dismissibleHandler(e) {
-      if (popover !== null && e.target === queryElement('.close',popover)) {
-        self.hide();
-      }
+
+    prev() {
+      const self = this;
+      if (!self.isAnimating) { self.index -= 1; self.to(self.index); }
     }
-    function getContents() {
-      return {
-        0 : options.title || element.getAttribute('data-title') || null,
-        1 : options.content || element.getAttribute('data-content') || null
+
+    to(idx) {
+      const self = this;
+      const {
+        element, isAnimating, slides, options,
+      } = self;
+      const activeItem = getActiveIndex(self);
+      let next = idx;
+
+      // when controled via methods, make sure to check again
+      // first return if we're on the same item #227
+      if (isAnimating || activeItem === next) return;
+
+      // determine transition direction
+      if ((activeItem < next) || (activeItem === 0 && next === slides.length - 1)) {
+        self.direction = 'left'; // next
+      } else if ((activeItem > next) || (activeItem === slides.length - 1 && next === 0)) {
+        self.direction = 'right'; // prev
       }
-    }
-    function removePopover() {
-      ops.container.removeChild(popover);
-      timer = null; popover = null;
-    }
-    function createPopover() {
-      titleString = getContents()[0] || null;
-      contentString = getContents()[1];
-      contentString = !!contentString ? contentString.trim() : null;
-      popover = document.createElement('div');
-      var popoverArrow = document.createElement('div');
-      popoverArrow.classList.add('arrow');
-      popover.appendChild(popoverArrow);
-      if ( contentString !== null && ops.template === null ) {
-        popover.setAttribute('role','tooltip');
-        if (titleString !== null) {
-          var popoverTitle = document.createElement('h3');
-          popoverTitle.classList.add('popover-header');
-          popoverTitle.innerHTML = ops.dismissible ? titleString + closeBtn : titleString;
-          popover.appendChild(popoverTitle);
+      const { direction } = self;
+
+      // find the right next index
+      if (next < 0) { next = slides.length - 1; } else if (next >= slides.length) { next = 0; }
+
+      // orientation, class name, eventProperties
+      const orientation = direction === 'left' ? 'next' : 'prev';
+      const directionClass = direction === 'left' ? 'start' : 'end';
+      const eventProperties = {
+        relatedTarget: slides[next], direction, from: activeItem, to: next,
+      };
+
+      // update event properties
+      Object.keys(eventProperties).forEach((k) => {
+        carouselSlideEvent[k] = eventProperties[k];
+        carouselSlidEvent[k] = eventProperties[k];
+      });
+
+      // discontinue when prevented
+      element.dispatchEvent(carouselSlideEvent);
+      if (carouselSlideEvent.defaultPrevented) return;
+
+      // update index
+      self.index = next;
+
+      clearInterval(self.timer);
+      self.timer = null;
+
+      self.isAnimating = true;
+      activateCarouselIndicator(self, next);
+
+      if (getElementTransitionDuration(slides[next]) && hasClass(element, 'slide')) {
+        addClass(slides[next], `${carouselItem}-${orientation}`);
+        reflow(slides[next]);
+        addClass(slides[next], `${carouselItem}-${directionClass}`);
+        addClass(slides[activeItem], `${carouselItem}-${directionClass}`);
+
+        emulateTransitionEnd(slides[next], () => carouselTransitionEndHandler(self));
+      } else {
+        addClass(slides[next], activeClass);
+        removeClass(slides[activeItem], activeClass);
+
+        setTimeout(() => {
+          self.isAnimating = false;
+
+          // check for element, might have been disposed
+          if (element && options.interval && !hasClass(element, pausedClass)) {
+            self.cycle();
+          }
+
+          element.dispatchEvent(carouselSlidEvent);
+        }, 100);
+      }
+    }
+
+    dispose() {
+      const self = this;
+      const { slides } = self;
+      const itemClasses = ['start', 'end', 'prev', 'next'];
+
+      Array.from(slides).forEach((slide, idx) => {
+        if (hasClass(slide, activeClass)) activateCarouselIndicator(self, idx);
+        itemClasses.forEach((c) => removeClass(slide, `${carouselItem}-${c}`));
+      });
+
+      toggleCarouselHandlers(self);
+      clearInterval(self.timer);
+      super.dispose(carouselComponent);
+    }
+  }
+
+  Carousel.init = {
+    component: carouselComponent,
+    selector: carouselSelector,
+    constructor: Carousel,
+  };
+
+  const ariaExpanded = 'aria-expanded';
+
+  // collapse / tab
+  const collapsingClass = 'collapsing';
+
+  const dataBsTarget = 'data-bs-target';
+
+  const dataBsParent = 'data-bs-parent';
+
+  const dataBsContainer = 'data-bs-container';
+
+  function getTargetElement(element) {
+    return queryElement(element.getAttribute(dataBsTarget) || element.getAttribute('href'))
+          || element.closest(element.getAttribute(dataBsParent))
+          || queryElement(element.getAttribute(dataBsContainer));
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Collapse
+  ----------------------------------------------- */
+
+  // COLLAPSE GC
+  // ===========
+  const collapseString = 'collapse';
+  const collapseComponent = 'Collapse';
+  const collapseSelector = `.${collapseString}`;
+  const collapseToggleSelector = `[${dataBsToggle}="${collapseString}"]`;
+
+  // COLLAPSE CUSTOM EVENTS
+  // ======================
+  const showCollapseEvent = bootstrapCustomEvent(`show.bs.${collapseString}`);
+  const shownCollapseEvent = bootstrapCustomEvent(`shown.bs.${collapseString}`);
+  const hideCollapseEvent = bootstrapCustomEvent(`hide.bs.${collapseString}`);
+  const hiddenCollapseEvent = bootstrapCustomEvent(`hidden.bs.${collapseString}`);
+
+  // COLLAPSE PRIVATE METHODS
+  // ========================
+  function expandCollapse(self) {
+    const {
+      element, parent, triggers,
+    } = self;
+
+    element.dispatchEvent(showCollapseEvent);
+    if (showCollapseEvent.defaultPrevented) return;
+
+    self.isAnimating = true;
+    if (parent) parent.isAnimating = true;
+
+    addClass(element, collapsingClass);
+    removeClass(element, collapseString);
+
+    element.style.height = `${element.scrollHeight}px`;
+
+    emulateTransitionEnd(element, () => {
+      self.isAnimating = false;
+      if (parent) parent.isAnimating = false;
+
+      triggers.forEach((btn) => btn.setAttribute(ariaExpanded, 'true'));
+
+      removeClass(element, collapsingClass);
+      addClass(element, collapseString);
+      addClass(element, showClass);
+
+      element.style.height = '';
+
+      element.dispatchEvent(shownCollapseEvent);
+    });
+  }
+
+  function collapseContent(self) {
+    const {
+      element, parent, triggers,
+    } = self;
+
+    element.dispatchEvent(hideCollapseEvent);
+
+    if (hideCollapseEvent.defaultPrevented) return;
+
+    self.isAnimating = true;
+    if (parent) parent.isAnimating = true;
+
+    element.style.height = `${element.scrollHeight}px`;
+
+    removeClass(element, collapseString);
+    removeClass(element, showClass);
+    addClass(element, collapsingClass);
+
+    reflow(element);
+    element.style.height = '0px';
+
+    emulateTransitionEnd(element, () => {
+      self.isAnimating = false;
+      if (parent) parent.isAnimating = false;
+
+      triggers.forEach((btn) => btn.setAttribute(ariaExpanded, 'false'));
+
+      removeClass(element, collapsingClass);
+      addClass(element, collapseString);
+
+      element.style.height = '';
+
+      element.dispatchEvent(hiddenCollapseEvent);
+    });
+  }
+
+  function toggleCollapseHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    const { triggers } = self;
+
+    if (triggers.length) {
+      triggers.forEach((btn) => btn[action]('click', collapseClickHandler));
+    }
+  }
+
+  // COLLAPSE EVENT HANDLER
+  // ======================
+  function collapseClickHandler(e) {
+    const { target } = e;
+    const trigger = target.closest(collapseToggleSelector);
+    const element = getTargetElement(trigger);
+    const self = element && element[collapseComponent];
+    if (self) self.toggle(target);
+
+    // event target is anchor link #398
+    if (trigger && trigger.tagName === 'A') e.preventDefault();
+  }
+
+  // COLLAPSE DEFINITION
+  // ===================
+  class Collapse extends BaseComponent {
+    constructor(target, config) {
+      super(collapseComponent, target, { parent: null }, config);
+      // bind
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+
+      // set triggering elements
+      self.triggers = Array.from(document.querySelectorAll(collapseToggleSelector))
+        .filter((btn) => getTargetElement(btn) === element);
+
+      // set parent accordion
+      self.parent = queryElement(self.options.parent);
+      const { parent } = self;
+
+      // set initial state
+      self.isAnimating = false;
+      if (parent) parent.isAnimating = false;
+
+      // add event listeners
+      toggleCollapseHandler(self, 1);
+    }
+
+    // COLLAPSE PUBLIC METHODS
+    // =======================
+    toggle(related) {
+      const self = this;
+      if (!hasClass(self.element, showClass)) self.show(related);
+      else self.hide(related);
+    }
+
+    hide() {
+      const self = this;
+      const { triggers, isAnimating } = self;
+      if (isAnimating) return;
+
+      collapseContent(self);
+      if (triggers.length) {
+        triggers.forEach((btn) => addClass(btn, `${collapseString}d`));
+      }
+    }
+
+    show() {
+      const self = this;
+      const {
+        element, parent, triggers, isAnimating,
+      } = self;
+      let activeCollapse;
+      let activeCollapseInstance;
+
+      if (parent) {
+        activeCollapse = Array.from(parent.querySelectorAll(`.${collapseString}.${showClass}`))
+          .find((i) => i[collapseComponent]);
+        activeCollapseInstance = activeCollapse && activeCollapse[collapseComponent];
+      }
+
+      if ((!parent || (parent && !parent.isAnimating)) && !isAnimating) {
+        if (activeCollapseInstance && activeCollapse !== element) {
+          collapseContent(activeCollapseInstance);
+          activeCollapseInstance.triggers.forEach((btn) => {
+            addClass(btn, `${collapseString}d`);
+          });
+        }
+
+        expandCollapse(self);
+        if (triggers.length) {
+          triggers.forEach((btn) => removeClass(btn, `${collapseString}d`));
+        }
+      }
+    }
+
+    dispose() {
+      const self = this;
+      const { parent } = self;
+      toggleCollapseHandler(self);
+
+      if (parent) delete parent.isAnimating;
+      super.dispose(collapseComponent);
+    }
+  }
+
+  Collapse.init = {
+    component: collapseComponent,
+    selector: collapseSelector,
+    constructor: Collapse,
+  };
+
+  const dropdownMenuClasses = ['dropdown', 'dropup', 'dropstart', 'dropend'];
+
+  const dropdownMenuClass = 'dropdown-menu';
+
+  function isEmptyAnchor(elem) {
+    const parentAnchor = elem.closest('A');
+    // anchor href starts with #
+    return elem && ((elem.href && elem.href.slice(-1) === '#')
+      // OR a child of an anchor with href starts with #
+      || (parentAnchor && parentAnchor.href && parentAnchor.href.slice(-1) === '#'));
+  }
+
+  function setFocus(element) {
+    element.focus();
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Dropdown
+  ----------------------------------------------- */
+
+  // DROPDOWN PRIVATE GC
+  // ===================
+  const [dropdownString] = dropdownMenuClasses;
+  const dropdownComponent = 'Dropdown';
+  const dropdownSelector = `[${dataBsToggle}="${dropdownString}"]`;
+
+  // DROPDOWN PRIVATE GC
+  // ===================
+  const dropupString = dropdownMenuClasses[1];
+  const dropstartString = dropdownMenuClasses[2];
+  const dropendString = dropdownMenuClasses[3];
+  const dropdownMenuEndClass = `${dropdownMenuClass}-end`;
+  const hideMenuClass = ['d-block', 'invisible'];
+  const verticalClass = [dropdownString, dropupString];
+  const horizontalClass = [dropstartString, dropendString];
+  const defaultDropdownOptions = {
+    offset: 5, // [number] 5(px)
+    display: 'dynamic', // [dynamic|static]
+  };
+
+  // DROPDOWN CUSTOM EVENTS
+  // ========================
+  const showDropdownEvent = bootstrapCustomEvent(`show.bs.${dropdownString}`);
+  const shownDropdownEvent = bootstrapCustomEvent(`shown.bs.${dropdownString}`);
+  const hideDropdownEvent = bootstrapCustomEvent(`hide.bs.${dropdownString}`);
+  const hiddenDropdownEvent = bootstrapCustomEvent(`hidden.bs.${dropdownString}`);
+
+  // DROPDOWN PRIVATE METHODS
+  // ========================
+  function styleDropdown(self, show) {
+    const {
+      element, menu, originalClass, menuEnd, options,
+    } = self;
+    const parent = element.parentElement;
+
+    // reset menu offset and position
+    const resetProps = ['margin', 'top', 'bottom', 'left', 'right'];
+    resetProps.forEach((p) => { menu.style[p] = ''; });
+    removeClass(parent, 'position-static');
+
+    if (!show) {
+      parent.className = originalClass.join(' ');
+      const menuAction = menuEnd && !hasClass(menu, dropdownMenuEndClass) ? addClass : removeClass;
+      menuAction(menu, dropdownMenuEndClass);
+      return;
+    }
+
+    const { offset } = options;
+    let positionClass = dropdownMenuClasses.find((c) => originalClass.includes(c));
+
+    let dropdownMargin = {
+      dropdown: [offset, 0, 0],
+      dropup: [0, 0, offset],
+      dropstart: [-1, offset, 0],
+      dropend: [-1, 0, 0, offset],
+    };
+
+    const dropdownPosition = {
+      dropdown: { top: '100%' },
+      dropup: { top: 'auto', bottom: '100%' },
+      dropstart: { left: 'auto', right: '100%' },
+      dropend: { left: '100%', right: 'auto' },
+      menuEnd: { right: 0, left: 'auto' },
+    };
+
+    // force showing the menu to calculate its size
+    hideMenuClass.forEach((c) => addClass(menu, c));
+
+    const dropdownRegex = new RegExp(`\\b(${dropdownString}|${dropupString}|${dropstartString}|${dropendString})+`);
+    const elementDimensions = { w: element.offsetWidth, h: element.offsetHeight };
+    const menuDimensions = { w: menu.offsetWidth, h: menu.offsetHeight };
+    const HTML = document.documentElement;
+    const BD = document.body;
+    const windowWidth = (HTML.clientWidth || BD.clientWidth);
+    const windowHeight = (HTML.clientHeight || BD.clientHeight);
+    const targetBCR = element.getBoundingClientRect();
+    // dropdownMenuEnd && [ dropdown | dropup ]
+    const leftExceed = targetBCR.left + elementDimensions.w - menuDimensions.w < 0;
+    // dropstart
+    const leftFullExceed = targetBCR.left - menuDimensions.w < 0;
+    // !dropdownMenuEnd && [ dropdown | dropup ]
+    const rightExceed = targetBCR.left + menuDimensions.w >= windowWidth;
+    // dropend
+    const rightFullExceed = targetBCR.left + menuDimensions.w + elementDimensions.w >= windowWidth;
+    // dropstart | dropend
+    const bottomExceed = targetBCR.top + menuDimensions.h >= windowHeight;
+    // dropdown
+    const bottomFullExceed = targetBCR.top + menuDimensions.h + elementDimensions.h >= windowHeight;
+    // dropup
+    const topExceed = targetBCR.top - menuDimensions.h < 0;
+
+    const btnGroup = parent.parentNode.closest('.btn-group,.btn-group-vertical');
+
+    // recompute position
+    if (horizontalClass.includes(positionClass) && leftFullExceed && rightFullExceed) {
+      positionClass = dropdownString;
+    }
+    if (horizontalClass.includes(positionClass) && bottomExceed) {
+      positionClass = dropupString;
+    }
+    if (positionClass === dropstartString && leftFullExceed && !bottomExceed) {
+      positionClass = dropendString;
+    }
+    if (positionClass === dropendString && rightFullExceed && !bottomExceed) {
+      positionClass = dropstartString;
+    }
+    if (positionClass === dropupString && topExceed && !bottomFullExceed) {
+      positionClass = dropdownString;
+    }
+    if (positionClass === dropdownString && bottomFullExceed && !topExceed) {
+      positionClass = dropupString;
+    }
+
+    // set spacing
+    dropdownMargin = dropdownMargin[positionClass];
+    menu.style.margin = `${dropdownMargin.map((x) => (x ? `${x}px` : x)).join(' ')}`;
+    Object.keys(dropdownPosition[positionClass]).forEach((position) => {
+      menu.style[position] = dropdownPosition[positionClass][position];
+    });
+
+    // update dropdown position class
+    if (!hasClass(parent, positionClass)) {
+      parent.className = parent.className.replace(dropdownRegex, positionClass);
+    }
+
+    // update dropdown / dropup to handle parent btn-group element
+    // as well as the dropdown-menu-end utility class
+    if (verticalClass.includes(positionClass)) {
+      const menuEndAction = rightExceed ? addClass : removeClass;
+
+      if (!btnGroup) menuEndAction(menu, dropdownMenuEndClass);
+      else if (leftExceed) addClass(parent, 'position-static');
+
+      if (hasClass(menu, dropdownMenuEndClass)) {
+        Object.keys(dropdownPosition.menuEnd).forEach((p) => {
+          menu.style[p] = dropdownPosition.menuEnd[p];
+        });
+      }
+    }
+
+    // remove util classes from the menu, we have its size
+    hideMenuClass.forEach((c) => removeClass(menu, c));
+  }
+
+  function toggleDropdownDismiss(self) {
+    const action = self.open ? addEventListener : removeEventListener;
+
+    document[action]('click', dropdownDismissHandler);
+    document[action]('focus', dropdownDismissHandler);
+    document[action]('keydown', dropdownPreventScroll);
+    document[action]('keyup', dropdownKeyHandler);
+    if (self.options.display === 'dynamic') {
+      window[action]('scroll', dropdownLayoutHandler, passiveHandler);
+      window[action]('resize', dropdownLayoutHandler, passiveHandler);
+    }
+  }
+
+  function toggleDropdownHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    self.element[action]('click', dropdownClickHandler);
+  }
+
+  function getCurrentOpenDropdown() {
+    const currentParent = dropdownMenuClasses
+      .map((c) => document.getElementsByClassName(`${c} ${showClass}`))
+      .find((x) => x.length);
+
+    if (currentParent && currentParent.length) {
+      return Array.from(currentParent[0].children).find((x) => x.hasAttribute(dataBsToggle));
+    }
+    return null;
+  }
+
+  // DROPDOWN EVENT HANDLERS
+  // =======================
+  function dropdownDismissHandler(e) {
+    const { target, type } = e;
+    if (!target.closest) return; // some weird FF bug #409
+
+    const element = getCurrentOpenDropdown();
+    const parent = element && element.parentNode;
+    const self = element && element[dropdownComponent];
+    const menu = self && self.menu;
+
+    const hasData = target.closest(dropdownSelector) !== null;
+    const isForm = parent && parent.contains(target)
+      && (target.tagName === 'form' || target.closest('form') !== null);
+
+    if (type === 'click' && isEmptyAnchor(target)) {
+      e.preventDefault();
+    }
+    if (type === 'focus'
+      && (target === element || target === menu || menu.contains(target))) {
+      return;
+    }
+
+    if (isForm || hasData) ; else if (self) {
+      self.hide(element);
+    }
+  }
+
+  function dropdownClickHandler(e) {
+    const element = this;
+    const self = element[dropdownComponent];
+    self.toggle(element);
+
+    if (isEmptyAnchor(e.target)) e.preventDefault();
+  }
+
+  function dropdownPreventScroll(e) {
+    if (e.which === 38 || e.which === 40) e.preventDefault();
+  }
+
+  function dropdownKeyHandler({ which }) {
+    const element = getCurrentOpenDropdown();
+    const self = element[dropdownComponent];
+    const { menu, menuItems, open } = self;
+    const activeItem = document.activeElement;
+    const isSameElement = activeItem === element;
+    const isInsideMenu = menu.contains(activeItem);
+    const isMenuItem = activeItem.parentNode === menu || activeItem.parentNode.parentNode === menu;
+
+    let idx = menuItems.indexOf(activeItem);
+
+    if (isMenuItem) { // navigate up | down
+      if (isSameElement) {
+        idx = 0;
+      } else if (which === 38) {
+        idx = idx > 1 ? idx - 1 : 0;
+      } else if (which === 40) {
+        idx = idx < menuItems.length - 1 ? idx + 1 : idx;
+      }
+
+      if (menuItems[idx]) setFocus(menuItems[idx]);
+    }
+
+    if (((menuItems.length && isMenuItem) // menu has items
+        || (!menuItems.length && (isInsideMenu || isSameElement)) // menu might be a form
+        || !isInsideMenu) // or the focused element is not in the menu at all
+        && open && which === 27 // menu must be open
+    ) {
+      self.toggle();
+    }
+  }
+
+  function dropdownLayoutHandler() {
+    const element = getCurrentOpenDropdown();
+    const self = element && element[dropdownComponent];
+
+    if (self && self.open) styleDropdown(self, 1);
+  }
+
+  // DROPDOWN DEFINITION
+  // ===================
+  class Dropdown extends BaseComponent {
+    constructor(target, config) {
+      super(dropdownComponent, target, defaultDropdownOptions, config);
+      // bind
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+
+      // set targets
+      const parent = element.parentElement;
+      self.menu = queryElement(`.${dropdownMenuClass}`, parent);
+      const { menu } = self;
+
+      self.originalClass = Array.from(parent.classList);
+
+      // set original position
+      self.menuEnd = hasClass(menu, dropdownMenuEndClass);
+
+      self.menuItems = [];
+
+      Array.from(menu.children).forEach((child) => {
+        if (child.children.length && (child.children[0].tagName === 'A')) self.menuItems.push(child.children[0]);
+        if (child.tagName === 'A') self.menuItems.push(child);
+      });
+
+      // set initial state to closed
+      self.open = false;
+
+      // add event listener
+      toggleDropdownHandler(self, 1);
+    }
+
+    // DROPDOWN PUBLIC METHODS
+    // =======================
+    toggle(related) {
+      const self = this;
+      const { open } = self;
+
+      if (open) self.hide(related);
+      else self.show(related);
+    }
+
+    show(related) {
+      const self = this;
+      const currentParent = queryElement(dropdownMenuClasses.map((c) => `.${c}.${showClass}`).join(','));
+      const currentElement = currentParent && queryElement(dropdownSelector, currentParent);
+
+      if (currentElement) currentElement[dropdownComponent].hide();
+
+      const { element, menu, open } = self;
+      const parent = element.parentNode;
+
+      // update relatedTarget and dispatch
+      showDropdownEvent.relatedTarget = related || null;
+      parent.dispatchEvent(showDropdownEvent);
+      if (showDropdownEvent.defaultPrevented) return;
+
+      // change menu position
+      styleDropdown(self, 1);
+
+      addClass(menu, showClass);
+      addClass(parent, showClass);
+
+      element.setAttribute(ariaExpanded, true);
+      self.open = !open;
+
+      setTimeout(() => {
+        setFocus(menu.getElementsByTagName('INPUT')[0] || element); // focus the first input item | element
+        toggleDropdownDismiss(self);
+
+        shownDropdownEvent.relatedTarget = related || null;
+        parent.dispatchEvent(shownDropdownEvent);
+      }, 1);
+    }
+
+    hide(related) {
+      const self = this;
+      const { element, menu, open } = self;
+      const parent = element.parentNode;
+      hideDropdownEvent.relatedTarget = related || null;
+      parent.dispatchEvent(hideDropdownEvent);
+      if (hideDropdownEvent.defaultPrevented) return;
+
+      removeClass(menu, showClass);
+      removeClass(parent, showClass);
+
+      // revert to original position
+      styleDropdown(self);
+
+      element.setAttribute(ariaExpanded, false);
+      self.open = !open;
+
+      setFocus(element);
+
+      // only re-attach handler if the instance is not disposed
+      setTimeout(() => toggleDropdownDismiss(self), 1);
+
+      // update relatedTarget and dispatch
+      hiddenDropdownEvent.relatedTarget = related || null;
+      parent.dispatchEvent(hiddenDropdownEvent);
+    }
+
+    dispose() {
+      const self = this;
+      const { element } = self;
+
+      if (hasClass(element.parentNode, showClass) && self.open) self.hide();
+
+      toggleDropdownHandler(self);
+
+      super.dispose(dropdownComponent);
+    }
+  }
+
+  Dropdown.init = {
+    component: dropdownComponent,
+    selector: dropdownSelector,
+    constructor: Dropdown,
+  };
+
+  const ariaHidden = 'aria-hidden';
+
+  const ariaModal = 'aria-modal';
+
+  const fixedTopClass = 'fixed-top';
+
+  const fixedBottomClass = 'fixed-bottom';
+
+  const stickyTopClass = 'sticky-top';
+
+  const fixedItems = Array.from(document.getElementsByClassName(fixedTopClass))
+    .concat(Array.from(document.getElementsByClassName(fixedBottomClass)))
+    .concat(Array.from(document.getElementsByClassName(stickyTopClass)))
+    .concat(Array.from(document.getElementsByClassName('is-fixed')));
+
+  function resetScrollbar() {
+    const bd = document.body;
+    bd.style.paddingRight = '';
+    bd.style.overflow = '';
+
+    if (fixedItems.length) {
+      fixedItems.forEach((fixed) => {
+        fixed.style.paddingRight = '';
+        fixed.style.marginRight = '';
+      });
+    }
+  }
+
+  function measureScrollbar() {
+    const windowWidth = document.documentElement.clientWidth;
+    return Math.abs(window.innerWidth - windowWidth);
+  }
+
+  function setScrollbar(scrollbarWidth, overflow) {
+    const bd = document.body;
+    const bdStyle = getComputedStyle(bd);
+    const bodyPad = parseInt(bdStyle.paddingRight, 10);
+    const isOpen = bdStyle.overflow === 'hidden';
+    const sbWidth = isOpen && bodyPad ? 0 : scrollbarWidth;
+
+    if (overflow) {
+      bd.style.overflow = 'hidden';
+      bd.style.paddingRight = `${bodyPad + sbWidth}px`;
+
+      if (fixedItems.length) {
+        fixedItems.forEach((fixed) => {
+          const isSticky = hasClass(fixed, stickyTopClass);
+          const itemPadValue = getComputedStyle(fixed).paddingRight;
+          fixed.style.paddingRight = `${parseInt(itemPadValue, 10) + sbWidth}px`;
+          if (isSticky) {
+            const itemMValue = getComputedStyle(fixed).marginRight;
+            fixed.style.marginRight = `${parseInt(itemMValue, 10) - sbWidth}px`;
+          }
+        });
+      }
+    }
+  }
+
+  const modalOpenClass = 'modal-open';
+  const modalBackdropClass = 'modal-backdrop';
+  const modalActiveSelector = `.modal.${showClass}`;
+  const offcanvasActiveSelector = `.offcanvas.${showClass}`;
+
+  const overlay = document.createElement('div');
+  overlay.setAttribute('class', `${modalBackdropClass}`);
+
+  function getCurrentOpen() {
+    return queryElement(`${modalActiveSelector},${offcanvasActiveSelector}`);
+  }
+
+  function appendOverlay(hasFade) {
+    document.body.appendChild(overlay);
+    if (hasFade) addClass(overlay, fadeClass);
+  }
+
+  function showOverlay() {
+    addClass(overlay, showClass);
+    reflow(overlay);
+  }
+
+  function hideOverlay() {
+    removeClass(overlay, showClass);
+  }
+
+  function removeOverlay() {
+    const bd = document.body;
+    const currentOpen = getCurrentOpen();
+
+    if (!currentOpen) {
+      removeClass(overlay, fadeClass);
+      removeClass(bd, modalOpenClass);
+      bd.removeChild(overlay);
+      resetScrollbar();
+    }
+  }
+
+  function isVisible(element) {
+    return getComputedStyle(element).visibility !== 'hidden'
+      && element.offsetParent !== null;
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Modal
+  -------------------------------------------- */
+
+  // MODAL PRIVATE GC
+  // ================
+  const modalString = 'modal';
+  const modalComponent = 'Modal';
+  const modalSelector = `.${modalString}`;
+  // const modalActiveSelector = `.${modalString}.${showClass}`;
+  const modalToggleSelector = `[${dataBsToggle}="${modalString}"]`;
+  const modalDismissSelector = `[${dataBsDismiss}="${modalString}"]`;
+  const modalStaticClass = `${modalString}-static`;
+  const modalDefaultOptions = {
+    backdrop: true, // boolean|string
+    keyboard: true, // boolean
+  };
+
+  // MODAL CUSTOM EVENTS
+  // ===================
+  const showModalEvent = bootstrapCustomEvent(`show.bs.${modalString}`);
+  const shownModalEvent = bootstrapCustomEvent(`shown.bs.${modalString}`);
+  const hideModalEvent = bootstrapCustomEvent(`hide.bs.${modalString}`);
+  const hiddenModalEvent = bootstrapCustomEvent(`hidden.bs.${modalString}`);
+
+  // MODAL PRIVATE METHODS
+  // =====================
+  function setModalScrollbar(self) {
+    const { element, scrollbarWidth } = self;
+    const bd = document.body;
+    const html = document.documentElement;
+    const bodyOverflow = html.clientHeight !== html.scrollHeight
+                      || bd.clientHeight !== bd.scrollHeight;
+    const modalOverflow = element.clientHeight !== element.scrollHeight;
+
+    if (!modalOverflow && scrollbarWidth) {
+      element.style.paddingRight = `${scrollbarWidth}px`;
+    }
+    setScrollbar(scrollbarWidth, (modalOverflow || bodyOverflow));
+  }
+
+  function toggleModalDismiss(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    window[action]('resize', self.update, passiveHandler);
+    self.element[action]('click', modalDismissHandler);
+    document[action]('keydown', modalKeyHandler);
+  }
+
+  function toggleModalHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    const { triggers } = self;
+
+    if (triggers.length) {
+      triggers.forEach((btn) => btn[action]('click', modalClickHandler));
+    }
+  }
+
+  function afterModalHide(self) {
+    const { triggers } = self;
+    removeOverlay();
+    self.element.style.paddingRight = '';
+    self.isAnimating = false;
+
+    if (triggers.length) {
+      const visibleTrigger = triggers.find((x) => isVisible(x));
+      if (visibleTrigger) setFocus(visibleTrigger);
+    }
+  }
+
+  function afterModalShow(self) {
+    const { element, relatedTarget } = self;
+    setFocus(element);
+    self.isAnimating = false;
+
+    toggleModalDismiss(self, 1);
+
+    shownModalEvent.relatedTarget = relatedTarget;
+    element.dispatchEvent(shownModalEvent);
+  }
+
+  function beforeModalShow(self) {
+    const { element, hasFade } = self;
+    element.style.display = 'block';
+
+    setModalScrollbar(self);
+    if (!queryElement(modalActiveSelector)) {
+      document.body.style.overflow = 'hidden';
+      addClass(document.body, modalOpenClass);
+    }
+
+    addClass(element, showClass);
+    element.removeAttribute(ariaHidden);
+    element.setAttribute(ariaModal, true);
+
+    if (hasFade) emulateTransitionEnd(element, () => afterModalShow(self));
+    else afterModalShow(self);
+  }
+
+  function beforeModalHide(self, force) {
+    const {
+      element, relatedTarget, hasFade,
+    } = self;
+    const currentOpen = getCurrentOpen();
+
+    element.style.display = '';
+
+    // force can also be the transitionEvent object, we wanna make sure it's not
+    // call is not forced and overlay is visible
+    if (!force && hasFade && hasClass(overlay, showClass)
+      && !currentOpen) { // AND no modal is visible
+      hideOverlay();
+      emulateTransitionEnd(overlay, () => afterModalHide(self));
+    } else {
+      afterModalHide(self);
+    }
+
+    toggleModalDismiss(self);
+
+    hiddenModalEvent.relatedTarget = relatedTarget;
+    element.dispatchEvent(hiddenModalEvent);
+  }
+
+  // MODAL EVENT HANDLERS
+  // ====================
+  function modalClickHandler(e) {
+    const { target } = e;
+    const trigger = target.closest(modalToggleSelector);
+    const element = getTargetElement(trigger);
+    const self = element && element[modalComponent];
+
+    if (trigger.tagName === 'A') e.preventDefault();
+
+    if (self.isAnimating) return;
+
+    self.relatedTarget = trigger;
+
+    self.toggle();
+  }
+
+  function modalKeyHandler({ which }) {
+    const element = queryElement(modalActiveSelector);
+    const self = element[modalComponent];
+    const { options, isAnimating } = self;
+    if (!isAnimating // modal has no animations running
+      && options.keyboard && which === 27 // the keyboard option is enabled and the key is 27
+      && hasClass(element, showClass)) { // the modal is not visible
+      self.relatedTarget = null;
+      self.hide();
+    }
+  }
+
+  function modalDismissHandler(e) {
+    const element = this;
+    const self = element[modalComponent];
+
+    if (self.isAnimating) return;
+
+    const { isStatic, modalDialog } = self;
+    const { target } = e;
+    const selectedText = document.getSelection().toString().length;
+    const targetInsideDialog = modalDialog.contains(target);
+    const dismiss = target.closest(modalDismissSelector);
+
+    if (isStatic && !targetInsideDialog) {
+      addClass(element, modalStaticClass);
+      self.isAnimating = true;
+      emulateTransitionEnd(modalDialog, () => staticTransitionEnd(self));
+    } else if (dismiss || (!selectedText && !isStatic && !targetInsideDialog)) {
+      self.relatedTarget = dismiss || null;
+      self.hide();
+      e.preventDefault();
+    }
+  }
+
+  function staticTransitionEnd(self) {
+    const duration = getElementTransitionDuration(self.modalDialog) + 17;
+    removeClass(self.element, modalStaticClass);
+    // user must wait for zoom out transition
+    setTimeout(() => { self.isAnimating = false; }, duration);
+  }
+
+  // MODAL DEFINITION
+  // ================
+  class Modal extends BaseComponent {
+    constructor(target, config) {
+      super(modalComponent, target, modalDefaultOptions, config);
+
+      // bind
+      const self = this;
+
+      // the modal
+      const { element } = self;
+
+      // the modal-dialog
+      self.modalDialog = queryElement(`.${modalString}-dialog`, element);
+
+      // modal can have multiple triggering elements
+      self.triggers = Array.from(document.querySelectorAll(modalToggleSelector))
+        .filter((btn) => getTargetElement(btn) === element);
+
+      // additional internals
+      self.isStatic = self.options.backdrop === 'static';
+      self.hasFade = hasClass(element, fadeClass);
+      self.isAnimating = false;
+      self.scrollbarWidth = measureScrollbar();
+      self.relatedTarget = null;
+
+      // attach event listeners
+      toggleModalHandler(self, 1);
+
+      // bind
+      self.update = self.update.bind(self);
+    }
+
+    // MODAL PUBLIC METHODS
+    // ====================
+    toggle() {
+      const self = this;
+      if (hasClass(self.element, showClass)) self.hide();
+      else self.show();
+    }
+
+    show() {
+      const self = this;
+      const {
+        element, isAnimating, hasFade, relatedTarget,
+      } = self;
+      let overlayDelay = 0;
+
+      if (hasClass(element, showClass) && !isAnimating) return;
+
+      showModalEvent.relatedTarget = relatedTarget || null;
+      element.dispatchEvent(showModalEvent);
+      if (showModalEvent.defaultPrevented) return;
+
+      self.isAnimating = true;
+
+      // we elegantly hide any opened modal/offcanvas
+      const currentOpen = getCurrentOpen();
+      if (currentOpen && currentOpen !== element) {
+        const that = currentOpen[modalComponent]
+          ? currentOpen[modalComponent]
+          : currentOpen.Offcanvas;
+        that.hide();
+      }
+
+      if (!queryElement(`.${modalBackdropClass}`)) {
+        appendOverlay(hasFade);
+      }
+      overlayDelay = getElementTransitionDuration(overlay);
+
+      if (!hasClass(overlay, showClass)) {
+        showOverlay();
+      }
+
+      if (!currentOpen) {
+        setTimeout(() => beforeModalShow(self), overlayDelay);
+      } else beforeModalShow(self);
+    }
+
+    hide(force) {
+      const self = this;
+      const {
+        element, isAnimating, hasFade, relatedTarget,
+      } = self;
+      if (!hasClass(element, showClass) && !isAnimating) return;
+
+      hideModalEvent.relatedTarget = relatedTarget || null;
+      element.dispatchEvent(hideModalEvent);
+      if (hideModalEvent.defaultPrevented) return;
+
+      self.isAnimating = true;
+      removeClass(element, showClass);
+      element.setAttribute(ariaHidden, true);
+      element.removeAttribute(ariaModal);
+
+      if (hasFade && force !== 1) {
+        emulateTransitionEnd(element, () => beforeModalHide(self));
+      } else {
+        beforeModalHide(self, force);
+      }
+    }
+
+    update() {
+      const self = this;
+
+      if (hasClass(self.element, showClass)) setModalScrollbar(self);
+    }
+
+    dispose() {
+      const self = this;
+      self.hide(1); // forced call
+
+      toggleModalHandler(self);
+
+      super.dispose(modalComponent);
+    }
+  }
+
+  Modal.init = {
+    component: modalComponent,
+    selector: modalSelector,
+    constructor: Modal,
+  };
+
+  /* Native JavaScript for Bootstrap 5 | OffCanvas
+  ------------------------------------------------ */
+
+  // OFFCANVAS PRIVATE GC
+  // ====================
+  const offcanvasString = 'offcanvas';
+  const offcanvasComponent = 'Offcanvas';
+  const OffcanvasSelector = `.${offcanvasString}`;
+  const offcanvasToggleSelector = `[${dataBsToggle}="${offcanvasString}"]`;
+  const offcanvasDismissSelector = `[${dataBsDismiss}="${offcanvasString}"]`;
+  const offcanvasTogglingClass = `${offcanvasString}-toggling`;
+  const offcanvasDefaultOptions = {
+    backdrop: true, // boolean
+    keyboard: true, // boolean
+    scroll: false, // boolean
+  };
+
+  // OFFCANVAS CUSTOM EVENTS
+  // =======================
+  const showOffcanvasEvent = bootstrapCustomEvent(`show.bs.${offcanvasString}`);
+  const shownOffcanvasEvent = bootstrapCustomEvent(`shown.bs.${offcanvasString}`);
+  const hideOffcanvasEvent = bootstrapCustomEvent(`hide.bs.${offcanvasString}`);
+  const hiddenOffcanvasEvent = bootstrapCustomEvent(`hidden.bs.${offcanvasString}`);
+
+  // OFFCANVAS PRIVATE METHODS
+  // =========================
+  function setOffCanvasScrollbar(self) {
+    const bd = document.body;
+    const html = document.documentElement;
+    const bodyOverflow = html.clientHeight !== html.scrollHeight
+                      || bd.clientHeight !== bd.scrollHeight;
+    setScrollbar(self.scrollbarWidth, bodyOverflow);
+  }
+
+  function toggleOffcanvasEvents(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    self.triggers.forEach((btn) => btn[action]('click', offcanvasTriggerHandler));
+  }
+
+  function toggleOffCanvasDismiss(add) {
+    const action = add ? addEventListener : removeEventListener;
+    document[action]('keydown', offcanvasKeyDismissHandler);
+    document[action]('click', offcanvasDismissHandler);
+  }
+
+  function beforeOffcanvasShow(self) {
+    const { element, options } = self;
+
+    if (!options.scroll) {
+      addClass(document.body, modalOpenClass);
+      document.body.style.overflow = 'hidden';
+      setOffCanvasScrollbar(self);
+    }
+
+    addClass(element, offcanvasTogglingClass);
+    addClass(element, showClass);
+    element.style.visibility = 'visible';
+
+    emulateTransitionEnd(element, () => showOffcanvasComplete(self));
+  }
+
+  function beforeOffcanvasHide(self) {
+    const { element, options } = self;
+    const currentOpen = getCurrentOpen();
+
+    element.blur();
+
+    if (!currentOpen && options.backdrop && hasClass(overlay, showClass)) {
+      hideOverlay();
+      emulateTransitionEnd(overlay, () => hideOffcanvasComplete(self));
+    } else hideOffcanvasComplete(self);
+  }
+
+  // OFFCANVAS EVENT HANDLERS
+  // ========================
+  function offcanvasTriggerHandler(e) {
+    const trigger = this.closest(offcanvasToggleSelector);
+    const element = getTargetElement(trigger);
+    const self = element && element[offcanvasComponent];
+
+    if (trigger.tagName === 'A') e.preventDefault();
+    if (self) {
+      self.relatedTarget = trigger;
+      self.toggle();
+    }
+  }
+
+  function offcanvasDismissHandler(e) {
+    const element = queryElement(offcanvasActiveSelector);
+    if (!element) return;
+
+    const offCanvasDismiss = queryElement(offcanvasDismissSelector, element);
+    const self = element[offcanvasComponent];
+    if (!self) return;
+
+    const { options, open, triggers } = self;
+    const { target } = e;
+    const trigger = target.closest(offcanvasToggleSelector);
+
+    if (trigger && trigger.tagName === 'A') e.preventDefault();
+
+    if (open && ((!element.contains(target) && options.backdrop
+      && (!trigger || (trigger && !triggers.includes(trigger))))
+      || offCanvasDismiss.contains(target))) {
+      self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
+      self.hide();
+    }
+  }
+
+  function offcanvasKeyDismissHandler({ which }) {
+    const element = queryElement(offcanvasActiveSelector);
+    if (!element) return;
+
+    const self = element[offcanvasComponent];
+
+    if (self && self.options.keyboard && which === 27) {
+      self.relatedTarget = null;
+      self.hide();
+    }
+  }
+
+  function showOffcanvasComplete(self) {
+    const { element, triggers, relatedTarget } = self;
+    removeClass(element, offcanvasTogglingClass);
+
+    element.removeAttribute(ariaHidden);
+    element.setAttribute(ariaModal, true);
+    element.setAttribute('role', 'dialog');
+    self.isAnimating = false;
+
+    if (triggers.length) {
+      triggers.forEach((btn) => btn.setAttribute(ariaExpanded, true));
+    }
+
+    shownOffcanvasEvent.relatedTarget = relatedTarget || null;
+    element.dispatchEvent(shownOffcanvasEvent);
+
+    toggleOffCanvasDismiss(1);
+    setFocus(element);
+  }
+
+  function hideOffcanvasComplete(self) {
+    const {
+      element, options, relatedTarget, triggers,
+    } = self;
+    const currentOpen = getCurrentOpen();
+
+    element.setAttribute(ariaHidden, true);
+    element.removeAttribute(ariaModal);
+    element.removeAttribute('role');
+    element.style.visibility = '';
+    self.open = false;
+    self.isAnimating = false;
+
+    if (triggers.length) {
+      triggers.forEach((btn) => btn.setAttribute(ariaExpanded, false));
+      const visibleTrigger = triggers.find((x) => isVisible(x));
+      if (visibleTrigger) setFocus(visibleTrigger);
+    }
+
+    // handle new offcanvas showing up
+    if (!currentOpen) {
+      if (options.backdrop) removeOverlay();
+      if (!options.scroll) {
+        resetScrollbar();
+        removeClass(document.body, modalOpenClass);
+      }
+    }
+
+    hiddenOffcanvasEvent.relatedTarget = relatedTarget || null;
+    element.dispatchEvent(hiddenOffcanvasEvent);
+    removeClass(element, offcanvasTogglingClass);
+
+    toggleOffCanvasDismiss();
+  }
+
+  // OFFCANVAS DEFINITION
+  // ====================
+  class Offcanvas extends BaseComponent {
+    constructor(target, config) {
+      super(offcanvasComponent, target, offcanvasDefaultOptions, config);
+      const self = this;
+
+      // instance element
+      const { element } = self;
+
+      // all the triggering buttons
+      self.triggers = Array.from(document.querySelectorAll(offcanvasToggleSelector))
+        .filter((btn) => getTargetElement(btn) === element);
+
+      // additional instance property
+      self.open = false;
+      self.isAnimating = false;
+      self.scrollbarWidth = measureScrollbar();
+
+      // attach event listeners
+      toggleOffcanvasEvents(self, 1);
+    }
+
+    // OFFCANVAS PUBLIC METHODS
+    // ========================
+    toggle() {
+      const self = this;
+      return self.open ? self.hide() : self.show();
+    }
+
+    show() {
+      const self = this[offcanvasComponent] ? this[offcanvasComponent] : this;
+      const {
+        element, options, isAnimating, relatedTarget,
+      } = self;
+      let overlayDelay = 0;
+
+      if (self.open || isAnimating) return;
+
+      showOffcanvasEvent.relatedTarget = relatedTarget || null;
+      element.dispatchEvent(showOffcanvasEvent);
+
+      if (showOffcanvasEvent.defaultPrevented) return;
+
+      // we elegantly hide any opened modal/offcanvas
+      const currentOpen = getCurrentOpen();
+      if (currentOpen && currentOpen !== element) {
+        const that = currentOpen[offcanvasComponent]
+          ? currentOpen[offcanvasComponent]
+          : currentOpen.Modal;
+        that.hide();
+      }
+
+      self.open = true;
+      self.isAnimating = true;
+
+      if (options.backdrop) {
+        if (!queryElement(`.${modalBackdropClass}`)) {
+          appendOverlay(1);
+        }
+
+        overlayDelay = getElementTransitionDuration(overlay);
+
+        if (!hasClass(overlay, showClass)) showOverlay();
+
+        setTimeout(() => beforeOffcanvasShow(self), overlayDelay);
+      } else beforeOffcanvasShow(self);
+    }
+
+    hide(force) {
+      const self = this;
+      const { element, isAnimating, relatedTarget } = self;
+
+      if (!self.open || isAnimating) return;
+
+      hideOffcanvasEvent.relatedTarget = relatedTarget || null;
+      element.dispatchEvent(hideOffcanvasEvent);
+      if (hideOffcanvasEvent.defaultPrevented) return;
+
+      self.isAnimating = true;
+      addClass(element, offcanvasTogglingClass);
+      removeClass(element, showClass);
+
+      if (!force) {
+        emulateTransitionEnd(element, () => beforeOffcanvasHide(self));
+      } else beforeOffcanvasHide(self);
+    }
+
+    dispose() {
+      const self = this;
+      self.hide(1);
+      toggleOffcanvasEvents(self);
+      super.dispose(offcanvasComponent);
+    }
+  }
+
+  Offcanvas.init = {
+    component: offcanvasComponent,
+    selector: OffcanvasSelector,
+    constructor: Offcanvas,
+  };
+
+  const ariaDescribedBy = 'aria-describedby';
+
+  var tipClassPositions = {
+    top: 'top', bottom: 'bottom', left: 'start', right: 'end',
+  };
+
+  function isVisibleTip(tip, container) {
+    return container.contains(tip);
+  }
+
+  function isMedia(element) {
+    return [SVGElement, HTMLImageElement, HTMLVideoElement]
+      .some((mediaType) => element instanceof mediaType);
+  }
+
+  function closestRelative(element) {
+    let retval = null;
+    let el = element;
+    while (el !== document.body) {
+      el = el.parentElement;
+      if (getComputedStyle(el).position === 'relative') {
+        retval = el;
+        break;
+      }
+    }
+    return retval;
+  }
+
+  // both popovers and tooltips (this, event)
+  function styleTip(self, e) {
+    const tipClasses = /\b(top|bottom|start|end)+/;
+    const tip = self.tooltip || self.popover;
+    // reset tip style
+    tip.style.top = '';
+    tip.style.left = '';
+    tip.style.right = '';
+    // continue with metrics
+    const isPopover = !!self.popover;
+    let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
+    const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
+    const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
+    const { element, options, arrow } = self;
+    let { container, placement } = options;
+    let parentIsBody = container === document.body;
+    const targetPosition = getComputedStyle(element).position;
+    const parentPosition = getComputedStyle(container).position;
+    const staticParent = !parentIsBody && parentPosition === 'static';
+    let relativeParent = !parentIsBody && parentPosition === 'relative';
+    const relContainer = staticParent && closestRelative(container);
+    // static containers should refer to another relative container or the body
+    container = relContainer || container;
+    relativeParent = staticParent && relContainer ? 1 : relativeParent;
+    parentIsBody = container === document.body;
+    const parentRect = container.getBoundingClientRect();
+    const leftBoundry = relativeParent ? parentRect.left : 0;
+    const rightBoundry = relativeParent ? parentRect.right : windowWidth;
+    // this case should not be possible
+    // absoluteParent = !parentIsBody && parentPosition === 'absolute',
+    // this case requires a container with placement: relative
+    const absoluteTarget = targetPosition === 'absolute';
+    const targetRect = element.getBoundingClientRect();
+    const scroll = parentIsBody
+      ? { x: window.pageXOffset, y: window.pageYOffset }
+      : { x: container.scrollLeft, y: container.scrollTop };
+    const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
+    const top = relativeParent ? element.offsetTop : targetRect.top;
+    const left = relativeParent ? element.offsetLeft : targetRect.left;
+    // reset arrow style
+    arrow.style.top = '';
+    arrow.style.left = '';
+    arrow.style.right = '';
+    let topPosition;
+    let leftPosition;
+    let rightPosition;
+    let arrowTop;
+    let arrowLeft;
+    let arrowRight;
+
+    // check placement
+    let topExceed = targetRect.top - tipDimensions.h < 0;
+    let bottomExceed = targetRect.top + tipDimensions.h + elemDimensions.h >= windowHeight;
+    let leftExceed = targetRect.left - tipDimensions.w < leftBoundry;
+    let rightExceed = targetRect.left + tipDimensions.w + elemDimensions.w >= rightBoundry;
+
+    topExceed = ['left', 'right'].includes(placement)
+      ? targetRect.top + elemDimensions.h / 2 - tipDimensions.h / 2 < 0
+      : topExceed;
+    bottomExceed = ['left', 'right'].includes(placement)
+      ? targetRect.top + tipDimensions.h / 2 + elemDimensions.h / 2 >= windowHeight
+      : bottomExceed;
+    leftExceed = ['top', 'bottom'].includes(placement)
+      ? targetRect.left + elemDimensions.w / 2 - tipDimensions.w / 2 < leftBoundry
+      : leftExceed;
+    rightExceed = ['top', 'bottom'].includes(placement)
+      ? targetRect.left + tipDimensions.w / 2 + elemDimensions.w / 2 >= rightBoundry
+      : rightExceed;
+
+    // recompute placement
+    // first, when both left and right limits are exceeded, we fall back to top|bottom
+    placement = (['left', 'right'].includes(placement)) && leftExceed && rightExceed ? 'top' : placement;
+    placement = placement === 'top' && topExceed ? 'bottom' : placement;
+    placement = placement === 'bottom' && bottomExceed ? 'top' : placement;
+    placement = placement === 'left' && leftExceed ? 'right' : placement;
+    placement = placement === 'right' && rightExceed ? 'left' : placement;
+
+    // update tooltip/popover class
+    if (!tip.className.includes(placement)) {
+      tip.className = tip.className.replace(tipClasses, tipClassPositions[placement]);
+    }
+    // if position has changed, update tip dimensions
+    tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
+
+    // we check the computed width & height and update here
+    const arrowWidth = arrow.offsetWidth || 0;
+    const arrowHeight = arrow.offsetHeight || 0;
+    const arrowAdjust = arrowWidth / 2;
+
+    // compute tooltip / popover coordinates
+    if (['left', 'right'].includes(placement)) { // secondary|side positions
+      if (placement === 'left') { // LEFT
+        leftPosition = left + scroll.x - tipDimensions.w - (isPopover ? arrowWidth : 0);
+      } else { // RIGHT
+        leftPosition = left + scroll.x + elemDimensions.w + (isPopover ? arrowWidth : 0);
+      }
+
+      // adjust top and arrow
+      if (topExceed) {
+        topPosition = top + scroll.y;
+        arrowTop = elemDimensions.h / 2 - arrowWidth;
+      } else if (bottomExceed) {
+        topPosition = top + scroll.y - tipDimensions.h + elemDimensions.h;
+        arrowTop = tipDimensions.h - elemDimensions.h / 2 - arrowWidth;
+      } else {
+        topPosition = top + scroll.y - tipDimensions.h / 2 + elemDimensions.h / 2;
+        arrowTop = tipDimensions.h / 2 - arrowHeight / 2;
+      }
+    } else if (['top', 'bottom'].includes(placement)) {
+      if (e && isMedia(element)) {
+        const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
+        const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0);
+
+        if (placement === 'top') {
+          topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
+        } else {
+          topPosition = eY + arrowHeight;
+        }
+
+        // adjust left | right and also the arrow
+        if (e.clientX - tipDimensions.w / 2 < leftBoundry) { // when exceeds left
+          leftPosition = 0;
+          arrowLeft = eX - arrowAdjust;
+        } else if (e.clientX + tipDimensions.w * 0.51 >= rightBoundry) { // when exceeds right
+          leftPosition = 'auto';
+          rightPosition = 0;
+          arrowLeft = tipDimensions.w - (rightBoundry - eX) - arrowAdjust;
+        } else { // normal top/bottom
+          leftPosition = eX - tipDimensions.w / 2;
+          arrowLeft = tipDimensions.w / 2 - arrowAdjust;
+        }
+      } else {
+        if (placement === 'top') {
+          topPosition = top + scroll.y - tipDimensions.h - (isPopover ? arrowHeight : 0);
+        } else { // BOTTOM
+          topPosition = top + scroll.y + elemDimensions.h + (isPopover ? arrowHeight : 0);
+        }
+
+        // adjust left | right and also the arrow
+        if (leftExceed) {
+          leftPosition = 0;
+          arrowLeft = left + elemDimensions.w / 2 - arrowAdjust;
+        } else if (rightExceed) {
+          leftPosition = 'auto';
+          rightPosition = 0;
+          arrowRight = elemDimensions.w / 2 + (parentRect.right - targetRect.right) - arrowAdjust;
+        } else {
+          leftPosition = left + scroll.x - tipDimensions.w / 2 + elemDimensions.w / 2;
+          arrowLeft = tipDimensions.w / 2 - arrowAdjust;
+        }
+      }
+    }
+
+    // apply style to tooltip/popover and its arrow
+    tip.style.top = `${topPosition}px`;
+    tip.style.left = leftPosition === 'auto' ? leftPosition : `${leftPosition}px`;
+    tip.style.right = rightPosition !== undefined ? `${rightPosition}px` : '';
+    // update arrow placement or clear side
+    if (arrowTop !== undefined) {
+      arrow.style.top = `${arrowTop}px`;
+    }
+
+    if (arrowLeft !== undefined) {
+      arrow.style.left = `${arrowLeft}px`;
+    } else if (arrowRight !== undefined) {
+      arrow.style.right = `${arrowRight}px`;
+    }
+  }
+
+  let bsnUID = 1;
+
+  // popover, tooltip, scrollspy need a unique id
+  function getUID(element, key) {
+    bsnUID += 1;
+    return element[key] || bsnUID;
+  }
+
+  function getTipContainer(element) {
+    // maybe the element is inside a modal
+    const modal = element.closest('.modal');
+
+    // OR maybe the element is inside a fixed navbar
+    const navbarFixed = element.closest(`.${fixedTopClass},.${fixedBottomClass}`);
+
+    // set default container option appropriate for the context
+    return modal || navbarFixed || document.body;
+  }
+
+  /* Native JavaScript for Bootstrap 5 | Popover
+  ---------------------------------------------- */
+
+  // POPOVER PRIVATE GC
+  // ==================
+  const popoverString = 'popover';
+  const popoverComponent = 'Popover';
+  const popoverSelector = `[${dataBsToggle}="${popoverString}"],[data-tip="${popoverString}"]`;
+  const popoverDefaultOptions = {
+    template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
+    title: null, // string
+    content: null, // string
+    sanitizeFn: null, // function
+    customClass: null, // string
+    dismissible: false, // boolean
+    animation: true, // boolean
+    trigger: 'hover', // string
+    placement: 'top', // string
+    delay: 200, // number
+  };
+
+  // POPOVER PRIVATE GC
+  // ==================
+  const appleBrands = /(iPhone|iPod|iPad)/;
+  const isIphone = navigator.userAgentData
+    ? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
+    : appleBrands.test(navigator.userAgent);
+  // popoverArrowClass = `${popoverString}-arrow`,
+  const popoverHeaderClass = `${popoverString}-header`;
+  const popoverBodyClass = `${popoverString}-body`;
+  // close btn for dissmissible popover
+  let popoverCloseButton = '<button type="button" class="btn-close"></button>';
+
+  // POPOVER CUSTOM EVENTS
+  // =====================
+  const showPopoverEvent = bootstrapCustomEvent(`show.bs.${popoverString}`);
+  const shownPopoverEvent = bootstrapCustomEvent(`shown.bs.${popoverString}`);
+  const hidePopoverEvent = bootstrapCustomEvent(`hide.bs.${popoverString}`);
+  const hiddenPopoverEvent = bootstrapCustomEvent(`hidden.bs.${popoverString}`);
+
+  // POPOVER EVENT HANDLERS
+  // ======================
+  function popoverForceFocus() {
+    setFocus(this);
+  }
+
+  function popoverTouchHandler({ target }) {
+    const self = this;
+    const { popover, element } = self;
+
+    if ((popover && popover.contains(target)) // popover includes touch target
+      || target === element // OR touch target is element
+      || element.contains(target)) ; else {
+      self.hide();
+    }
+  }
+
+  // POPOVER PRIVATE METHODS
+  // =======================
+  function createPopover(self) {
+    const { id, options } = self;
+    const {
+      animation, customClass, sanitizeFn, placement, dismissible,
+    } = options;
+    let { title, content, template } = options;
+
+    // set initial popover class
+    const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
+
+    // fixing #233
+    title = title ? title.trim() : null;
+    content = content ? content.trim() : null;
+
+    // sanitize title && content
+    if (sanitizeFn) {
+      title = title ? sanitizeFn(title) : null;
+      content = content ? sanitizeFn(content) : null;
+      template = template ? sanitizeFn(template) : null;
+      popoverCloseButton = sanitizeFn(popoverCloseButton);
+    }
+
+    self.popover = document.createElement('div');
+    const { popover } = self;
+
+    // set id and aria-describedby
+    popover.setAttribute('id', id);
+    popover.setAttribute('role', 'tooltip');
+
+    // load template
+    const popoverTemplate = document.createElement('div');
+    popoverTemplate.innerHTML = template.trim();
+    popover.className = popoverTemplate.firstChild.className;
+    popover.innerHTML = popoverTemplate.firstChild.innerHTML;
+
+    const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
+    const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
+
+    // set arrow
+    self.arrow = queryElement(`.${popoverString}-arrow`, popover);
+
+    // set dismissible button
+    if (dismissible) {
+      title = title ? title + popoverCloseButton : title;
+      content = title === null ? +popoverCloseButton : content;
+    }
+
+    // fill the template with content from data attributes
+    if (title && popoverHeader) popoverHeader.innerHTML = title.trim();
+    if (content && popoverBody) popoverBody.innerHTML = content.trim();
+
+    // set popover animation and placement
+    if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
+    if (animation && !hasClass(popover, fadeClass)) addClass(popover, fadeClass);
+    if (customClass && !hasClass(popover, customClass)) {
+      addClass(popover, customClass);
+    }
+    if (!hasClass(popover, placementClass)) addClass(popover, placementClass);
+  }
+
+  function removePopover(self) {
+    const { element, popover, options } = self;
+    element.removeAttribute(ariaDescribedBy);
+    options.container.removeChild(popover);
+    self.timer = null;
+  }
+
+  function togglePopoverHandlers(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    const { element, options } = self;
+    const { trigger, dismissible } = options;
+    self.enabled = !!add;
+
+    if (trigger === 'hover') {
+      element[action]('mousedown', self.show);
+      element[action]('mouseenter', self.show);
+      if (isMedia(element)) element[action]('mousemove', self.update, passiveHandler);
+      if (!dismissible) element[action]('mouseleave', self.hide);
+    } else if (trigger === 'click') {
+      element[action](trigger, self.toggle);
+    } else if (trigger === 'focus') {
+      if (isIphone) element[action]('click', popoverForceFocus);
+      element[action]('focusin', self.show);
+    }
+  }
+
+  function dismissHandlerToggle(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    const { options, element, popover } = self;
+    const { trigger, dismissible } = options;
+
+    if (dismissible) {
+      const [btnClose] = popover.getElementsByClassName('btn-close');
+      if (btnClose) btnClose[action]('click', self.hide);
+    } else {
+      if (trigger === 'focus') element[action]('focusout', self.hide);
+      if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
+    }
+
+    if (!isMedia(element)) {
+      window[action]('scroll', self.update, passiveHandler);
+      window[action]('resize', self.update, passiveHandler);
+    }
+  }
+
+  function popoverShowTrigger(self) {
+    dismissHandlerToggle(self, 1);
+    self.element.dispatchEvent(shownPopoverEvent);
+  }
+
+  function popoverHideTrigger(self) {
+    dismissHandlerToggle(self);
+    removePopover(self);
+    self.element.dispatchEvent(hiddenPopoverEvent);
+  }
+
+  // POPOVER DEFINITION
+  // ==================
+  class Popover extends BaseComponent {
+    constructor(target, config) {
+      popoverDefaultOptions.container = getTipContainer(queryElement(target));
+      super(popoverComponent, target, popoverDefaultOptions, config);
+
+      // bind
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+      // additional instance properties
+      self.timer = null;
+      self.popover = null;
+      self.arrow = null;
+      self.enabled = false;
+      // set unique ID for aria-describedby
+      self.id = `${popoverString}-${getUID(element)}`;
+
+      // set instance options
+      const { options } = self;
+
+      // media elements only work with body as a container
+      self.options.container = isMedia(element)
+        ? popoverDefaultOptions.container
+        : queryElement(options.container);
+
+      // reset default container
+      popoverDefaultOptions.container = null;
+
+      // invalidate when no content is set
+      if (!options.content) return;
+
+      // crate popover
+      createPopover(self);
+
+      // bind
+      self.update = self.update.bind(self);
+
+      // attach event listeners
+      togglePopoverHandlers(self, 1);
+    }
+
+    update(e) {
+      styleTip(this, e);
+    }
+
+    // POPOVER PUBLIC METHODS
+    // ======================
+    toggle(e) {
+      const self = e ? this[popoverComponent] : this;
+      const { popover, options } = self;
+      if (!isVisibleTip(popover, options.container)) self.show();
+      else self.hide();
+    }
+
+    show(e) {
+      const self = e ? this[popoverComponent] : this;
+      const {
+        element, popover, options, id,
+      } = self;
+      const { container } = options;
+
+      clearTimeout(self.timer);
+
+      self.timer = setTimeout(() => {
+        if (!isVisibleTip(popover, container)) {
+          element.dispatchEvent(showPopoverEvent);
+          if (showPopoverEvent.defaultPrevented) return;
+
+          // append to the container
+          container.appendChild(popover);
+          element.setAttribute(ariaDescribedBy, id);
+
+          self.update(e);
+          if (!hasClass(popover, showClass)) addClass(popover, showClass);
+
+          if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
+          else popoverShowTrigger(self);
+        }
+      }, 17);
+    }
+
+    hide(e) {
+      let self;
+      if (e && this[popoverComponent]) {
+        self = this[popoverComponent];
+      } else if (e) { // dismissible popover
+        const dPopover = this.closest(`.${popoverString}`);
+        const dEl = dPopover && queryElement(`[${ariaDescribedBy}="${dPopover.id}"]`);
+        self = dEl[popoverComponent];
+      } else {
+        self = this;
+      }
+      const { element, popover, options } = self;
+
+      clearTimeout(self.timer);
+
+      self.timer = setTimeout(() => {
+        if (isVisibleTip(popover, options.container)) {
+          element.dispatchEvent(hidePopoverEvent);
+          if (hidePopoverEvent.defaultPrevented) return;
+
+          removeClass(popover, showClass);
+
+          if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
+          else popoverHideTrigger(self);
+        }
+      }, options.delay + 17);
+    }
+
+    enable() {
+      const self = this;
+      const { enabled } = self;
+      if (!enabled) {
+        togglePopoverHandlers(self, 1);
+        self.enabled = !enabled;
+      }
+    }
+
+    disable() {
+      const self = this;
+      const { enabled, popover, options } = self;
+      if (enabled) {
+        if (isVisibleTip(popover, options.container) && options.animation) {
+          self.hide();
+
+          setTimeout(
+            () => togglePopoverHandlers(self),
+            getElementTransitionDuration(popover) + options.delay + 17,
+          );
+        } else {
+          togglePopoverHandlers(self);
+        }
+        self.enabled = !enabled;
+      }
+    }
+
+    toggleEnabled() {
+      const self = this;
+      if (!self.enabled) self.enable();
+      else self.disable();
+    }
+
+    dispose() {
+      const self = this;
+      const { popover, options } = self;
+      const { container, animation } = options;
+      if (animation && isVisibleTip(popover, container)) {
+        options.delay = 0; // reset delay
+        self.hide();
+        emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
+      } else {
+        togglePopoverHandlers(self);
+      }
+      super.dispose(popoverComponent);
+    }
+  }
+
+  Popover.init = {
+    component: popoverComponent,
+    selector: popoverSelector,
+    constructor: Popover,
+  };
+
+  /* Native JavaScript for Bootstrap 5 | ScrollSpy
+  ------------------------------------------------ */
+
+  // SCROLLSPY PRIVATE GC
+  // ====================
+  const scrollspyString = 'scrollspy';
+  const scrollspyComponent = 'ScrollSpy';
+  const scrollspySelector = '[data-bs-spy="scroll"]';
+  const scrollSpyDefaultOptions = {
+    offset: 10,
+    target: null,
+  };
+
+  // SCROLLSPY CUSTOM EVENT
+  // ======================
+  const activateScrollSpy = bootstrapCustomEvent(`activate.bs.${scrollspyString}`);
+
+  // SCROLLSPY PRIVATE METHODS
+  // =========================
+  function updateSpyTargets(self) {
+    const {
+      target, scrollTarget, isWindow, options, itemsLength, scrollHeight,
+    } = self;
+    const { offset } = options;
+    const links = target.getElementsByTagName('A');
+
+    self.scrollTop = isWindow
+      ? scrollTarget.pageYOffset
+      : scrollTarget.scrollTop;
+
+    // only update items/offsets once or with each mutation
+    if (itemsLength !== links.length || getScrollHeight(scrollTarget) !== scrollHeight) {
+      let href;
+      let targetItem;
+      let rect;
+
+      // reset arrays & update
+      self.items = [];
+      self.offsets = [];
+      self.scrollHeight = getScrollHeight(scrollTarget);
+      self.maxScroll = self.scrollHeight - getOffsetHeight(self);
+
+      Array.from(links).forEach((link) => {
+        href = link.getAttribute('href');
+        targetItem = href && href.charAt(0) === '#' && href.slice(-1) !== '#' && queryElement(href);
+
+        if (targetItem) {
+          self.items.push(link);
+          rect = targetItem.getBoundingClientRect();
+          self.offsets.push((isWindow ? rect.top + self.scrollTop : targetItem.offsetTop) - offset);
+        }
+      });
+      self.itemsLength = self.items.length;
+    }
+  }
+
+  function getScrollHeight(scrollTarget) {
+    return scrollTarget.scrollHeight || Math.max(
+      document.body.scrollHeight,
+      document.documentElement.scrollHeight,
+    );
+  }
+
+  function getOffsetHeight({ element, isWindow }) {
+    if (!isWindow) return element.getBoundingClientRect().height;
+    return window.innerHeight;
+  }
+
+  function clear(target) {
+    Array.from(target.getElementsByTagName('A')).forEach((item) => {
+      if (hasClass(item, activeClass)) removeClass(item, activeClass);
+    });
+  }
+
+  function activate(self, item) {
+    const { target, element } = self;
+    clear(target);
+    self.activeItem = item;
+    addClass(item, activeClass);
+
+    // activate all parents
+    const parents = [];
+    let parentItem = item;
+    while (parentItem !== document.body) {
+      parentItem = parentItem.parentNode;
+      if (hasClass(parentItem, 'nav') || hasClass(parentItem, 'dropdown-menu')) parents.push(parentItem);
+    }
+
+    parents.forEach((menuItem) => {
+      const parentLink = menuItem.previousElementSibling;
+
+      if (parentLink && !hasClass(parentLink, activeClass)) {
+        addClass(parentLink, activeClass);
+      }
+    });
+
+    // update relatedTarget and dispatch
+    activateScrollSpy.relatedTarget = item;
+    element.dispatchEvent(activateScrollSpy);
+  }
+
+  function toggleSpyHandlers(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    self.scrollTarget[action]('scroll', self.refresh, passiveHandler);
+  }
+
+  // SCROLLSPY DEFINITION
+  // ====================
+  class ScrollSpy extends BaseComponent {
+    constructor(target, config) {
+      super(scrollspyComponent, target, scrollSpyDefaultOptions, config);
+      // bind
+      const self = this;
+
+      // initialization element & options
+      const { element, options } = self;
+
+      // additional properties
+      self.target = queryElement(options.target);
+
+      // invalidate
+      if (!self.target) return;
+
+      // set initial state
+      self.scrollTarget = element.clientHeight < element.scrollHeight ? element : window;
+      self.isWindow = self.scrollTarget === window;
+      self.scrollTop = 0;
+      self.maxScroll = 0;
+      self.scrollHeight = 0;
+      self.activeItem = null;
+      self.items = [];
+      self.offsets = [];
+
+      // bind events
+      self.refresh = self.refresh.bind(self);
+
+      // add event handlers
+      toggleSpyHandlers(self, 1);
+
+      self.refresh();
+    }
+
+    // SCROLLSPY PUBLIC METHODS
+    // ========================
+    refresh() {
+      const self = this;
+      const { target } = self;
+
+      // check if target is visible and invalidate
+      if (target.offsetHeight === 0) return;
+
+      updateSpyTargets(self);
+
+      const {
+        scrollTop, maxScroll, itemsLength, items, activeItem,
+      } = self;
+
+      if (scrollTop >= maxScroll) {
+        const newActiveItem = items[itemsLength - 1];
+
+        if (activeItem !== newActiveItem) {
+          activate(self, newActiveItem);
         }
-        var popoverBodyMarkup = document.createElement('div');
-        popoverBodyMarkup.classList.add('popover-body');
-        popoverBodyMarkup.innerHTML = ops.dismissible && titleString === null ? contentString + closeBtn : contentString;
-        popover.appendChild(popoverBodyMarkup);
-      } else {
-        var popoverTemplate = document.createElement('div');
-        popoverTemplate.innerHTML = ops.template.trim();
-        popover.className = popoverTemplate.firstChild.className;
-        popover.innerHTML = popoverTemplate.firstChild.innerHTML;
-        var popoverHeader = queryElement('.popover-header',popover),
-            popoverBody = queryElement('.popover-body',popover);
-        titleString && popoverHeader && (popoverHeader.innerHTML = titleString.trim());
-        contentString && popoverBody && (popoverBody.innerHTML = contentString.trim());
+        return;
       }
-      ops.container.appendChild(popover);
-      popover.style.display = 'block';
-      !popover.classList.contains( 'popover') && popover.classList.add('popover');
-      !popover.classList.contains( ops.animation) && popover.classList.add(ops.animation);
-      !popover.classList.contains( placementClass) && popover.classList.add(placementClass);
-    }
-    function showPopover() {
-      !popover.classList.contains('show') && ( popover.classList.add('show') );
-    }
-    function updatePopover() {
-      styleTip(element, popover, ops.placement, ops.container);
-    }
-    function forceFocus () {
-      if (popover === null) { element.focus(); }
-    }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      if (ops.trigger === 'hover') {
-        element[action]( mouseClickEvents.down, self.show );
-        element[action]( mouseHoverEvents[0], self.show );
-        if (!ops.dismissible) { element[action]( mouseHoverEvents[1], self.hide ); }
-      } else if ('click' == ops.trigger) {
-        element[action]( ops.trigger, self.toggle );
-      } else if ('focus' == ops.trigger) {
-        isIphone && element[action]( 'click', forceFocus, false );
-        element[action]( ops.trigger, self.toggle );
+
+      const { offsets } = self;
+
+      if (activeItem && scrollTop < offsets[0] && offsets[0] > 0) {
+        self.activeItem = null;
+        clear(target);
+        return;
       }
+
+      items.forEach((item, i) => {
+        if (activeItem !== item && scrollTop >= offsets[i]
+          && (typeof offsets[i + 1] === 'undefined' || scrollTop < offsets[i + 1])) {
+          activate(self, item);
+        }
+      });
     }
-    function touchHandler(e){
-      if ( popover && popover.contains(e.target) || e.target === element || element.contains(e.target)) ; else {
-        self.hide();
-      }
+
+    dispose() {
+      toggleSpyHandlers(this);
+      super.dispose(scrollspyComponent);
     }
-    function dismissHandlerToggle(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      if (ops.dismissible) {
-        document[action]('click', dismissibleHandler, false );
+  }
+
+  ScrollSpy.init = {
+    component: scrollspyComponent,
+    selector: scrollspySelector,
+    constructor: ScrollSpy,
+  };
+
+  const ariaSelected = 'aria-selected';
+
+  /* Native JavaScript for Bootstrap 5 | Tab
+  ------------------------------------------ */
+
+  // TAB PRIVATE GC
+  // ================
+  const tabString = 'tab';
+  const tabComponent = 'Tab';
+  const tabSelector = `[${dataBsToggle}="${tabString}"]`;
+
+  // TAB CUSTOM EVENTS
+  // =================
+  const showTabEvent = bootstrapCustomEvent(`show.bs.${tabString}`);
+  const shownTabEvent = bootstrapCustomEvent(`shown.bs.${tabString}`);
+  const hideTabEvent = bootstrapCustomEvent(`hide.bs.${tabString}`);
+  const hiddenTabEvent = bootstrapCustomEvent(`hidden.bs.${tabString}`);
+
+  let nextTab;
+  let nextTabContent;
+  let nextTabHeight;
+  let activeTab;
+  let activeTabContent;
+  let tabContainerHeight;
+  let tabEqualContents;
+
+  // TAB PRIVATE METHODS
+  // ===================
+  function triggerTabEnd(self) {
+    const { tabContent, nav } = self;
+    tabContent.style.height = '';
+    removeClass(tabContent, collapsingClass);
+    nav.isAnimating = false;
+  }
+
+  function triggerTabShow(self) {
+    const { tabContent, nav } = self;
+
+    if (tabContent) { // height animation
+      if (tabEqualContents) {
+        triggerTabEnd(self);
       } else {
-        'focus' == ops.trigger && element[action]( 'blur', self.hide );
-        'hover' == ops.trigger && document[action]( 'touchstart', touchHandler, passiveHandler );
+        setTimeout(() => { // enables height animation
+          tabContent.style.height = `${nextTabHeight}px`; // height animation
+          reflow(tabContent);
+          emulateTransitionEnd(tabContent, () => triggerTabEnd(self));
+        }, 50);
       }
-      window[action]('resize', self.hide, passiveHandler );
+    } else {
+      nav.isAnimating = false;
     }
-    function showTrigger() {
-      dismissHandlerToggle(1);
-      dispatchCustomEvent.call(element, shownCustomEvent);
+    shownTabEvent.relatedTarget = activeTab;
+    nextTab.dispatchEvent(shownTabEvent);
+  }
+
+  function triggerTabHide(self) {
+    const { tabContent } = self;
+    if (tabContent) {
+      activeTabContent.style.float = 'left';
+      nextTabContent.style.float = 'left';
+      tabContainerHeight = activeTabContent.scrollHeight;
     }
-    function hideTrigger() {
-      dismissHandlerToggle();
-      removePopover();
-      dispatchCustomEvent.call(element, hiddenCustomEvent);
+
+    // update relatedTarget and dispatch event
+    showTabEvent.relatedTarget = activeTab;
+    hiddenTabEvent.relatedTarget = nextTab;
+    nextTab.dispatchEvent(showTabEvent);
+    if (showTabEvent.defaultPrevented) return;
+
+    addClass(nextTabContent, activeClass);
+    removeClass(activeTabContent, activeClass);
+
+    if (tabContent) {
+      nextTabHeight = nextTabContent.scrollHeight;
+      tabEqualContents = nextTabHeight === tabContainerHeight;
+      addClass(tabContent, collapsingClass);
+      tabContent.style.height = `${tabContainerHeight}px`; // height animation
+      reflow(tabContent);
+      activeTabContent.style.float = '';
+      nextTabContent.style.float = '';
     }
-    self.toggle = function () {
-      if (popover === null) { self.show(); }
-      else { self.hide(); }
-    };
-    self.show = function () {
-      clearTimeout(timer);
-      timer = setTimeout( function () {
-        if (popover === null) {
-          dispatchCustomEvent.call(element, showCustomEvent);
-          if ( showCustomEvent.defaultPrevented ) { return; }
-          createPopover();
-          updatePopover();
-          showPopover();
-          !!ops.animation ? emulateTransitionEnd(popover, showTrigger) : showTrigger();
-        }
-      }, 20 );
-    };
-    self.hide = function () {
-      clearTimeout(timer);
-      timer = setTimeout( function () {
-        if (popover && popover !== null && popover.classList.contains('show')) {
-          dispatchCustomEvent.call(element, hideCustomEvent);
-          if ( hideCustomEvent.defaultPrevented ) { return; }
-          popover.classList.remove('show');
-          !!ops.animation ? emulateTransitionEnd(popover, hideTrigger) : hideTrigger();
-        }
-      }, ops.delay );
-    };
-    self.dispose = function () {
-      self.hide();
-      toggleEvents();
-      delete element.Popover;
-    };
-    element = queryElement(element);
-    element.Popover && element.Popover.dispose();
-    triggerData = element.getAttribute('data-trigger');
-    animationData = element.getAttribute('data-animation');
-    placementData = element.getAttribute('data-placement');
-    dismissibleData = element.getAttribute('data-dismissible');
-    delayData = element.getAttribute('data-delay');
-    containerData = element.getAttribute('data-container');
-    closeBtn = '<button type="button" class="close">×</button>';
-    showCustomEvent = bootstrapCustomEvent('show', 'popover');
-    shownCustomEvent = bootstrapCustomEvent('shown', 'popover');
-    hideCustomEvent = bootstrapCustomEvent('hide', 'popover');
-    hiddenCustomEvent = bootstrapCustomEvent('hidden', 'popover');
-    containerElement = queryElement(options.container);
-    containerDataElement = queryElement(containerData);
-    modal = element.closest('.modal');
-    navbarFixedTop = element.closest('.fixed-top');
-    navbarFixedBottom = element.closest('.fixed-bottom');
-    ops.template = options.template ? options.template : null;
-    ops.trigger = options.trigger ? options.trigger : triggerData || 'hover';
-    ops.animation = options.animation && options.animation !== 'fade' ? options.animation : animationData || 'fade';
-    ops.placement = options.placement ? options.placement : placementData || 'top';
-    ops.delay = parseInt(options.delay || delayData) || 200;
-    ops.dismissible = options.dismissible || dismissibleData === 'true' ? true : false;
-    ops.container = containerElement ? containerElement
-                            : containerDataElement ? containerDataElement
-                            : navbarFixedTop ? navbarFixedTop
-                            : navbarFixedBottom ? navbarFixedBottom
-                            : modal ? modal : document.body;
-    placementClass = "bs-popover-" + (ops.placement);
-    var popoverContents = getContents();
-    titleString = popoverContents[0];
-    contentString = popoverContents[1];
-    if ( !contentString && !ops.template ) { return; }
-    if ( !element.Popover ) {
-      toggleEvents(1);
-    }
-    element.Popover = self;
-  }
-
-  function ScrollSpy(element,options) {
-    options = options || {};
-    var self = this,
-      vars,
-      targetData,
-      offsetData,
-      spyTarget,
-      scrollTarget,
-      ops = {};
-    function updateTargets(){
-      var links = spyTarget.getElementsByTagName('A');
-      if (vars.length !== links.length) {
-        vars.items = [];
-        vars.targets = [];
-        Array.from(links).map(function (link){
-          var href = link.getAttribute('href'),
-            targetItem = href && href.charAt(0) === '#' && href.slice(-1) !== '#' && queryElement(href);
-          if ( targetItem ) {
-            vars.items.push(link);
-            vars.targets.push(targetItem);
-          }
+
+    if (hasClass(nextTabContent, fadeClass)) {
+      setTimeout(() => {
+        addClass(nextTabContent, showClass);
+        emulateTransitionEnd(nextTabContent, () => {
+          triggerTabShow(self);
         });
-        vars.length = links.length;
-      }
+      }, 20);
+    } else { triggerTabShow(self); }
+
+    activeTab.dispatchEvent(hiddenTabEvent);
+  }
+
+  function getActiveTab({ nav }) {
+    const activeTabs = nav.getElementsByClassName(activeClass);
+
+    if (activeTabs.length === 1
+      && !dropdownMenuClasses.some((c) => hasClass(activeTabs[0].parentNode, c))) {
+      [activeTab] = activeTabs;
+    } else if (activeTabs.length > 1) {
+      activeTab = activeTabs[activeTabs.length - 1];
+    }
+    return activeTab;
+  }
+
+  function getActiveTabContent(self) {
+    return queryElement(getActiveTab(self).getAttribute('href'));
+  }
+
+  function toggleTabHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    self.element[action]('click', tabClickHandler);
+  }
+
+  // TAB EVENT HANDLER
+  // =================
+  function tabClickHandler(e) {
+    const self = this[tabComponent];
+    e.preventDefault();
+    if (!self.nav.isAnimating) self.show();
+  }
+
+  // TAB DEFINITION
+  // ==============
+  class Tab extends BaseComponent {
+    constructor(target) {
+      super(tabComponent, target);
+      // bind
+      const self = this;
+
+      // initialization element
+      const { element } = self;
+
+      // event targets
+      self.nav = element.closest('.nav');
+      const { nav } = self;
+      self.dropdown = nav && queryElement(`.${dropdownMenuClasses[0]}-toggle`, nav);
+      activeTabContent = getActiveTabContent(self);
+      self.tabContent = supportTransition && activeTabContent.closest('.tab-content');
+      tabContainerHeight = activeTabContent.scrollHeight;
+
+      // set default animation state
+      nav.isAnimating = false;
+
+      // add event listener
+      toggleTabHandler(self, 1);
     }
-    function updateItem(index) {
-      var item = vars.items[index],
-        targetItem = vars.targets[index],
-        dropmenu = item.classList.contains('dropdown-item') && item.closest('.dropdown-menu'),
-        dropLink = dropmenu && dropmenu.previousElementSibling,
-        nextSibling = item.nextElementSibling,
-        activeSibling = nextSibling && nextSibling.getElementsByClassName('active').length,
-        targetRect = vars.isWindow && targetItem.getBoundingClientRect(),
-        isActive = item.classList.contains('active') || false,
-        topEdge = (vars.isWindow ? targetRect.top + vars.scrollOffset : targetItem.offsetTop) - ops.offset,
-        bottomEdge = vars.isWindow ? targetRect.bottom + vars.scrollOffset - ops.offset
-                   : vars.targets[index+1] ? vars.targets[index+1].offsetTop - ops.offset
-                   : element.scrollHeight,
-        inside = activeSibling || vars.scrollOffset >= topEdge && bottomEdge > vars.scrollOffset;
-       if ( !isActive && inside ) {
-        item.classList.add('active');
-        if (dropLink && !dropLink.classList.contains('active') ) {
-          dropLink.classList.add('active');
+
+    // TAB PUBLIC METHODS
+    // ==================
+    show() { // the tab we clicked is now the nextTab tab
+      const self = this;
+      const { element, nav, dropdown } = self;
+      nextTab = element;
+      if (!hasClass(nextTab, activeClass)) {
+        // this is the actual object, the nextTab tab content to activate
+        nextTabContent = queryElement(nextTab.getAttribute('href'));
+        activeTab = getActiveTab({ nav });
+        activeTabContent = getActiveTabContent({ nav });
+
+        // update relatedTarget and dispatch
+        hideTabEvent.relatedTarget = nextTab;
+        activeTab.dispatchEvent(hideTabEvent);
+        if (hideTabEvent.defaultPrevented) return;
+
+        nav.isAnimating = true;
+        removeClass(activeTab, activeClass);
+        activeTab.setAttribute(ariaSelected, 'false');
+        addClass(nextTab, activeClass);
+        nextTab.setAttribute(ariaSelected, 'true');
+
+        if (dropdown) {
+          if (!hasClass(element.parentNode, dropdownMenuClass)) {
+            if (hasClass(dropdown, activeClass)) removeClass(dropdown, activeClass);
+          } else if (!hasClass(dropdown, activeClass)) addClass(dropdown, activeClass);
         }
-        dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', { relatedTarget: vars.items[index] }));
-      } else if ( isActive && !inside ) {
-        item.classList.remove('active');
-        if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) {
-          dropLink.classList.remove('active');
+
+        if (hasClass(activeTabContent, fadeClass)) {
+          removeClass(activeTabContent, showClass);
+          emulateTransitionEnd(activeTabContent, () => triggerTabHide(self));
+        } else {
+          triggerTabHide(self);
         }
-      } else if ( isActive && inside || !inside && !isActive ) {
-        return;
       }
     }
-    function updateItems() {
-      updateTargets();
-      vars.scrollOffset = vars.isWindow ? getScroll().y : element.scrollTop;
-      vars.items.map(function (l,idx){ return updateItem(idx); });
+
+    dispose() {
+      toggleTabHandler(this);
+      super.dispose(tabComponent);
     }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      scrollTarget[action]('scroll', self.refresh, passiveHandler );
-      window[action]( 'resize', self.refresh, passiveHandler );
+  }
+
+  Tab.init = {
+    component: tabComponent,
+    selector: tabSelector,
+    constructor: Tab,
+  };
+
+  /* Native JavaScript for Bootstrap 5 | Toast
+  -------------------------------------------- */
+
+  // TOAST PRIVATE GC
+  // ================
+  const toastString = 'toast';
+  const toastComponent = 'Toast';
+  const toastSelector = `.${toastString}`;
+  const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
+  const showingClass = 'showing';
+  const hideClass = 'hide';
+  const toastDefaultOptions = {
+    animation: true,
+    autohide: true,
+    delay: 500,
+  };
+
+  // TOAST CUSTOM EVENTS
+  // ===================
+  const showToastEvent = bootstrapCustomEvent(`show.bs.${toastString}`);
+  const hideToastEvent = bootstrapCustomEvent(`hide.bs.${toastString}`);
+  const shownToastEvent = bootstrapCustomEvent(`shown.bs.${toastString}`);
+  const hiddenToastEvent = bootstrapCustomEvent(`hidden.bs.${toastString}`);
+
+  // TOAST PRIVATE METHODS
+  // =====================
+  function showToastComplete(self) {
+    const { element, options } = self;
+    if (!options.animation) {
+      removeClass(element, showingClass);
+      addClass(element, showClass);
     }
-    self.refresh = function () {
-      updateItems();
-    };
-    self.dispose = function () {
-      toggleEvents();
-      delete element.ScrollSpy;
-    };
-    element = queryElement(element);
-    element.ScrollSpy && element.ScrollSpy.dispose();
-    targetData = element.getAttribute('data-target');
-    offsetData = element.getAttribute('data-offset');
-    spyTarget = queryElement(options.target || targetData);
-    scrollTarget = element.offsetHeight < element.scrollHeight ? element : window;
-    if (!spyTarget) { return }
-    ops.target = spyTarget;
-    ops.offset = parseInt(options.offset || offsetData) || 10;
-    vars = {};
-    vars.length = 0;
-    vars.items = [];
-    vars.targets = [];
-    vars.isWindow = scrollTarget === window;
-    if ( !element.ScrollSpy ) {
-      toggleEvents(1);
-    }
-    self.refresh();
-    element.ScrollSpy = self;
-  }
-
-  function Tab(element,options) {
-    options = options || {};
-    var self = this,
-      heightData,
-      tabs, dropdown,
-      showCustomEvent,
-      shownCustomEvent,
-      hideCustomEvent,
-      hiddenCustomEvent,
-      next,
-      tabsContentContainer = false,
-      activeTab,
-      activeContent,
-      nextContent,
-      containerHeight,
-      equalContents,
-      nextHeight,
-      animateHeight;
-    function triggerEnd() {
-      tabsContentContainer.style.height = '';
-      tabsContentContainer.classList.remove('collapsing');
-      tabs.isAnimating = false;
-    }
-    function triggerShow() {
-      if (tabsContentContainer) {
-        if ( equalContents ) {
-          triggerEnd();
-        } else {
-          setTimeout(function () {
-            tabsContentContainer.style.height = nextHeight + "px";
-            tabsContentContainer.offsetWidth;
-            emulateTransitionEnd(tabsContentContainer, triggerEnd);
-          },50);
-        }
-      } else {
-        tabs.isAnimating = false;
-      }
-      shownCustomEvent = bootstrapCustomEvent('shown', 'tab', { relatedTarget: activeTab });
-      dispatchCustomEvent.call(next, shownCustomEvent);
-    }
-    function triggerHide() {
-      if (tabsContentContainer) {
-        activeContent.style.float = 'left';
-        nextContent.style.float = 'left';
-        containerHeight = activeContent.scrollHeight;
-      }
-      showCustomEvent = bootstrapCustomEvent('show', 'tab', { relatedTarget: activeTab });
-      hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', { relatedTarget: next });
-      dispatchCustomEvent.call(next, showCustomEvent);
-      if ( showCustomEvent.defaultPrevented ) { return; }
-      nextContent.classList.add('active');
-      activeContent.classList.remove('active');
-      if (tabsContentContainer) {
-        nextHeight = nextContent.scrollHeight;
-        equalContents = nextHeight === containerHeight;
-        tabsContentContainer.classList.add('collapsing');
-        tabsContentContainer.style.height = containerHeight + "px";
-        tabsContentContainer.offsetHeight;
-        activeContent.style.float = '';
-        nextContent.style.float = '';
+
+    element.dispatchEvent(shownToastEvent);
+    if (options.autohide) self.hide();
+  }
+
+  function hideToastComplete(self) {
+    const { element } = self;
+    addClass(element, hideClass);
+    element.dispatchEvent(hiddenToastEvent);
+  }
+
+  function closeToast(self) {
+    const { element, options } = self;
+    removeClass(element, showClass);
+
+    if (options.animation) {
+      reflow(element);
+      emulateTransitionEnd(element, () => hideToastComplete(self));
+    } else {
+      hideToastComplete(self);
+    }
+  }
+
+  function openToast(self) {
+    const { element, options } = self;
+    removeClass(element, hideClass);
+
+    if (options.animation) {
+      reflow(element);
+      addClass(element, showingClass);
+      addClass(element, showClass);
+
+      emulateTransitionEnd(element, () => showToastComplete(self));
+    } else {
+      showToastComplete(self);
+    }
+  }
+
+  function toggleToastHandler(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    if (self.dismiss) {
+      self.dismiss[action]('click', self.hide);
+    }
+  }
+
+  // TOAST EVENT HANDLERS
+  // ====================
+  function completeDisposeToast(self) {
+    clearTimeout(self.timer);
+    toggleToastHandler(self);
+  }
+
+  // TOAST DEFINITION
+  // ================
+  class Toast extends BaseComponent {
+    constructor(target, config) {
+      super(toastComponent, target, toastDefaultOptions, config);
+      // bind
+      const self = this;
+
+      // dismiss button
+      self.dismiss = queryElement(toastDismissSelector, self.element);
+
+      // bind
+      self.show = self.show.bind(self);
+      self.hide = self.hide.bind(self);
+
+      // add event listener
+      toggleToastHandler(self, 1);
+    }
+
+    // TOAST PUBLIC METHODS
+    // ====================
+    show() {
+      const self = this;
+      const { element } = self;
+      if (element && hasClass(element, hideClass)) {
+        element.dispatchEvent(showToastEvent);
+        if (showToastEvent.defaultPrevented) return;
+
+        addClass(element, fadeClass);
+        clearTimeout(self.timer);
+        self.timer = setTimeout(() => openToast(self), 10);
       }
-      if ( nextContent.classList.contains('fade') ) {
-        setTimeout(function () {
-          nextContent.classList.add('show');
-          emulateTransitionEnd(nextContent,triggerShow);
-        },20);
-      } else { triggerShow(); }
-      dispatchCustomEvent.call(activeTab, hiddenCustomEvent);
-    }
-    function getActiveTab() {
-      var activeTabs = tabs.getElementsByClassName('active'), activeTab;
-      if ( activeTabs.length === 1 && !activeTabs[0].parentNode.classList.contains('dropdown') ) {
-        activeTab = activeTabs[0];
-      } else if ( activeTabs.length > 1 ) {
-        activeTab = activeTabs[activeTabs.length-1];
+    }
+
+    hide(noTimer) {
+      const self = this;
+      const { element, options } = self;
+
+      if (element && hasClass(element, showClass)) {
+        element.dispatchEvent(hideToastEvent);
+        if (hideToastEvent.defaultPrevented) return;
+
+        clearTimeout(self.timer);
+        self.timer = setTimeout(() => closeToast(self), // Bugfix by BlackDex to get autohide with a delay working.
+          noTimer ? 10 : options.delay,
+        );
       }
-      return activeTab;
     }
-    function getActiveContent() { return queryElement(getActiveTab().getAttribute('href')) }
-    function clickHandler(e) {
-      e.preventDefault();
-      next = e.currentTarget;
-      !tabs.isAnimating && self.show();
-    }
-    self.show = function () {
-      next = next || element;
-      if (!next.classList.contains('active')) {
-        nextContent = queryElement(next.getAttribute('href'));
-        activeTab = getActiveTab();
-        activeContent = getActiveContent();
-        hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', { relatedTarget: next });
-        dispatchCustomEvent.call(activeTab, hideCustomEvent);
-        if (hideCustomEvent.defaultPrevented) { return; }
-        tabs.isAnimating = true;
-        activeTab.classList.remove('active');
-        activeTab.setAttribute('aria-selected','false');
-        next.classList.add('active');
-        next.setAttribute('aria-selected','true');
-        if ( dropdown ) {
-          if ( !element.parentNode.classList.contains('dropdown-menu') ) {
-            if (dropdown.classList.contains('active')) { dropdown.classList.remove('active'); }
-          } else {
-            if (!dropdown.classList.contains('active')) { dropdown.classList.add('active'); }
-          }
+
+    dispose() {
+      const self = this;
+      const { element, options } = self;
+      self.hide();
+
+      if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
+      else completeDisposeToast(self);
+
+      super.dispose(toastComponent);
+    }
+  }
+
+  Toast.init = {
+    component: toastComponent,
+    selector: toastSelector,
+    constructor: Toast,
+  };
+
+  const dataOriginalTitle = 'data-original-title';
+
+  /* Native JavaScript for Bootstrap 5 | Tooltip
+  ---------------------------------------------- */
+
+  // TOOLTIP PRIVATE GC
+  // ==================
+  const tooltipString = 'tooltip';
+  const tooltipComponent = 'Tooltip';
+  const tooltipSelector = `[${dataBsToggle}="${tooltipString}"],[data-tip="${tooltipString}"]`;
+
+  const titleAttr = 'title';
+  const tooltipInnerClass = `${tooltipString}-inner`;
+  const tooltipDefaultOptions = {
+    title: null,
+    template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+    placement: 'top',
+    animation: true,
+    customClass: null,
+    delay: 200,
+    sanitizeFn: null,
+  };
+
+  // TOOLTIP CUSTOM EVENTS
+  // =====================
+  const showTooltipEvent = bootstrapCustomEvent(`show.bs.${tooltipString}`);
+  const shownTooltipEvent = bootstrapCustomEvent(`shown.bs.${tooltipString}`);
+  const hideTooltipEvent = bootstrapCustomEvent(`hide.bs.${tooltipString}`);
+  const hiddenTooltipEvent = bootstrapCustomEvent(`hidden.bs.${tooltipString}`);
+
+  // TOOLTIP PRIVATE METHODS
+  // =======================
+  function createTooltip(self) {
+    const { options, id } = self;
+    const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`;
+    let titleString = options.title.trim();
+
+    // sanitize stuff
+    if (options.sanitizeFn) {
+      titleString = options.sanitizeFn(titleString);
+      options.template = options.sanitizeFn(options.template);
+    }
+
+    if (!titleString) return;
+
+    // create tooltip
+    self.tooltip = document.createElement('div');
+    const { tooltip } = self;
+
+    // set aria
+    tooltip.setAttribute('id', id);
+
+    // set markup
+    const tooltipMarkup = document.createElement('div');
+    tooltipMarkup.innerHTML = options.template.trim();
+
+    tooltip.className = tooltipMarkup.firstChild.className;
+    tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
+
+    queryElement(`.${tooltipInnerClass}`, tooltip).innerHTML = titleString;
+
+    // set arrow
+    self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
+
+    // set class and role attribute
+    tooltip.setAttribute('role', tooltipString);
+    // set classes
+    if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
+    if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
+    if (options.customClass && !hasClass(tooltip, options.customClass)) {
+      addClass(tooltip, options.customClass);
+    }
+    if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
+  }
+
+  function removeTooltip(self) {
+    const { element, options, tooltip } = self;
+    element.removeAttribute(ariaDescribedBy);
+    options.container.removeChild(tooltip);
+    self.timer = null;
+  }
+
+  function disposeTooltipComplete(self) {
+    const { element } = self;
+    toggleTooltipHandlers(self);
+    if (element.hasAttribute(dataOriginalTitle)) toggleTooltipTitle(self);
+  }
+  function toggleTooltipAction(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+
+    document[action]('touchstart', tooltipTouchHandler, passiveHandler);
+
+    if (!isMedia(self.element)) {
+      window[action]('scroll', self.update, passiveHandler);
+      window[action]('resize', self.update, passiveHandler);
+    }
+  }
+  function tooltipShownAction(self) {
+    toggleTooltipAction(self, 1);
+    self.element.dispatchEvent(shownTooltipEvent);
+  }
+  function tooltipHiddenAction(self) {
+    toggleTooltipAction(self);
+    removeTooltip(self);
+    self.element.dispatchEvent(hiddenTooltipEvent);
+  }
+  function toggleTooltipHandlers(self, add) {
+    const action = add ? addEventListener : removeEventListener;
+    const { element } = self;
+
+    if (isMedia(element)) element[action]('mousemove', self.update, passiveHandler);
+    element[action]('mousedown', self.show);
+    element[action]('mouseenter', self.show);
+    element[action]('mouseleave', self.hide);
+  }
+
+  function toggleTooltipTitle(self, content) {
+    // [0 - add, 1 - remove] | [0 - remove, 1 - add]
+    const titleAtt = [dataOriginalTitle, titleAttr];
+    const { element } = self;
+
+    element.setAttribute(titleAtt[content ? 0 : 1],
+      (content || element.getAttribute(titleAtt[0])));
+    element.removeAttribute(titleAtt[content ? 1 : 0]);
+  }
+
+  // TOOLTIP EVENT HANDLERS
+  // ======================
+  function tooltipTouchHandler({ target }) {
+    const { tooltip, element } = this;
+    if (tooltip.contains(target) || target === element || element.contains(target)) ; else {
+      this.hide();
+    }
+  }
+
+  // TOOLTIP DEFINITION
+  // ==================
+  class Tooltip extends BaseComponent {
+    constructor(target, config) {
+      // initialization element
+      const element = queryElement(target);
+      tooltipDefaultOptions.title = element.getAttribute(titleAttr);
+      tooltipDefaultOptions.container = getTipContainer(element);
+      super(tooltipComponent, element, tooltipDefaultOptions, config);
+
+      // bind
+      const self = this;
+
+      // additional properties
+      self.tooltip = null;
+      self.arrow = null;
+      self.timer = null;
+      self.enabled = false;
+
+      // instance options
+      const { options } = self;
+
+      // media elements only work with body as a container
+      self.options.container = isMedia(element)
+        ? tooltipDefaultOptions.container
+        : queryElement(options.container);
+
+      // reset default options
+      tooltipDefaultOptions.container = null;
+      tooltipDefaultOptions[titleAttr] = null;
+
+      // invalidate
+      if (!options.title) return;
+
+      // all functions bind
+      tooltipTouchHandler.bind(self);
+      self.update = self.update.bind(self);
+
+      // set title attributes and add event listeners
+      if (element.hasAttribute(titleAttr)) toggleTooltipTitle(self, options.title);
+
+      // create tooltip here
+      self.id = `${tooltipString}-${getUID(element)}`;
+      createTooltip(self);
+
+      // attach events
+      toggleTooltipHandlers(self, 1);
+    }
+
+    // TOOLTIP PUBLIC METHODS
+    // ======================
+    show(e) {
+      const self = e ? this[tooltipComponent] : this;
+      const {
+        options, tooltip, element, id,
+      } = self;
+      clearTimeout(self.timer);
+      self.timer = setTimeout(() => {
+        if (!isVisibleTip(tooltip, options.container)) {
+          element.dispatchEvent(showTooltipEvent);
+          if (showTooltipEvent.defaultPrevented) return;
+
+          // append to container
+          options.container.appendChild(tooltip);
+          element.setAttribute(ariaDescribedBy, id);
+
+          self.update(e);
+          if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
+          if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
+          else tooltipShownAction(self);
         }
-        if (activeContent.classList.contains('fade')) {
-          activeContent.classList.remove('show');
-          emulateTransitionEnd(activeContent, triggerHide);
-        } else { triggerHide(); }
-      }
-    };
-    self.dispose = function () {
-      element.removeEventListener('click',clickHandler,false);
-      delete element.Tab;
-    };
-    element = queryElement(element);
-    element.Tab && element.Tab.dispose();
-    heightData = element.getAttribute('data-height');
-    tabs = element.closest('.nav');
-    dropdown = tabs && queryElement('.dropdown-toggle',tabs);
-    animateHeight = !supportTransition || (options.height === false || heightData === 'false') ? false : true;
-    tabs.isAnimating = false;
-    if ( !element.Tab ) {
-      element.addEventListener('click',clickHandler,false);
-    }
-    if (animateHeight) { tabsContentContainer = getActiveContent().parentNode; }
-    element.Tab = self;
-  }
-
-  function Toast(element,options) {
-    options = options || {};
-    var self = this,
-        toast, timer = 0,
-        animationData,
-        autohideData,
-        delayData,
-        showCustomEvent,
-        hideCustomEvent,
-        shownCustomEvent,
-        hiddenCustomEvent,
-        ops = {};
-    function showComplete() {
-      toast.classList.remove( 'showing' );
-      toast.classList.add( 'show' );
-      dispatchCustomEvent.call(toast,shownCustomEvent);
-      if (ops.autohide) { self.hide(); }
-    }
-    function hideComplete() {
-      toast.classList.add( 'hide' );
-      dispatchCustomEvent.call(toast,hiddenCustomEvent);
-    }
-    function close () {
-      toast.classList.remove('show' );
-      ops.animation ? emulateTransitionEnd(toast, hideComplete) : hideComplete();
-    }
-    function disposeComplete() {
-      clearTimeout(timer);
-      element.removeEventListener('click',self.hide,false);
-      delete element.Toast;
-    }
-    self.show = function () {
-      if (toast && !toast.classList.contains('show')) {
-        dispatchCustomEvent.call(toast,showCustomEvent);
-        if (showCustomEvent.defaultPrevented) { return; }
-        ops.animation && toast.classList.add( 'fade' );
-        toast.classList.remove('hide' );
-        toast.offsetWidth;
-        toast.classList.add('showing' );
-        ops.animation ? emulateTransitionEnd(toast, showComplete) : showComplete();
-      }
-    };
-    self.hide = function (noTimer) {
-      if (toast && toast.classList.contains('show')) {
-        dispatchCustomEvent.call(toast,hideCustomEvent);
-        if(hideCustomEvent.defaultPrevented) { return; }
-        noTimer ? close() : (timer = setTimeout( close, ops.delay));
+      }, 20);
+    }
+
+    hide(e) {
+      const self = e ? this[tooltipComponent] : this;
+      const { options, tooltip, element } = self;
+
+      clearTimeout(self.timer);
+      self.timer = setTimeout(() => {
+        if (isVisibleTip(tooltip, options.container)) {
+          element.dispatchEvent(hideTooltipEvent);
+          if (hideTooltipEvent.defaultPrevented) return;
+
+          removeClass(tooltip, showClass);
+          if (options.animation) emulateTransitionEnd(tooltip, () => tooltipHiddenAction(self));
+          else tooltipHiddenAction(self);
+        }
+      }, options.delay);
+    }
+
+    update(e) {
+      styleTip(this, e);
+    }
+
+    toggle() {
+      const self = this;
+      const { tooltip, options } = self;
+      if (!isVisibleTip(tooltip, options.container)) self.show();
+      else self.hide();
+    }
+
+    enable() {
+      const self = this;
+      const { enabled } = self;
+      if (!enabled) {
+        toggleTooltipHandlers(self, 1);
+        self.enabled = !enabled;
       }
-    };
-    self.dispose = function () {
-      ops.animation ? emulateTransitionEnd(toast, disposeComplete) : disposeComplete();
-    };
-    element = queryElement(element);
-    element.Toast && element.Toast.dispose();
-    toast = element.closest('.toast');
-    animationData = element.getAttribute('data-animation');
-    autohideData = element.getAttribute('data-autohide');
-    delayData = element.getAttribute('data-delay');
-    showCustomEvent = bootstrapCustomEvent('show', 'toast');
-    hideCustomEvent = bootstrapCustomEvent('hide', 'toast');
-    shownCustomEvent = bootstrapCustomEvent('shown', 'toast');
-    hiddenCustomEvent = bootstrapCustomEvent('hidden', 'toast');
-    ops.animation = options.animation === false || animationData === 'false' ? 0 : 1;
-    ops.autohide = options.autohide === false || autohideData === 'false' ? 0 : 1;
-    ops.delay = parseInt(options.delay || delayData) || 500;
-    if ( !element.Toast ) {
-      element.addEventListener('click',self.hide,false);
-    }
-    element.Toast = self;
-  }
-
-  function Tooltip(element,options) {
-    options = options || {};
-    var self = this,
-        tooltip = null, timer = 0, titleString,
-        animationData,
-        placementData,
-        delayData,
-        containerData,
-        showCustomEvent,
-        shownCustomEvent,
-        hideCustomEvent,
-        hiddenCustomEvent,
-        containerElement,
-        containerDataElement,
-        modal,
-        navbarFixedTop,
-        navbarFixedBottom,
-        placementClass,
-        ops = {};
-    function getTitle() {
-      return element.getAttribute('title')
-          || element.getAttribute('data-title')
-          || element.getAttribute('data-original-title')
-    }
-    function removeToolTip() {
-      ops.container.removeChild(tooltip);
-      tooltip = null; timer = null;
-    }
-    function createToolTip() {
-      titleString = getTitle();
-      if ( titleString ) {
-        tooltip = document.createElement('div');
-        if (ops.template) {
-          var tooltipMarkup = document.createElement('div');
-          tooltipMarkup.innerHTML = ops.template.trim();
-          tooltip.className = tooltipMarkup.firstChild.className;
-          tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
-          queryElement('.tooltip-inner',tooltip).innerHTML = titleString.trim();
+    }
+
+    disable() {
+      const self = this;
+      const { tooltip, options, enabled } = self;
+      if (enabled) {
+        if (!isVisibleTip(tooltip, options.container) && options.animation) {
+          self.hide();
+
+          setTimeout(
+            () => toggleTooltipHandlers(self),
+            getElementTransitionDuration(tooltip) + options.delay + 17,
+          );
         } else {
-          var tooltipArrow = document.createElement('div');
-          tooltipArrow.classList.add('arrow');
-          tooltip.appendChild(tooltipArrow);
-          var tooltipInner = document.createElement('div');
-          tooltipInner.classList.add('tooltip-inner');
-          tooltip.appendChild(tooltipInner);
-          tooltipInner.innerHTML = titleString;
+          toggleTooltipHandlers(self);
         }
-        tooltip.style.left = '0';
-        tooltip.style.top = '0';
-        tooltip.setAttribute('role','tooltip');
-        !tooltip.classList.contains('tooltip') && tooltip.classList.add('tooltip');
-        !tooltip.classList.contains(ops.animation) && tooltip.classList.add(ops.animation);
-        !tooltip.classList.contains(placementClass) && tooltip.classList.add(placementClass);
-        ops.container.appendChild(tooltip);
+        self.enabled = !enabled;
       }
     }
-    function updateTooltip() {
-      styleTip(element, tooltip, ops.placement, ops.container);
-    }
-    function showTooltip() {
-      !tooltip.classList.contains('show') && ( tooltip.classList.add('show') );
+
+    toggleEnabled() {
+      const self = this;
+      if (!self.enabled) self.enable();
+      else self.disable();
     }
-    function touchHandler(e){
-      if ( tooltip && tooltip.contains(e.target) || e.target === element || element.contains(e.target)) ; else {
+
+    dispose() {
+      const self = this;
+      const { tooltip, options } = self;
+
+      if (options.animation && isVisibleTip(tooltip, options.container)) {
+        options.delay = 0; // reset delay
         self.hide();
+        emulateTransitionEnd(tooltip, () => disposeTooltipComplete(self));
+      } else {
+        disposeTooltipComplete(self);
       }
+      super.dispose(tooltipComponent);
     }
-    function toggleAction(action){
-      action = action ? 'addEventListener' : 'removeEventListener';
-      document[action]( 'touchstart', touchHandler, passiveHandler );
-      window[action]( 'resize', self.hide, passiveHandler );
-    }
-    function showAction() {
-      toggleAction(1);
-      dispatchCustomEvent.call(element, shownCustomEvent);
-    }
-    function hideAction() {
-      toggleAction();
-      removeToolTip();
-      dispatchCustomEvent.call(element, hiddenCustomEvent);
-    }
-    function toggleEvents(action) {
-      action = action ? 'addEventListener' : 'removeEventListener';
-      element[action](mouseClickEvents.down, self.show,false);
-      element[action](mouseHoverEvents[0], self.show,false);
-      element[action](mouseHoverEvents[1], self.hide,false);
-    }
-    self.show = function () {
-      clearTimeout(timer);
-      timer = setTimeout( function () {
-        if (tooltip === null) {
-          dispatchCustomEvent.call(element, showCustomEvent);
-          if (showCustomEvent.defaultPrevented) { return; }
-          if(createToolTip() !== false) {
-            updateTooltip();
-            showTooltip();
-            !!ops.animation ? emulateTransitionEnd(tooltip, showAction) : showAction();
-          }
-        }
-      }, 20 );
-    };
-    self.hide = function () {
-      clearTimeout(timer);
-      timer = setTimeout( function () {
-        if (tooltip && tooltip.classList.contains('show')) {
-          dispatchCustomEvent.call(element, hideCustomEvent);
-          if (hideCustomEvent.defaultPrevented) { return; }
-          tooltip.classList.remove('show');
-          !!ops.animation ? emulateTransitionEnd(tooltip, hideAction) : hideAction();
-        }
-      }, ops.delay);
-    };
-    self.toggle = function () {
-      if (!tooltip) { self.show(); }
-      else { self.hide(); }
-    };
-    self.dispose = function () {
-      toggleEvents();
-      self.hide();
-      element.setAttribute('title', element.getAttribute('data-original-title'));
-      element.removeAttribute('data-original-title');
-      delete element.Tooltip;
-    };
-    element = queryElement(element);
-    element.Tooltip && element.Tooltip.dispose();
-    animationData = element.getAttribute('data-animation');
-    placementData = element.getAttribute('data-placement');
-    delayData = element.getAttribute('data-delay');
-    containerData = element.getAttribute('data-container');
-    showCustomEvent = bootstrapCustomEvent('show', 'tooltip');
-    shownCustomEvent = bootstrapCustomEvent('shown', 'tooltip');
-    hideCustomEvent = bootstrapCustomEvent('hide', 'tooltip');
-    hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tooltip');
-    containerElement = queryElement(options.container);
-    containerDataElement = queryElement(containerData);
-    modal = element.closest('.modal');
-    navbarFixedTop = element.closest('.fixed-top');
-    navbarFixedBottom = element.closest('.fixed-bottom');
-    ops.animation = options.animation && options.animation !== 'fade' ? options.animation : animationData || 'fade';
-    ops.placement = options.placement ? options.placement : placementData || 'top';
-    ops.template = options.template ? options.template : null;
-    ops.delay = parseInt(options.delay || delayData) || 200;
-    ops.container = containerElement ? containerElement
-                            : containerDataElement ? containerDataElement
-                            : navbarFixedTop ? navbarFixedTop
-                            : navbarFixedBottom ? navbarFixedBottom
-                            : modal ? modal : document.body;
-    placementClass = "bs-tooltip-" + (ops.placement);
-    titleString = getTitle();
-    if ( !titleString ) { return; }
-    if (!element.Tooltip) {
-      element.setAttribute('data-original-title',titleString);
-      element.removeAttribute('title');
-      toggleEvents(1);
-    }
-    element.Tooltip = self;
-  }
-
-  var componentsInit = {};
-
-  function initializeDataAPI( Constructor, collection ){
-    Array.from(collection).map(function (x){ return new Constructor(x); });
-  }
-  function initCallback(lookUp){
-    lookUp = lookUp || document;
-    for (var component in componentsInit) {
-      initializeDataAPI( componentsInit[component][0], lookUp.querySelectorAll (componentsInit[component][1]) );
-    }
-  }
-
-  componentsInit.Alert = [ Alert, '[data-dismiss="alert"]'];
-  componentsInit.Button = [ Button, '[data-toggle="buttons"]' ];
-  componentsInit.Carousel = [ Carousel, '[data-ride="carousel"]' ];
-  componentsInit.Collapse = [ Collapse, '[data-toggle="collapse"]' ];
-  componentsInit.Dropdown = [ Dropdown, '[data-toggle="dropdown"]'];
-  componentsInit.Modal = [ Modal, '[data-toggle="modal"]' ];
-  componentsInit.Popover = [ Popover, '[data-toggle="popover"],[data-tip="popover"]' ];
-  componentsInit.ScrollSpy = [ ScrollSpy, '[data-spy="scroll"]' ];
-  componentsInit.Tab = [ Tab, '[data-toggle="tab"]' ];
-  componentsInit.Toast = [ Toast, '[data-dismiss="toast"]' ];
-  componentsInit.Tooltip = [ Tooltip, '[data-toggle="tooltip"],[data-tip="tooltip"]' ];
-  document.body ? initCallback() : document.addEventListener( 'DOMContentLoaded', function initWrapper(){
-  	initCallback();
-  	document.removeEventListener('DOMContentLoaded',initWrapper,false);
-  }, false );
-
-  function removeElementDataAPI( ConstructorName, collection ){
-    Array.from(collection).map(function (x){ return x[ConstructorName].dispose(); });
-  }
-  function removeDataAPI(lookUp) {
-    lookUp = lookUp || document;
-    for (var component in componentsInit) {
-      removeElementDataAPI( component, lookUp.querySelectorAll (componentsInit[component][1]) );
-    }
-  }
-
-  var version = "3.0.15";
+  }
+
+  Tooltip.init = {
+    component: tooltipComponent,
+    selector: tooltipSelector,
+    constructor: Tooltip,
+  };
+
+  var version = "4.0.2";
+
+  // import { alertInit } from '../components/alert-native.js';
+  // import { buttonInit } from '../components/button-native.js';
+  // import { carouselInit } from '../components/carousel-native.js';
+  // import { collapseInit } from '../components/collapse-native.js';
+  // import { dropdownInit } from '../components/dropdown-native.js';
+  // import { modalInit } from '../components/modal-native.js';
+  // import { offcanvasInit } from '../components/offcanvas-native.js';
+  // import { popoverInit } from '../components/popover-native.js';
+  // import { scrollSpyInit } from '../components/scrollspy-native.js';
+  // import { tabInit } from '../components/tab-native.js';
+  // import { toastInit } from '../components/toast-native.js';
+  // import { tooltipInit } from '../components/tooltip-native.js';
+
+  const componentsInit = {
+    Alert: Alert.init,
+    Button: Button.init,
+    Carousel: Carousel.init,
+    Collapse: Collapse.init,
+    Dropdown: Dropdown.init,
+    Modal: Modal.init,
+    Offcanvas: Offcanvas.init,
+    Popover: Popover.init,
+    ScrollSpy: ScrollSpy.init,
+    Tab: Tab.init,
+    Toast: Toast.init,
+    Tooltip: Tooltip.init,
+  };
+
+  function initializeDataAPI(Konstructor, collection) {
+    Array.from(collection).forEach((x) => new Konstructor(x));
+  }
+
+  function initCallback(context) {
+    const lookUp = context instanceof Element ? context : document;
+
+    Object.keys(componentsInit).forEach((comp) => {
+      const { constructor, selector } = componentsInit[comp];
+      initializeDataAPI(constructor, lookUp.querySelectorAll(selector));
+    });
+  }
+
+  // bulk initialize all components
+  if (document.body) initCallback();
+  else {
+    document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
+  }
 
   var index = {
-    Alert: Alert,
-    Button: Button,
-    Carousel: Carousel,
-    Collapse: Collapse,
-    Dropdown: Dropdown,
-    Modal: Modal,
-    Popover: Popover,
-    ScrollSpy: ScrollSpy,
-    Tab: Tab,
-    Toast: Toast,
-    Tooltip: Tooltip,
-    initCallback: initCallback,
-    removeDataAPI: removeDataAPI,
-    componentsInit: componentsInit,
-    Version: version
+    Alert,
+    Button,
+    Carousel,
+    Collapse,
+    Dropdown,
+    Modal,
+    Offcanvas,
+    Popover,
+    ScrollSpy,
+    Tab,
+    Toast,
+    Tooltip,
+
+    initCallback,
+    Version: version,
   };
 
   return index;

File diff suppressed because it is too large
+ 1354 - 1224
src/static/scripts/bootstrap.css


+ 20 - 6
src/static/scripts/datatables.css

@@ -4,13 +4,18 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs4/dt-1.10.23
+ *   https://datatables.net/download/#bs5/dt-1.10.25
  *
  * Included libraries:
- *   DataTables 1.10.23
+ *   DataTables 1.10.25
  */
 
 @charset "UTF-8";
+/*! Bootstrap 5 integration for DataTables
+ *
+ * ©2020 SpryMedia Ltd, all rights reserved.
+ * License: MIT datatables.net/license/mit
+ */
 table.dataTable {
   clear: both;
   margin-top: 6px !important;
@@ -105,7 +110,7 @@ table.dataTable > thead .sorting_asc_disabled:after,
 table.dataTable > thead .sorting_desc_disabled:before,
 table.dataTable > thead .sorting_desc_disabled:after {
   position: absolute;
-  bottom: 0.9em;
+  bottom: 0.5em;
   display: block;
   opacity: 0.3;
 }
@@ -193,18 +198,27 @@ table.dataTable.table-sm .sorting_desc:after {
 table.table-bordered.dataTable {
   border-right-width: 0;
 }
+table.table-bordered.dataTable thead tr:first-child th,
+table.table-bordered.dataTable thead tr:first-child td {
+  border-top-width: 1px;
+}
 table.table-bordered.dataTable th,
 table.table-bordered.dataTable td {
   border-left-width: 0;
 }
+table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child,
+table.table-bordered.dataTable td:first-child,
+table.table-bordered.dataTable td:first-child {
+  border-left-width: 1px;
+}
 table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
 table.table-bordered.dataTable td:last-child,
 table.table-bordered.dataTable td:last-child {
   border-right-width: 1px;
 }
-table.table-bordered.dataTable tbody th,
-table.table-bordered.dataTable tbody td {
-  border-bottom-width: 0;
+table.table-bordered.dataTable th,
+table.table-bordered.dataTable td {
+  border-bottom-width: 1px;
 }
 
 div.dataTables_scrollHead table.table-bordered {

+ 44 - 46
src/static/scripts/datatables.js

@@ -4,24 +4,24 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs4/dt-1.10.23
+ *   https://datatables.net/download/#bs5/dt-1.10.25
  *
  * Included libraries:
- *   DataTables 1.10.23
+ *   DataTables 1.10.25
  */
 
-/*! DataTables 1.10.23
- * ©2008-2020 SpryMedia Ltd - datatables.net/license
+/*! DataTables 1.10.25
+ * ©2008-2021 SpryMedia Ltd - datatables.net/license
  */
 
 /**
  * @summary     DataTables
  * @description Paginate, search and order HTML tables
- * @version     1.10.23
+ * @version     1.10.25
  * @file        jquery.dataTables.js
  * @author      SpryMedia Ltd
  * @contact     www.datatables.net
- * @copyright   Copyright 2008-2020 SpryMedia Ltd.
+ * @copyright   Copyright 2008-2021 SpryMedia Ltd.
  *
  * This source file is free software, available under the following license:
  *   MIT license - http://datatables.net/license
@@ -1100,6 +1100,8 @@
 						_fnLanguageCompat( json );
 						_fnCamelToHungarian( defaults.oLanguage, json );
 						$.extend( true, oLanguage, json );
+			
+						_fnCallbackFire( oSettings, null, 'i18n', [oSettings]);
 						_fnInitialise( oSettings );
 					},
 					error: function () {
@@ -1109,6 +1111,9 @@
 				} );
 				bInitHandedOff = true;
 			}
+			else {
+				_fnCallbackFire( oSettings, null, 'i18n', [oSettings]);
+			}
 			
 			/*
 			 * Stripes
@@ -1260,7 +1265,7 @@
 			
 				var tbody = $this.children('tbody');
 				if ( tbody.length === 0 ) {
-					tbody = $('<tbody/>').appendTo($this);
+					tbody = $('<tbody/>').insertAfter(thead);
 				}
 				oSettings.nTBody = tbody[0];
 			
@@ -2315,8 +2320,9 @@
 						}
 	
 						// Only a single match is needed for html type since it is
-						// bottom of the pile and very similar to string
-						if ( detectedType === 'html' ) {
+						// bottom of the pile and very similar to string - but it
+						// must not be empty
+						if ( detectedType === 'html' && ! _empty(cache[k]) ) {
 							break;
 						}
 					}
@@ -3421,9 +3427,10 @@
 	/**
 	 * Insert the required TR nodes into the table for display
 	 *  @param {object} oSettings dataTables settings object
+	 *  @param ajaxComplete true after ajax call to complete rendering
 	 *  @memberof DataTable#oApi
 	 */
-	function _fnDraw( oSettings )
+	function _fnDraw( oSettings, ajaxComplete )
 	{
 		/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
 		var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
@@ -3472,8 +3479,9 @@
 		{
 			oSettings.iDraw++;
 		}
-		else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+		else if ( !oSettings.bDestroying && !ajaxComplete)
 		{
+			_fnAjaxUpdate( oSettings );
 			return;
 		}
 	
@@ -4005,21 +4013,16 @@
 	 */
 	function _fnAjaxUpdate( settings )
 	{
-		if ( settings.bAjaxDataGet ) {
-			settings.iDraw++;
-			_fnProcessingDisplay( settings, true );
-	
-			_fnBuildAjax(
-				settings,
-				_fnAjaxParameters( settings ),
-				function(json) {
-					_fnAjaxUpdateDraw( settings, json );
-				}
-			);
+		settings.iDraw++;
+		_fnProcessingDisplay( settings, true );
 	
-			return false;
-		}
-		return true;
+		_fnBuildAjax(
+			settings,
+			_fnAjaxParameters( settings ),
+			function(json) {
+				_fnAjaxUpdateDraw( settings, json );
+			}
+		);
 	}
 	
 	
@@ -4172,14 +4175,12 @@
 		}
 		settings.aiDisplay = settings.aiDisplayMaster.slice();
 	
-		settings.bAjaxDataGet = false;
-		_fnDraw( settings );
+		_fnDraw( settings, true );
 	
 		if ( ! settings._bInitComplete ) {
 			_fnInitComplete( settings, json );
 		}
 	
-		settings.bAjaxDataGet = true;
 		_fnProcessingDisplay( settings, false );
 	}
 	
@@ -6108,7 +6109,7 @@
 		{
 			var col = columns[i];
 			var asSorting = col.asSorting;
-			var sTitle = col.sTitle.replace( /<.*?>/g, "" );
+			var sTitle = col.ariaTitle || col.sTitle.replace( /<.*?>/g, "" );
 			var th = col.nTh;
 	
 			// IE7 is throwing an error when setting these properties with jQuery's
@@ -9542,7 +9543,7 @@
 	 *  @type string
 	 *  @default Version number
 	 */
-	DataTable.version = "1.10.23";
+	DataTable.version = "1.10.25";
 
 	/**
 	 * Private data store, containing all of the settings objects that are
@@ -13623,13 +13624,6 @@
 		 */
 		"sAjaxDataProp": null,
 	
-		/**
-		 * Note if draw should be blocked while getting data
-		 *  @type boolean
-		 *  @default true
-		 */
-		"bAjaxDataGet": true,
-	
 		/**
 		 * The last jQuery XHR object that was used for server-side data gathering.
 		 * This can be used for working with the XHR information in one of the
@@ -13966,7 +13960,7 @@
 		 *
 		 *  @type string
 		 */
-		build:"bs4/dt-1.10.23",
+		build:"bs5/dt-1.10.25",
 	
 	
 		/**
@@ -14494,8 +14488,8 @@
 		"sSortAsc": "sorting_asc",
 		"sSortDesc": "sorting_desc",
 		"sSortable": "sorting", /* Sortable in both directions */
-		"sSortableAsc": "sorting_asc_disabled",
-		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableAsc": "sorting_desc_disabled",
+		"sSortableDesc": "sorting_asc_disabled",
 		"sSortableNone": "sorting_disabled",
 		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
 	
@@ -14936,7 +14930,6 @@
 	
 					cell
 						.removeClass(
-							column.sSortingClass +' '+
 							classes.sSortAsc +' '+
 							classes.sSortDesc
 						)
@@ -15061,6 +15054,11 @@
 						decimal+(d - intPart).toFixed( precision ).substring( 2 ):
 						'';
 	
+					// If zero, then can't have a negative prefix
+					if (intPart === 0 && parseFloat(floatPart) === 0) {
+						negative = '';
+					}
+	
 					return negative + (prefix||'') +
 						intPart.toString().replace(
 							/\B(?=(\d{3})+(?!\d))/g, thousands
@@ -15395,12 +15393,12 @@
 }));
 
 
-/*! DataTables Bootstrap 4 integration
- * ©2011-2017 SpryMedia Ltd - datatables.net/license
+/*! DataTables Bootstrap 5 integration
+ * 2020 SpryMedia Ltd - datatables.net/license
  */
 
 /**
- * DataTables integration for Bootstrap 4. This requires Bootstrap 4 and
+ * DataTables integration for Bootstrap 4. This requires Bootstrap 5 and
  * DataTables 1.10 or newer.
  *
  * This file sets the defaults and adds options to DataTables to style its
@@ -15452,9 +15450,9 @@ $.extend( true, DataTable.defaults, {
 
 /* Default class modification */
 $.extend( DataTable.ext.classes, {
-	sWrapper:      "dataTables_wrapper dt-bootstrap4",
+	sWrapper:      "dataTables_wrapper dt-bootstrap5",
 	sFilterInput:  "form-control form-control-sm",
-	sLengthSelect: "custom-select custom-select-sm form-control form-control-sm",
+	sLengthSelect: "form-select form-select-sm",
 	sProcessing:   "dataTables_processing card",
 	sPageButton:   "paginate_button page-item"
 } );

+ 106 - 101
src/static/scripts/jquery-3.5.1.slim.js → src/static/scripts/jquery-3.6.0.slim.js

@@ -1,15 +1,15 @@
 /*!
- * jQuery JavaScript Library v3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
+ * jQuery JavaScript Library v3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
  * https://jquery.com/
  *
  * Includes Sizzle.js
  * https://sizzlejs.com/
  *
- * Copyright JS Foundation and other contributors
+ * Copyright OpenJS Foundation and other contributors
  * Released under the MIT license
  * https://jquery.org/license
  *
- * Date: 2020-05-04T22:49Z
+ * Date: 2021-03-02T17:08Z
  */
 ( function( global, factory ) {
 
@@ -76,12 +76,16 @@ var support = {};
 
 var isFunction = function isFunction( obj ) {
 
-      // Support: Chrome <=57, Firefox <=52
-      // In some browsers, typeof returns "function" for HTML <object> elements
-      // (i.e., `typeof document.createElement( "object" ) === "function"`).
-      // We don't want to classify *any* DOM node as a function.
-      return typeof obj === "function" && typeof obj.nodeType !== "number";
-  };
+		// Support: Chrome <=57, Firefox <=52
+		// In some browsers, typeof returns "function" for HTML <object> elements
+		// (i.e., `typeof document.createElement( "object" ) === "function"`).
+		// We don't want to classify *any* DOM node as a function.
+		// Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5
+		// Plus for old WebKit, typeof returns "function" for HTML collections
+		// (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756)
+		return typeof obj === "function" && typeof obj.nodeType !== "number" &&
+			typeof obj.item !== "function";
+	};
 
 
 var isWindow = function isWindow( obj ) {
@@ -147,7 +151,7 @@ function toType( obj ) {
 
 
 var
-	version = "3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
+	version = "3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
 
 	// Define a local copy of jQuery
 	jQuery = function( selector, context ) {
@@ -401,7 +405,7 @@ jQuery.extend( {
 			if ( isArrayLike( Object( arr ) ) ) {
 				jQuery.merge( ret,
 					typeof arr === "string" ?
-					[ arr ] : arr
+						[ arr ] : arr
 				);
 			} else {
 				push.call( ret, arr );
@@ -496,9 +500,9 @@ if ( typeof Symbol === "function" ) {
 
 // Populate the class2type map
 jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
-function( _i, name ) {
-	class2type[ "[object " + name + "]" ] = name.toLowerCase();
-} );
+	function( _i, name ) {
+		class2type[ "[object " + name + "]" ] = name.toLowerCase();
+	} );
 
 function isArrayLike( obj ) {
 
@@ -518,14 +522,14 @@ function isArrayLike( obj ) {
 }
 var Sizzle =
 /*!
- * Sizzle CSS Selector Engine v2.3.5
+ * Sizzle CSS Selector Engine v2.3.6
  * https://sizzlejs.com/
  *
  * Copyright JS Foundation and other contributors
  * Released under the MIT license
  * https://js.foundation/
  *
- * Date: 2020-03-14
+ * Date: 2021-02-16
  */
 ( function( window ) {
 var i,
@@ -1108,8 +1112,8 @@ support = Sizzle.support = {};
  * @returns {Boolean} True iff elem is a non-HTML XML node
  */
 isXML = Sizzle.isXML = function( elem ) {
-	var namespace = elem.namespaceURI,
-		docElem = ( elem.ownerDocument || elem ).documentElement;
+	var namespace = elem && elem.namespaceURI,
+		docElem = elem && ( elem.ownerDocument || elem ).documentElement;
 
 	// Support: IE <=8
 	// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
@@ -3024,9 +3028,9 @@ var rneedsContext = jQuery.expr.match.needsContext;
 
 function nodeName( elem, name ) {
 
-  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+	return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
 
-};
+}
 var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
 
 
@@ -3997,8 +4001,8 @@ jQuery.extend( {
 			resolveContexts = Array( i ),
 			resolveValues = slice.call( arguments ),
 
-			// the master Deferred
-			master = jQuery.Deferred(),
+			// the primary Deferred
+			primary = jQuery.Deferred(),
 
 			// subordinate callback factory
 			updateFunc = function( i ) {
@@ -4006,30 +4010,30 @@ jQuery.extend( {
 					resolveContexts[ i ] = this;
 					resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
 					if ( !( --remaining ) ) {
-						master.resolveWith( resolveContexts, resolveValues );
+						primary.resolveWith( resolveContexts, resolveValues );
 					}
 				};
 			};
 
 		// Single- and empty arguments are adopted like Promise.resolve
 		if ( remaining <= 1 ) {
-			adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+			adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject,
 				!remaining );
 
 			// Use .then() to unwrap secondary thenables (cf. gh-3000)
-			if ( master.state() === "pending" ||
+			if ( primary.state() === "pending" ||
 				isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
 
-				return master.then();
+				return primary.then();
 			}
 		}
 
 		// Multiple arguments are aggregated like Promise.all array elements
 		while ( i-- ) {
-			adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+			adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject );
 		}
 
-		return master.promise();
+		return primary.promise();
 	}
 } );
 
@@ -4180,8 +4184,8 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
 			for ( ; i < len; i++ ) {
 				fn(
 					elems[ i ], key, raw ?
-					value :
-					value.call( elems[ i ], i, fn( elems[ i ], key ) )
+						value :
+						value.call( elems[ i ], i, fn( elems[ i ], key ) )
 				);
 			}
 		}
@@ -5089,10 +5093,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
 }
 
 
-var
-	rkeyEvent = /^key/,
-	rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
-	rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+var rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
 
 function returnTrue() {
 	return true;
@@ -5387,8 +5388,8 @@ jQuery.event = {
 			event = jQuery.event.fix( nativeEvent ),
 
 			handlers = (
-					dataPriv.get( this, "events" ) || Object.create( null )
-				)[ event.type ] || [],
+				dataPriv.get( this, "events" ) || Object.create( null )
+			)[ event.type ] || [],
 			special = jQuery.event.special[ event.type ] || {};
 
 		// Use the fix-ed jQuery.Event rather than the (read-only) native event
@@ -5512,12 +5513,12 @@ jQuery.event = {
 			get: isFunction( hook ) ?
 				function() {
 					if ( this.originalEvent ) {
-							return hook( this.originalEvent );
+						return hook( this.originalEvent );
 					}
 				} :
 				function() {
 					if ( this.originalEvent ) {
-							return this.originalEvent[ name ];
+						return this.originalEvent[ name ];
 					}
 				},
 
@@ -5656,7 +5657,13 @@ function leverageNative( el, type, expectSync ) {
 						// Cancel the outer synthetic event
 						event.stopImmediatePropagation();
 						event.preventDefault();
-						return result.value;
+
+						// Support: Chrome 86+
+						// In Chrome, if an element having a focusout handler is blurred by
+						// clicking outside of it, it invokes the handler synchronously. If
+						// that handler calls `.remove()` on the element, the data is cleared,
+						// leaving `result` undefined. We need to guard against this.
+						return result && result.value;
 					}
 
 				// If this is an inner synthetic event for an event with a bubbling surrogate
@@ -5821,34 +5828,7 @@ jQuery.each( {
 	targetTouches: true,
 	toElement: true,
 	touches: true,
-
-	which: function( event ) {
-		var button = event.button;
-
-		// Add which for key events
-		if ( event.which == null && rkeyEvent.test( event.type ) ) {
-			return event.charCode != null ? event.charCode : event.keyCode;
-		}
-
-		// Add which for click: 1 === left; 2 === middle; 3 === right
-		if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
-			if ( button & 1 ) {
-				return 1;
-			}
-
-			if ( button & 2 ) {
-				return 3;
-			}
-
-			if ( button & 4 ) {
-				return 2;
-			}
-
-			return 0;
-		}
-
-		return event.which;
-	}
+	which: true
 }, jQuery.event.addProp );
 
 jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
@@ -5874,6 +5854,12 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp
 			return true;
 		},
 
+		// Suppress native focus or blur as it's already being fired
+		// in leverageNative.
+		_default: function() {
+			return true;
+		},
+
 		delegateType: delegateType
 	};
 } );
@@ -6541,6 +6527,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
 		// set in CSS while `offset*` properties report correct values.
 		// Behavior in IE 9 is more subtle than in newer versions & it passes
 		// some versions of this test; make sure not to make it pass there!
+		//
+		// Support: Firefox 70+
+		// Only Firefox includes border widths
+		// in computed dimensions. (gh-4529)
 		reliableTrDimensions: function() {
 			var table, tr, trChild, trStyle;
 			if ( reliableTrDimensionsVal == null ) {
@@ -6548,17 +6538,32 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
 				tr = document.createElement( "tr" );
 				trChild = document.createElement( "div" );
 
-				table.style.cssText = "position:absolute;left:-11111px";
+				table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate";
+				tr.style.cssText = "border:1px solid";
+
+				// Support: Chrome 86+
+				// Height set through cssText does not get applied.
+				// Computed height then comes back as 0.
 				tr.style.height = "1px";
 				trChild.style.height = "9px";
 
+				// Support: Android 8 Chrome 86+
+				// In our bodyBackground.html iframe,
+				// display for all div elements is set to "inline",
+				// which causes a problem only in Android 8 Chrome 86.
+				// Ensuring the div is display: block
+				// gets around this issue.
+				trChild.style.display = "block";
+
 				documentElement
 					.appendChild( table )
 					.appendChild( tr )
 					.appendChild( trChild );
 
 				trStyle = window.getComputedStyle( tr );
-				reliableTrDimensionsVal = parseInt( trStyle.height ) > 3;
+				reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) +
+					parseInt( trStyle.borderTopWidth, 10 ) +
+					parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight;
 
 				documentElement.removeChild( table );
 			}
@@ -7022,10 +7027,10 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) {
 					// Running getBoundingClientRect on a disconnected node
 					// in IE throws an error.
 					( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
-						swap( elem, cssShow, function() {
-							return getWidthOrHeight( elem, dimension, extra );
-						} ) :
-						getWidthOrHeight( elem, dimension, extra );
+					swap( elem, cssShow, function() {
+						return getWidthOrHeight( elem, dimension, extra );
+					} ) :
+					getWidthOrHeight( elem, dimension, extra );
 			}
 		},
 
@@ -7084,7 +7089,7 @@ jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
 					swap( elem, { marginLeft: 0 }, function() {
 						return elem.getBoundingClientRect().left;
 					} )
-				) + "px";
+			) + "px";
 		}
 	}
 );
@@ -7608,8 +7613,8 @@ jQuery.fn.extend( {
 				if ( this.setAttribute ) {
 					this.setAttribute( "class",
 						className || value === false ?
-						"" :
-						dataPriv.get( this, "__className__" ) || ""
+							"" :
+							dataPriv.get( this, "__className__" ) || ""
 					);
 				}
 			}
@@ -7624,7 +7629,7 @@ jQuery.fn.extend( {
 		while ( ( elem = this[ i++ ] ) ) {
 			if ( elem.nodeType === 1 &&
 				( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
-					return true;
+				return true;
 			}
 		}
 
@@ -7914,9 +7919,7 @@ jQuery.extend( jQuery.event, {
 				special.bindType || type;
 
 			// jQuery handler
-			handle = (
-					dataPriv.get( cur, "events" ) || Object.create( null )
-				)[ event.type ] &&
+			handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] &&
 				dataPriv.get( cur, "handle" );
 			if ( handle ) {
 				handle.apply( cur, data );
@@ -8057,7 +8060,7 @@ if ( !support.focusin ) {
 
 // Cross-browser xml parsing
 jQuery.parseXML = function( data ) {
-	var xml;
+	var xml, parserErrorElem;
 	if ( !data || typeof data !== "string" ) {
 		return null;
 	}
@@ -8066,12 +8069,17 @@ jQuery.parseXML = function( data ) {
 	// IE throws on parseFromString with invalid input.
 	try {
 		xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
-	} catch ( e ) {
-		xml = undefined;
-	}
-
-	if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
-		jQuery.error( "Invalid XML: " + data );
+	} catch ( e ) {}
+
+	parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ];
+	if ( !xml || parserErrorElem ) {
+		jQuery.error( "Invalid XML: " + (
+			parserErrorElem ?
+				jQuery.map( parserErrorElem.childNodes, function( el ) {
+					return el.textContent;
+				} ).join( "\n" ) :
+				data
+		) );
 	}
 	return xml;
 };
@@ -8172,16 +8180,14 @@ jQuery.fn.extend( {
 			// Can add propHook for "elements" to filter or add form elements
 			var elements = jQuery.prop( this, "elements" );
 			return elements ? jQuery.makeArray( elements ) : this;
-		} )
-		.filter( function() {
+		} ).filter( function() {
 			var type = this.type;
 
 			// Use .is( ":disabled" ) so that fieldset[disabled] works
 			return this.name && !jQuery( this ).is( ":disabled" ) &&
 				rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
 				( this.checked || !rcheckableType.test( type ) );
-		} )
-		.map( function( _i, elem ) {
+		} ).map( function( _i, elem ) {
 			var val = jQuery( this ).val();
 
 			if ( val == null ) {
@@ -8387,12 +8393,6 @@ jQuery.offset = {
 			options.using.call( elem, props );
 
 		} else {
-			if ( typeof props.top === "number" ) {
-				props.top += "px";
-			}
-			if ( typeof props.left === "number" ) {
-				props.left += "px";
-			}
 			curElem.css( props );
 		}
 	}
@@ -8561,8 +8561,11 @@ jQuery.each( [ "top", "left" ], function( _i, prop ) {
 
 // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
 jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
-	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
-		function( defaultExtra, funcName ) {
+	jQuery.each( {
+		padding: "inner" + name,
+		content: type,
+		"": "outer" + name
+	}, function( defaultExtra, funcName ) {
 
 		// Margin is only for outerHeight, outerWidth
 		jQuery.fn[ funcName ] = function( margin, value ) {
@@ -8631,7 +8634,8 @@ jQuery.fn.extend( {
 	}
 } );
 
-jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+jQuery.each(
+	( "blur focus focusin focusout resize scroll click dblclick " +
 	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
 	"change select submit keydown keypress keyup contextmenu" ).split( " " ),
 	function( _i, name ) {
@@ -8642,7 +8646,8 @@ jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
 				this.on( name, null, data, fn ) :
 				this.trigger( name );
 		};
-	} );
+	}
+);
 
 
 

+ 12 - 9
src/static/templates/admin/base.hbs

@@ -15,14 +15,16 @@
             width: 48px;
             height: 48px;
         }
-        .navbar .vaultwarden-icon {
+        .vaultwarden-icon {
             height: 32px;
             width: auto;
-            margin: -5px -3px 0 0;
+            margin: -5px 0 0 0;
         }
     </style>
     <script src="{{urlpath}}/bwrs_static/identicon.js"></script>
     <script>
+        'use strict';
+
         function reload() { window.location.reload(); }
         function msg(text, reload_page = true) {
             text && alert(text);
@@ -78,19 +80,18 @@
             });
         }
     </script>
-
 </head>
 
 <body class="bg-light">
     <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
         <div class="container-xl">
-            <a class="navbar-brand" href="{{urlpath}}/admin"><img class="pr-1 vaultwarden-icon" src="{{urlpath}}/bwrs_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
-            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
+            <a class="navbar-brand" href="{{urlpath}}/admin"><img class="vaultwarden-icon" src="{{urlpath}}/bwrs_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
+            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
                     aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                 <span class="navbar-toggler-icon"></span>
             </button>
             <div class="collapse navbar-collapse" id="navbarCollapse">
-                <ul class="navbar-nav mr-auto">
+                <ul class="navbar-nav me-auto">
                 {{#if logged_in}}
                     <li class="nav-item">
                         <a class="nav-link" href="{{urlpath}}/admin">Settings</a>
@@ -121,17 +122,19 @@
 
     <!-- This script needs to be at the bottom, else it will fail! -->
     <script>
+        'use strict';
+
         // get current URL path and assign 'active' class to the correct nav-item
-        (function () {
+        (() => {
             var pathname = window.location.pathname;
             if (pathname === "") return;
             var navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
             if (navItem.length === 1) {
-                navItem[0].parentElement.className = navItem[0].parentElement.className + ' active';
+                navItem[0].className = navItem[0].className + ' active';
+                navItem[0].setAttribute('aria-current', 'page');
             }
         })();
     </script>
-    <!-- This script needs to be at the bottom, else it will fail! -->
     <script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script>
 </body>
 </html>

+ 112 - 79
src/static/templates/admin/diagnostics.hbs

@@ -7,37 +7,37 @@
             <div class="col-md">
                 <dl class="row">
                     <dt class="col-sm-5">Server Installed
-                        <span class="badge badge-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
-                        <span class="badge badge-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span>
-                        <span class="badge badge-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
+                        <span class="badge bg-success d-none" id="server-success" title="Latest version is installed.">Ok</span>
+                        <span class="badge bg-warning d-none" id="server-warning" title="There seems to be an update available.">Update</span>
+                        <span class="badge bg-info d-none" id="server-branch" title="This is a branched version.">Branched</span>
                     </dt>
                     <dd class="col-sm-7">
                         <span id="server-installed">{{version}}</span>
                     </dd>
                     <dt class="col-sm-5">Server Latest
-                        <span class="badge badge-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
+                        <span class="badge bg-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="server-latest">{{diagnostics.latest_release}}<span id="server-latest-commit" class="d-none">-{{diagnostics.latest_commit}}</span></span>
+                        <span id="server-latest">{{page_data.latest_release}}<span id="server-latest-commit" class="d-none">-{{page_data.latest_commit}}</span></span>
                     </dd>
-                    {{#if diagnostics.web_vault_enabled}}
+                    {{#if page_data.web_vault_enabled}}
                     <dt class="col-sm-5">Web Installed
-                        <span class="badge badge-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
-                        <span class="badge badge-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span>
+                        <span class="badge bg-success d-none" id="web-success" title="Latest version is installed.">Ok</span>
+                        <span class="badge bg-warning d-none" id="web-warning" title="There seems to be an update available.">Update</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="web-installed">{{diagnostics.web_vault_version}}</span>
+                        <span id="web-installed">{{page_data.web_vault_version}}</span>
                     </dd>
-                    {{#unless diagnostics.running_within_docker}}
+                    {{#unless page_data.running_within_docker}}
                     <dt class="col-sm-5">Web Latest
-                        <span class="badge badge-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span>
+                        <span class="badge bg-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="web-latest">{{diagnostics.latest_web_build}}</span>
+                        <span id="web-latest">{{page_data.latest_web_build}}</span>
                     </dd>
                     {{/unless}}
                     {{/if}}
-                    {{#unless diagnostics.web_vault_enabled}}
+                    {{#unless page_data.web_vault_enabled}}
                     <dt class="col-sm-5">Web Installed</dt>
                     <dd class="col-sm-7">
                         <span id="web-installed">Web Vault is disabled</span>
@@ -45,7 +45,7 @@
                     {{/unless}}
                     <dt class="col-sm-5">Database</dt>
                     <dd class="col-sm-7">
-                        <span><b>{{diagnostics.db_type}}:</b> {{diagnostics.db_version}}</span>
+                        <span><b>{{page_data.db_type}}:</b> {{page_data.db_version}}</span>
                     </dd>
                 </dl>
             </div>
@@ -57,96 +57,105 @@
                 <dl class="row">
                     <dt class="col-sm-5">Running within Docker</dt>
                     <dd class="col-sm-7">
-                    {{#if diagnostics.running_within_docker}}
+                    {{#if page_data.running_within_docker}}
                         <span class="d-block"><b>Yes</b></span>
                     {{/if}}
-                    {{#unless diagnostics.running_within_docker}}
+                    {{#unless page_data.running_within_docker}}
+                        <span class="d-block"><b>No</b></span>
+                    {{/unless}}
+                    </dd>
+                    <dt class="col-sm-5">Environment settings overridden</dt>
+                    <dd class="col-sm-7">
+                    {{#if page_data.overrides}}
+                        <span class="d-block" title="The following settings are overridden: {{page_data.overrides}}"><b>Yes</b></span>
+                    {{/if}}
+                    {{#unless page_data.overrides}}
                         <span class="d-block"><b>No</b></span>
                     {{/unless}}
                     </dd>
                     <dt class="col-sm-5">Uses a reverse proxy</dt>
                     <dd class="col-sm-7">
-                    {{#if diagnostics.ip_header_exists}}
+                    {{#if page_data.ip_header_exists}}
                         <span class="d-block" title="IP Header found."><b>Yes</b></span>
                     {{/if}}
-                    {{#unless diagnostics.ip_header_exists}}
+                    {{#unless page_data.ip_header_exists}}
                         <span class="d-block" title="No IP Header found."><b>No</b></span>
                     {{/unless}}
                     </dd>
                     {{!-- Only show this if the IP Header Exists --}}
-                    {{#if diagnostics.ip_header_exists}}
+                    {{#if page_data.ip_header_exists}}
                     <dt class="col-sm-5">IP header
-                    {{#if diagnostics.ip_header_match}}
-                        <span class="badge badge-success" title="IP_HEADER config seems to be valid.">Match</span>
+                    {{#if page_data.ip_header_match}}
+                        <span class="badge bg-success" title="IP_HEADER config seems to be valid.">Match</span>
                     {{/if}}
-                    {{#unless diagnostics.ip_header_match}}
-                        <span class="badge badge-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
+                    {{#unless page_data.ip_header_match}}
+                        <span class="badge bg-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
                     {{/unless}}
                     </dt>
                     <dd class="col-sm-7">
-                    {{#if diagnostics.ip_header_match}}
-                        <span class="d-block"><b>Config/Server:</b> {{ diagnostics.ip_header_name }}</span>
+                    {{#if page_data.ip_header_match}}
+                        <span class="d-block"><b>Config/Server:</b> {{ page_data.ip_header_name }}</span>
                     {{/if}}
-                    {{#unless diagnostics.ip_header_match}}
-                        <span class="d-block"><b>Config:</b> {{ diagnostics.ip_header_config }}</span>
-                        <span class="d-block"><b>Server:</b> {{ diagnostics.ip_header_name }}</span>
+                    {{#unless page_data.ip_header_match}}
+                        <span class="d-block"><b>Config:</b> {{ page_data.ip_header_config }}</span>
+                        <span class="d-block"><b>Server:</b> {{ page_data.ip_header_name }}</span>
                     {{/unless}}
                     </dd>
                     {{/if}}
                     {{!-- End if IP Header Exists --}}
                     <dt class="col-sm-5">Internet access
-                    {{#if diagnostics.has_http_access}}
-                        <span class="badge badge-success" title="We have internet access!">Ok</span>
+                    {{#if page_data.has_http_access}}
+                        <span class="badge bg-success" title="We have internet access!">Ok</span>
                     {{/if}}
-                    {{#unless diagnostics.has_http_access}}
-                        <span class="badge badge-danger" title="There seems to be no internet access. Please fix.">Error</span>
+                    {{#unless page_data.has_http_access}}
+                        <span class="badge bg-danger" title="There seems to be no internet access. Please fix.">Error</span>
                     {{/unless}}
                     </dt>
                     <dd class="col-sm-7">
-                    {{#if diagnostics.has_http_access}}
+                    {{#if page_data.has_http_access}}
                         <span class="d-block"><b>Yes</b></span>
                     {{/if}}
-                    {{#unless diagnostics.has_http_access}}
+                    {{#unless page_data.has_http_access}}
                         <span class="d-block"><b>No</b></span>
                     {{/unless}}
                     </dd>
                     <dt class="col-sm-5">Internet access via a proxy</dt>
                     <dd class="col-sm-7">
-                    {{#if diagnostics.uses_proxy}}
+                    {{#if page_data.uses_proxy}}
                         <span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span>
                     {{/if}}
-                    {{#unless diagnostics.uses_proxy}}
+                    {{#unless page_data.uses_proxy}}
                         <span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span>
                     {{/unless}}
                     </dd>
                     <dt class="col-sm-5">DNS (github.com)
-                        <span class="badge badge-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
-                        <span class="badge badge-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
+                        <span class="badge bg-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span>
+                        <span class="badge bg-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="dns-resolved">{{diagnostics.dns_resolved}}</span>
+                        <span id="dns-resolved">{{page_data.dns_resolved}}</span>
                     </dd>
                     <dt class="col-sm-5">Date & Time (Local)</dt>
                     <dd class="col-sm-7">
-                        <span><b>Server:</b> {{diagnostics.server_time_local}}</span>
+                        <span><b>Server:</b> {{page_data.server_time_local}}</span>
                     </dd>
                     <dt class="col-sm-5">Date & Time (UTC)
-                        <span class="badge badge-success d-none" id="time-success" title="Time offsets seem to be correct.">Ok</span>
-                        <span class="badge badge-danger d-none" id="time-warning" title="Time offsets are too mouch at drift.">Error</span>
+                        <span class="badge bg-success d-none" id="time-success" title="Time offsets seem to be correct.">Ok</span>
+                        <span class="badge bg-danger d-none" id="time-warning" title="Time offsets are too mouch at drift.">Error</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{diagnostics.server_time}}</span></span>
+                        <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span>
                         <span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span>
                     </dd>
 
                     <dt class="col-sm-5">Domain configuration
-                        <span class="badge badge-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
-                        <span class="badge badge-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
-                        <span class="badge badge-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
-                        <span class="badge badge-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span>
+                        <span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
+                        <span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
+                        <span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
+                        <span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span>
                     </dt>
                     <dd class="col-sm-7">
-                        <span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{diagnostics.admin_url}}</span></span>
+                        <span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{page_data.admin_url}}</span></span>
                         <span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span>
                     </dd>
                 </dl>
@@ -173,10 +182,17 @@
                     <dt class="col-sm-3">
                         <button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button>
                         <br><br>
-                        <button type="button" id="copy-support" class="btn btn-info d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button>
+                        <button type="button" id="copy-support" class="btn btn-info mb-3 d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button>
+                        <div class="toast-container position-absolute float-start" style="width: 15rem;">
+                            <div id="toastClipboardCopy" class="toast fade hide" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="1500">
+                                <div class="toast-body">
+                                    Copied to clipboard!
+                                </div>
+                            </div>
+                        </div>
                     </dt>
                     <dd class="col-sm-9">
-                        <pre id="support-string" class="pre-scrollable d-none" style="width: 100%; height: 16em; size: 0.6em; border: 1px solid; padding: 4px;"></pre>
+                        <pre id="support-string" class="pre-scrollable d-none w-100 border p-2" style="height: 16rem;"></pre>
                     </dd>
                 </dl>
             </div>
@@ -185,10 +201,13 @@
 </main>
 
 <script>
-    dnsCheck = false;
-    timeCheck = false;
-    domainCheck = false;
-    httpsCheck = false;
+    'use strict';
+
+    var dnsCheck = false;
+    var timeCheck = false;
+    var domainCheck = false;
+    var httpsCheck = false;
+
     (() => {
         // ================================
         // Date & Time Check
@@ -203,7 +222,10 @@
         document.getElementById("time-browser-string").innerText = browserUTC;
 
         const serverUTC = document.getElementById("time-server-string").innerText;
-        const timeDrift = (Date.parse(serverUTC) - Date.parse(browserUTC)) / 1000;
+        const timeDrift = (
+                Date.parse(serverUTC.replace(' ', 'T').replace(' UTC', '')) -
+                Date.parse(browserUTC.replace(' ', 'T').replace(' UTC', ''))
+            ) / 1000;
         if (timeDrift > 30 || timeDrift < -30) {
             document.getElementById('time-warning').classList.remove('d-none');
         } else {
@@ -233,7 +255,7 @@
         const webInstalled = document.getElementById('web-installed').innerText;
         checkVersions('server', serverInstalled, serverLatest, serverLatestCommit);
 
-        {{#unless diagnostics.running_within_docker}}
+        {{#unless page_data.running_within_docker}}
         const webLatest = document.getElementById('web-latest').innerText;
         checkVersions('web', webInstalled, webLatest);
         {{/unless}}
@@ -303,30 +325,38 @@
     // ================================
     // Generate support string to be pasted on github or the forum
     async function generateSupportString() {
-        supportString = "### Your environment (Generated via diagnostics page)\n";
+        let supportString = "### Your environment (Generated via diagnostics page)\n";
 
         supportString += "* Vaultwarden version: v{{ version }}\n";
-        supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n";
-        supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n";
-        supportString += "* Uses a reverse proxy: {{ diagnostics.ip_header_exists }}\n";
-        {{#if diagnostics.ip_header_exists}}
-        supportString += "* IP Header check: {{ diagnostics.ip_header_match }} ({{ diagnostics.ip_header_name }})\n";
+        supportString += "* Web-vault version: v{{ page_data.web_vault_version }}\n";
+        supportString += "* Running within Docker: {{ page_data.running_within_docker }}\n";
+        supportString += "* Environment settings overridden: ";
+        {{#if page_data.overrides}}
+            supportString += "true\n"
+        {{else}}
+            supportString += "false\n"
+        {{/if}}
+        supportString += "* Uses a reverse proxy: {{ page_data.ip_header_exists }}\n";
+        {{#if page_data.ip_header_exists}}
+        supportString += "* IP Header check: {{ page_data.ip_header_match }} ({{ page_data.ip_header_name }})\n";
         {{/if}}
-        supportString += "* Internet access: {{ diagnostics.has_http_access }}\n";
-        supportString += "* Internet access via a proxy: {{ diagnostics.uses_proxy }}\n";
+        supportString += "* Internet access: {{ page_data.has_http_access }}\n";
+        supportString += "* Internet access via a proxy: {{ page_data.uses_proxy }}\n";
         supportString += "* DNS Check: " + dnsCheck + "\n";
         supportString += "* Time Check: " + timeCheck + "\n";
         supportString += "* Domain Configuration Check: " + domainCheck + "\n";
         supportString += "* HTTPS Check: " + httpsCheck + "\n";
-        supportString += "* Database type: {{ diagnostics.db_type }}\n";
-        supportString += "* Database version: {{ diagnostics.db_version }}\n";
+        supportString += "* Database type: {{ page_data.db_type }}\n";
+        supportString += "* Database version: {{ page_data.db_version }}\n";
         supportString += "* Clients used: \n";
         supportString += "* Reverse proxy and version: \n";
         supportString += "* Other relevant information: \n";
 
-        jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config');
-        configJson = await jsonResponse.json();
-        supportString += "\n### Config (Generated via diagnostics page)\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n";
+        let jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config');
+        const configJson = await jsonResponse.json();
+        supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n"
+        supportString += "\n**Environment settings which are overridden:** {{page_data.overrides}}\n"
+        supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
 
         document.getElementById('support-string').innerText = supportString;
         document.getElementById('support-string').classList.remove('d-none');
@@ -334,16 +364,19 @@
     }
 
     function copyToClipboard() {
-        const str = document.getElementById('support-string').innerText;
-        const el = document.createElement('textarea');
-        el.value = str;
-        el.setAttribute('readonly', '');
-        el.style.position = 'absolute';
-        el.style.left = '-9999px';
-        document.body.appendChild(el);
-        el.select();
+        const supportStr = document.getElementById('support-string').innerText;
+        const tmpCopyEl = document.createElement('textarea');
+
+        tmpCopyEl.setAttribute('id', 'copy-support-string');
+        tmpCopyEl.setAttribute('readonly', '');
+        tmpCopyEl.value = supportStr;
+        tmpCopyEl.style.position = 'absolute';
+        tmpCopyEl.style.left = '-9999px';
+        document.body.appendChild(tmpCopyEl);
+        tmpCopyEl.select();
         document.execCommand('copy');
-        document.body.removeChild(el);
-    }
+        tmpCopyEl.remove();
 
+        new BSN.Toast('#toastClipboardCopy').show();
+    }
 </script>

+ 11 - 11
src/static/templates/admin/organizations.hbs

@@ -1,7 +1,6 @@
 <main class="container-xl">
     <div id="organizations-block" class="my-3 p-3 bg-white rounded shadow">
         <h6 class="border-bottom pb-2 mb-3">Organizations</h6>
-
         <div class="table-responsive-xl small">
             <table id="orgs-table" class="table table-sm table-striped table-hover">
                 <thead>
@@ -10,19 +9,19 @@
                         <th>Users</th>
                         <th>Items</th>
                         <th>Attachments</th>
-                        <th style="width: 120px; min-width: 120px;">Actions</th>
+                        <th style="width: 130px; min-width: 130px;">Actions</th>
                     </tr>
                 </thead>
                 <tbody>
-                    {{#each organizations}}
+                    {{#each page_data}}
                     <tr>
                         <td>
-                            <img class="mr-2 float-left rounded identicon" data-src="{{Id}}">
-                            <div class="float-left">
+                            <img class="float-start me-2 rounded identicon" data-src="{{Id}}">
+                            <div class="float-start">
                                 <strong>{{Name}}</strong>
-                                <span class="mr-2">({{BillingEmail}})</span>
+                                <span class="me-2">({{BillingEmail}})</span>
                                 <span class="d-block">
-                                    <span class="badge badge-success">{{Id}}</span>
+                                    <span class="badge bg-success">{{Id}}</span>
                                 </span>
                             </div>
                         </td>
@@ -38,7 +37,7 @@
                             <span class="d-block"><strong>Size:</strong> {{attachment_size}}</span>
                             {{/if}}
                         </td>
-                        <td style="font-size: 90%; text-align: right; padding-right: 15px">
+                        <td class="text-end pe-2 small">
                             <a class="d-block" href="#" onclick='deleteOrganization({{jsesc Id}}, {{jsesc Name}}, {{jsesc BillingEmail}})'>Delete Organization</a>
                         </td>
                     </tr>
@@ -46,14 +45,15 @@
                 </tbody>
             </table>
         </div>
-
     </div>
 </main>
 
 <link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" />
-<script src="{{urlpath}}/bwrs_static/jquery-3.5.1.slim.js"></script>
+<script src="{{urlpath}}/bwrs_static/jquery-3.6.0.slim.js"></script>
 <script src="{{urlpath}}/bwrs_static/datatables.js"></script>
 <script>
+    'use strict';
+
     function deleteOrganization(id, name, billing_email) {
         // First make sure the user wants to delete this organization
         var continueDelete = confirm("WARNING: All data of this organization ("+ name +") will be lost!\nMake sure you have a backup, this cannot be undone!");
@@ -79,7 +79,7 @@
         }
     })();
 
-    document.addEventListener("DOMContentLoaded", function(event) {
+    document.addEventListener("DOMContentLoaded", function() {
         $('#orgs-table').DataTable({
             "responsive": true,
             "lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],

+ 59 - 44
src/static/templates/admin/settings.hbs

@@ -3,34 +3,32 @@
         <div>
             <h6 class="text-white mb-3">Configuration</h6>
             <div class="small text-white mb-3">
-                NOTE: The settings here override the environment variables. Once saved, it's recommended to stop setting
-                them to avoid confusion. This does not apply to the read-only section, which can only be set through the
-                environment.
+                <span class="font-weight-bolder">NOTE:</span> The settings here override the environment variables. Once saved, it's recommended to stop setting them to avoid confusion.<br>
+                This does not apply to the read-only section, which can only be set via environment variables.<br>
+                Settings which are overridden are shown with <span class="is-overridden-true">double underscores</span>.
             </div>
 
-            <form class="form accordion" id="config-form" onsubmit="saveConfig(); return false;">
+            <form class="form needs-validation" id="config-form" onsubmit="saveConfig(); return false;" novalidate>
                 {{#each config}}
                 {{#if groupdoc}}
                 <div class="card bg-light mb-3">
-                    <div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
-                            data-target="#g_{{group}}">{{groupdoc}}</button></div>
-                    <div id="g_{{group}}" class="card-body collapse" data-parent="#config-form">
+                    <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">
+                        <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button>
+                    </div>
+                    <div id="g_{{group}}" class="card-body collapse">
                         {{#each elements}}
                         {{#if editable}}
-                        <div class="form-group row align-items-center" title="[{{name}}] {{doc.description}}">
+                        <div class="row my-2 align-items-center is-overridden-{{overridden}}" title="[{{name}}] {{doc.description}}">
                             {{#case type "text" "number" "password"}}
                             <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
-                            <div class="col-sm-8 input-group">
+                            <div class="col-sm-8">
+                                <div class="input-group">
                                 <input class="form-control conf-{{type}}" id="input_{{name}}" type="{{type}}"
-                                    name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"
-                                    {{/if}}>
-
+                                    name="{{name}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}"{{/if}}>
                                 {{#case type "password"}}
-                                <div class="input-group-append">
-                                    <button class="btn btn-outline-secondary" type="button"
-                                        onclick="toggleVis('input_{{name}}');">Show/hide</button>
-                                </div>
+                                    <button class="btn btn-outline-secondary input-group-text" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
                                 {{/case}}
+                                </div>
                             </div>
                             {{/case}}
                             {{#case type "checkbox"}}
@@ -48,13 +46,12 @@
                         {{/if}}
                         {{/each}}
                         {{#case group "smtp"}}
-                            <div class="form-group row align-items-center pt-3 border-top" title="Send a test email to given email address">
+                            <div class="row my-2 align-items-center pt-3 border-top" title="Send a test email to given email address">
                                 <label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
                                 <div class="col-sm-8 input-group">
-                                    <input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email">
-                                    <div class="input-group-append">
-                                        <button type="button" class="btn btn-outline-primary" onclick="smtpTest(); return false;">Send test email</button>
-                                    </div>
+                                    <input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required>
+                                    <button type="button" class="btn btn-outline-primary input-group-text" onclick="smtpTest(); return false;">Send test email</button>
+                                    <div class="invalid-tooltip">Please provide a valid email address</div>
                                 </div>
                             </div>
                         {{/case}}
@@ -64,9 +61,11 @@
                 {{/each}}
 
                 <div class="card bg-light mb-3">
-                    <div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
-                            data-target="#g_readonly">Read-Only Config</button></div>
-                    <div id="g_readonly" class="card-body collapse" data-parent="#config-form">
+                    <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_readonly">
+                        <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_readonly">Read-Only Config</button>
+                    </div>
+
+                    <div id="g_readonly" class="card-body collapse">
                         <div class="small mb-3">
                             NOTE: These options can't be modified in the editor because they would require the server
                             to be restarted. To modify them, you need to set the correct environment variables when
@@ -76,19 +75,17 @@
                         {{#each config}}
                         {{#each elements}}
                         {{#unless editable}}
-                        <div class="form-group row align-items-center" title="[{{name}}] {{doc.description}}">
+                        <div class="row my-2 align-items-center" title="[{{name}}] {{doc.description}}">
                             {{#case type "text" "number" "password"}}
                             <label for="input_{{name}}" class="col-sm-3 col-form-label">{{doc.name}}</label>
-                            <div class="col-sm-8 input-group">
+                            <div class="col-sm-8">
+                                <div class="input-group">
                                 <input readonly class="form-control" id="input_{{name}}" type="{{type}}"
                                     value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
-
                                 {{#case type "password"}}
-                                <div class="input-group-append">
-                                    <button class="btn btn-outline-secondary" type="button"
-                                        onclick="toggleVis('input_{{name}}');">Show/hide</button>
-                                </div>
+                                    <button class="btn btn-outline-secondary" type="button" onclick="toggleVis('input_{{name}}');">Show/hide</button>
                                 {{/case}}
+                                </div>
                             </div>
                             {{/case}}
                             {{#case type "checkbox"}}
@@ -112,9 +109,10 @@
 
                 {{#if can_backup}}
                 <div class="card bg-light mb-3">
-                    <div class="card-header"><button type="button" class="btn btn-link collapsed" data-toggle="collapse"
-                            data-target="#g_database">Backup Database</button></div>
-                    <div id="g_database" class="card-body collapse" data-parent="#config-form">
+                    <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_database">
+                        <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button>
+                    </div>
+                    <div id="g_database" class="card-body collapse">
                         <div class="small mb-3">
                             WARNING: This function only creates a backup copy of the SQLite database.
                             This does not include any configuration or file attachment data that may
@@ -128,7 +126,7 @@
                 {{/if}}
 
                 <button type="submit" class="btn btn-primary">Save</button>
-                <button type="button" class="btn btn-danger float-right" onclick="deleteConf();">Reset defaults</button>
+                <button type="button" class="btn btn-danger float-end" onclick="deleteConf();">Reset defaults</button>
             </form>
         </div>
     </div>
@@ -139,16 +137,34 @@
         /* Most modern browsers support this now. */
         color: orangered;
     }
+
+    .is-overridden-true {
+        text-decoration: underline double;
+    }
 </style>
 
 <script>
+    'use strict';
+
     function smtpTest() {
         if (formHasChanges(config_form)) {
+            event.preventDefault();
+            event.stopPropagation();
             alert("Config has been changed but not yet saved.\nPlease save the changes first before sending a test email.");
             return false;
         }
-        test_email = document.getElementById("smtp-test-email");
-        data = JSON.stringify({ "email": test_email.value });
+
+        let test_email = document.getElementById("smtp-test-email");
+
+        // Do a very very basic email address check.
+        if (test_email.value.match(/\S+@\S+/i) === null) {
+            test_email.parentElement.classList.add('was-validated');
+            event.preventDefault();
+            event.stopPropagation();
+            return false;
+        }
+
+        const data = JSON.stringify({ "email": test_email.value });
         _post("{{urlpath}}/admin/test/smtp/",
             "SMTP Test email sent correctly",
             "Error sending SMTP test email", data, false);
@@ -157,21 +173,21 @@
     function getFormData() {
         let data = {};
 
-        document.querySelectorAll(".conf-checkbox").forEach(function (e, i) {
+        document.querySelectorAll(".conf-checkbox").forEach(function (e) {
             data[e.name] = e.checked;
         });
 
-        document.querySelectorAll(".conf-number").forEach(function (e, i) {
+        document.querySelectorAll(".conf-number").forEach(function (e) {
             data[e.name] = e.value ? +e.value : null;
         });
 
-        document.querySelectorAll(".conf-text, .conf-password").forEach(function (e, i) {
+        document.querySelectorAll(".conf-text, .conf-password").forEach(function (e) {
             data[e.name] = e.value || null;
         });
         return data;
     }
     function saveConfig() {
-        data = JSON.stringify(getFormData());
+        const data = JSON.stringify(getFormData());
         _post("{{urlpath}}/admin/config/", "Config saved correctly",
             "Error saving config", data);
         return false;
@@ -198,10 +214,10 @@
     function masterCheck(check_id, inputs_query) {
         function onChanged(checkbox, inputs_query) {
             return function _fn() {
-                document.querySelectorAll(inputs_query).forEach(function (e, i) { e.disabled = !checkbox.checked; });
+                document.querySelectorAll(inputs_query).forEach(function (e) { e.disabled = !checkbox.checked; });
                 checkbox.disabled = false;
             };
-        };
+        }
 
         const checkbox = document.getElementById(check_id);
         const onChange = onChanged(checkbox, inputs_query);
@@ -238,7 +254,6 @@
         Array.from(risk_el).forEach((el) => {
             if (el.innerText.toLowerCase().includes('risks') ) {
                 el.parentElement.className += ' alert-danger'
-                console.log(el)
             }
         });
     }

+ 27 - 27
src/static/templates/admin/users.hbs

@@ -7,34 +7,34 @@
                 <thead>
                     <tr>
                         <th>User</th>
-                        <th style="width:65px; min-width: 65px;">Created at</th>
-                        <th style="width:70px; min-width: 65px;">Last Active</th>
-                        <th style="width:35px; min-width: 35px;">Items</th>
+                        <th style="width: 85px; min-width: 70px;">Created at</th>
+                        <th style="width: 85px; min-width: 70px;">Last Active</th>
+                        <th style="width: 35px; min-width: 35px;">Items</th>
                         <th>Attachments</th>
                         <th style="min-width: 120px;">Organizations</th>
-                        <th style="width: 120px; min-width: 120px;">Actions</th>
+                        <th style="width: 130px; min-width: 130px;">Actions</th>
                     </tr>
                 </thead>
                 <tbody>
-                    {{#each users}}
+                    {{#each page_data}}
                     <tr>
                         <td>
-                            <img class="float-left mr-2 rounded identicon" data-src="{{Email}}">
-                            <div class="float-left">
+                            <img class="float-start me-2 rounded identicon" data-src="{{Email}}">
+                            <div class="float-start">
                                 <strong>{{Name}}</strong>
                                 <span class="d-block">{{Email}}</span>
                                 <span class="d-block">
                                     {{#unless user_enabled}}
-                                        <span class="badge badge-danger mr-2" title="User is disabled">Disabled</span>
+                                        <span class="badge bg-danger me-2" title="User is disabled">Disabled</span>
                                     {{/unless}}
                                     {{#if TwoFactorEnabled}}
-                                        <span class="badge badge-success mr-2" title="2FA is enabled">2FA</span>
+                                        <span class="badge bg-success me-2" title="2FA is enabled">2FA</span>
                                     {{/if}}
                                     {{#case _Status 1}}
-                                        <span class="badge badge-warning mr-2" title="User is invited">Invited</span>
+                                        <span class="badge bg-warning me-2" title="User is invited">Invited</span>
                                     {{/case}}
                                     {{#if EmailVerified}}
-                                        <span class="badge badge-success mr-2" title="Email has been verified">Verified</span>
+                                        <span class="badge bg-success me-2" title="Email has been verified">Verified</span>
                                     {{/if}}
                                 </span>
                             </div>
@@ -57,11 +57,11 @@
                         <td>
                             <div class="overflow-auto" style="max-height: 120px;">
                             {{#each Organizations}}
-                            <button class="badge badge-primary" data-toggle="modal" data-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button>
+                            <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button>
                             {{/each}}
                             </div>
                         </td>
-                        <td style="font-size: 90%; text-align: right; padding-right: 15px">
+                        <td class="text-end pe-2 small">
                             {{#if TwoFactorEnabled}}
                             <a class="d-block" href="#" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</a>
                             {{/if}}
@@ -85,7 +85,7 @@
                 Force clients to resync
             </button>
 
-            <button type="button" class="btn btn-sm btn-primary float-right" onclick="reload();">Reload users</button>
+            <button type="button" class="btn btn-sm btn-primary float-end" onclick="reload();">Reload users</button>
         </div>
     </div>
 
@@ -94,8 +94,8 @@
             <h6 class="mb-0 text-white">Invite User</h6>
             <small>Email:</small>
 
-            <form class="form-inline" id="invite-form" onsubmit="inviteUser(); return false;">
-                <input type="email" class="form-control w-50 mr-2" id="email-invite" placeholder="Enter email">
+            <form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;">
+                <input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required>
                 <button type="submit" class="btn btn-primary">Invite</button>
             </form>
         </div>
@@ -106,9 +106,7 @@
             <div class="modal-content">
                 <div class="modal-header">
                     <h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
-                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                        <span aria-hidden="true">&times;</span>
-                    </button>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                 </div>
                 <form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;">
                     <input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value="">
@@ -128,7 +126,7 @@
                         </div>
                     </div>
                     <div class="modal-footer">
-                        <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">Cancel</button>
+                        <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
                         <button type="submit" class="btn btn-sm btn-primary">Change Role</button>
                     </div>
                 </form>
@@ -138,9 +136,11 @@
 </main>
 
 <link rel="stylesheet" href="{{urlpath}}/bwrs_static/datatables.css" />
-<script src="{{urlpath}}/bwrs_static/jquery-3.5.1.slim.js"></script>
+<script src="{{urlpath}}/bwrs_static/jquery-3.6.0.slim.js"></script>
 <script src="{{urlpath}}/bwrs_static/datatables.js"></script>
 <script>
+    'use strict';
+
     function deleteUser(id, mail) {
         var input_mail = prompt("To delete user '" + mail + "', please type the email below")
         if (input_mail != null) {
@@ -191,8 +191,8 @@
         return false;
     }
     function inviteUser() {
-        inv = document.getElementById("email-invite");
-        data = JSON.stringify({ "email": inv.value });
+        const inv = document.getElementById("email-invite");
+        const data = JSON.stringify({ "email": inv.value });
         inv.value = "";
         _post("{{urlpath}}/admin/invite/", "User invited correctly",
             "Error inviting user", data);
@@ -212,7 +212,7 @@
         }
     })();
 
-    document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
+    document.querySelectorAll("[data-orgtype]").forEach(function (e) {
         let orgtype = OrgTypes[e.dataset.orgtype];
         e.style.backgroundColor = orgtype.color;
         e.title = orgtype.name;
@@ -225,7 +225,7 @@
             let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim();
             if ( sortDate !== '' ) {
                 let dtParts = sortDate.split(' ');
-                var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : [00,00,00];
+                var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : ['00','00','00'];
                 var dateParts = dtParts[0].split('-');
                 x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1;
                 if ( isNaN(x) ) {
@@ -246,7 +246,7 @@
         }
     });
 
-    document.addEventListener("DOMContentLoaded", function(event) {
+    document.addEventListener("DOMContentLoaded", function() {
         $('#users-table').DataTable({
             "responsive": true,
             "lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
@@ -275,7 +275,7 @@
     }, false);
 
     // Prevent accidental submission of the form with valid elements after the modal has been hidden.
-    userOrgTypeDialog.addEventListener('hide.bs.modal', function(event){
+    userOrgTypeDialog.addEventListener('hide.bs.modal', function(){
         document.getElementById("userOrgTypeDialogTitle").innerHTML = '';
         document.getElementById("userOrgTypeUserUuid").value = '';
         document.getElementById("userOrgTypeOrgUuid").value = '';

Some files were not shown because too many files changed in this diff