From 3077afb9d1a403bcfe664f4451b685abf44adb73 Mon Sep 17 00:00:00 2001 From: Jonas Pardeyke Date: Wed, 1 Jul 2026 08:43:39 +0200 Subject: [PATCH 1/5] Add Bootstrap 6 (v6.0.0.alpha1) support Tracks the upstream twbs/bootstrap `v6-dev` branch. - SCSS: Bootstrap 6 uses the Sass module system, so consumers now use `@use "bootstrap"` (with `... with (...)` for overrides). Requires a Dart Sass engine; sassc-rails stays a listed engine option but cannot compile the v6 stylesheets. - JS: ES-module only (no UMD / no `window.bootstrap` global), loaded via importmaps. Ships the self-contained `bootstrap.bundle.{js,min.js}` (inlines Floating UI + vanilla-calendar-pro) plus the individual modules, and vendors a self-contained `@floating-ui/dom` build (`floating-ui.js`). Dropped the popper_js runtime dependency. - Updater: fixed exact-branch ls-remote resolution, follows only relative ES-module imports, ships the dist bundles, vendors Floating UI, and adds a `rake update_scss` (skip-JS) task. - Added a headless-browser JavaScript smoke test. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 26 + Gemfile | 1 - README.md | 113 +- Rakefile | 9 +- .../bootstrap-global-this-define.js | 6 - .../bootstrap-global-this-undefine.js | 2 - assets/javascripts/bootstrap-sprockets.js | 28 - assets/javascripts/bootstrap.bundle.js | 9237 +++++++++++++ assets/javascripts/bootstrap.bundle.min.js | 8 + assets/javascripts/bootstrap.js | 11122 ++++++++++------ assets/javascripts/bootstrap.min.js | 6 +- assets/javascripts/bootstrap/alert.js | 122 +- .../javascripts/bootstrap/base-component.js | 144 +- assets/javascripts/bootstrap/button.js | 119 +- assets/javascripts/bootstrap/carousel.js | 1081 +- assets/javascripts/bootstrap/chips.js | 584 + assets/javascripts/bootstrap/collapse.js | 426 +- assets/javascripts/bootstrap/combobox.js | 397 + assets/javascripts/bootstrap/datepicker.js | 439 + assets/javascripts/bootstrap/dialog-base.js | 264 + assets/javascripts/bootstrap/dialog.js | 166 + assets/javascripts/bootstrap/dom/data.js | 104 +- .../bootstrap/dom/event-handler.js | 378 +- .../javascripts/bootstrap/dom/manipulator.js | 118 +- .../bootstrap/dom/selector-engine.js | 179 +- assets/javascripts/bootstrap/drawer.js | 165 + assets/javascripts/bootstrap/dropdown.js | 401 - assets/javascripts/bootstrap/menu.js | 811 ++ assets/javascripts/bootstrap/modal.js | 319 - assets/javascripts/bootstrap/nav-overflow.js | 309 + assets/javascripts/bootstrap/offcanvas.js | 245 - assets/javascripts/bootstrap/otp-input.js | 265 + assets/javascripts/bootstrap/popover.js | 162 +- assets/javascripts/bootstrap/range.js | 213 + assets/javascripts/bootstrap/scrollspy.js | 461 +- assets/javascripts/bootstrap/strength.js | 240 + assets/javascripts/bootstrap/tab.js | 463 +- assets/javascripts/bootstrap/toast.js | 332 +- assets/javascripts/bootstrap/toggler.js | 93 + assets/javascripts/bootstrap/tooltip.js | 1108 +- assets/javascripts/bootstrap/util/backdrop.js | 138 - .../bootstrap/util/component-functions.js | 86 +- assets/javascripts/bootstrap/util/config.js | 105 +- .../javascripts/bootstrap/util/floating-ui.js | 137 + .../javascripts/bootstrap/util/focustrap.js | 112 - assets/javascripts/bootstrap/util/index.js | 486 +- .../javascripts/bootstrap/util/sanitizer.js | 187 +- .../javascripts/bootstrap/util/scrollbar.js | 112 - assets/javascripts/bootstrap/util/swipe.js | 239 +- .../bootstrap/util/template-factory.js | 253 +- assets/javascripts/floating-ui.js | 3 + assets/stylesheets/_bootstrap-grid.scss | 62 - assets/stylesheets/_bootstrap-reboot.scss | 10 - assets/stylesheets/_bootstrap-utilities.scss | 19 - assets/stylesheets/_bootstrap.scss | 83 +- assets/stylesheets/bootstrap/_accordion.scss | 266 +- assets/stylesheets/bootstrap/_alert.scss | 111 +- assets/stylesheets/bootstrap/_avatar.scss | 159 + assets/stylesheets/bootstrap/_badge.scss | 122 +- assets/stylesheets/bootstrap/_banner.scss | 7 + assets/stylesheets/bootstrap/_breadcrumb.scss | 116 +- .../stylesheets/bootstrap/_button-group.scss | 147 - assets/stylesheets/bootstrap/_buttons.scss | 216 - assets/stylesheets/bootstrap/_card.scss | 447 +- assets/stylesheets/bootstrap/_carousel.scss | 427 +- assets/stylesheets/bootstrap/_chip.scss | 148 + assets/stylesheets/bootstrap/_close.scss | 66 - assets/stylesheets/bootstrap/_colors.scss | 102 + assets/stylesheets/bootstrap/_config.scss | 348 + assets/stylesheets/bootstrap/_containers.scss | 41 - assets/stylesheets/bootstrap/_datepicker.scss | 415 + assets/stylesheets/bootstrap/_dialog.scss | 289 + assets/stylesheets/bootstrap/_drawer.scss | 296 + assets/stylesheets/bootstrap/_dropdown.scss | 250 - assets/stylesheets/bootstrap/_forms.scss | 9 - assets/stylesheets/bootstrap/_functions.scss | 218 +- assets/stylesheets/bootstrap/_grid.scss | 39 - assets/stylesheets/bootstrap/_helpers.scss | 12 - assets/stylesheets/bootstrap/_images.scss | 42 - assets/stylesheets/bootstrap/_list-group.scss | 324 +- assets/stylesheets/bootstrap/_maps.scss | 174 - assets/stylesheets/bootstrap/_menu.scss | 289 + assets/stylesheets/bootstrap/_mixins.scss | 42 - assets/stylesheets/bootstrap/_modal.scss | 240 - .../stylesheets/bootstrap/_nav-overflow.scss | 39 + assets/stylesheets/bootstrap/_nav.scss | 396 +- assets/stylesheets/bootstrap/_navbar.scss | 529 +- assets/stylesheets/bootstrap/_offcanvas.scss | 147 - assets/stylesheets/bootstrap/_pagination.scss | 206 +- .../stylesheets/bootstrap/_placeholder.scss | 72 + .../stylesheets/bootstrap/_placeholders.scss | 51 - assets/stylesheets/bootstrap/_popover.scss | 351 +- assets/stylesheets/bootstrap/_progress.scss | 127 +- assets/stylesheets/bootstrap/_reboot.scss | 617 - assets/stylesheets/bootstrap/_root.scss | 345 +- assets/stylesheets/bootstrap/_spinner.scss | 118 + assets/stylesheets/bootstrap/_spinners.scss | 86 - assets/stylesheets/bootstrap/_stepper.scss | 156 + assets/stylesheets/bootstrap/_tables.scss | 171 - assets/stylesheets/bootstrap/_theme.scss | 217 + assets/stylesheets/bootstrap/_toasts.scss | 146 +- assets/stylesheets/bootstrap/_tooltip.scss | 208 +- .../stylesheets/bootstrap/_transitions.scss | 3 + assets/stylesheets/bootstrap/_type.scss | 106 - assets/stylesheets/bootstrap/_utilities.scss | 718 +- .../bootstrap/_variables-dark.scss | 102 - assets/stylesheets/bootstrap/_variables.scss | 1753 --- .../stylesheets/bootstrap/bootstrap-grid.scss | 68 + .../bootstrap/bootstrap-reboot.scss | 6 + .../bootstrap/bootstrap-utilities.scss | 13 + .../bootstrap/buttons/_button-group.scss | 135 + .../bootstrap/buttons/_button.scss | 451 + .../stylesheets/bootstrap/buttons/_close.scss | 63 + .../stylesheets/bootstrap/buttons/index.scss | 3 + .../bootstrap/content/_images.scss | 74 + .../stylesheets/bootstrap/content/_prose.scss | 143 + .../bootstrap/content/_reboot.scss | 631 + .../bootstrap/content/_tables.scss | 255 + .../stylesheets/bootstrap/content/_type.scss | 86 + .../stylesheets/bootstrap/content/index.scss | 5 + .../stylesheets/bootstrap/forms/_check.scss | 105 + .../bootstrap/forms/_chip-input.scss | 74 + .../bootstrap/forms/_combobox.scss | 71 + .../bootstrap/forms/_floating-labels.scss | 190 +- .../bootstrap/forms/_form-adorn.scss | 68 + .../bootstrap/forms/_form-check.scss | 189 - .../bootstrap/forms/_form-control.scss | 438 +- .../bootstrap/forms/_form-field.scss | 79 + .../bootstrap/forms/_form-range.scss | 263 +- .../bootstrap/forms/_form-select.scss | 80 - .../bootstrap/forms/_form-text.scss | 39 +- .../bootstrap/forms/_input-group.scss | 225 +- .../stylesheets/bootstrap/forms/_labels.scss | 75 +- .../bootstrap/forms/_otp-input.scss | 159 + .../stylesheets/bootstrap/forms/_radio.scss | 88 + .../bootstrap/forms/_strength.scss | 111 + .../stylesheets/bootstrap/forms/_switch.scss | 123 + .../bootstrap/forms/_validation.scss | 366 +- assets/stylesheets/bootstrap/forms/index.scss | 16 + .../bootstrap/helpers/_clearfix.scss | 3 - .../bootstrap/helpers/_color-bg.scss | 7 - .../bootstrap/helpers/_colored-links.scss | 30 - .../bootstrap/helpers/_focus-ring.scss | 9 +- .../bootstrap/helpers/_icon-link.scss | 43 +- .../bootstrap/helpers/_position.scss | 56 +- .../stylesheets/bootstrap/helpers/_ratio.scss | 26 - .../bootstrap/helpers/_stacks.scss | 44 +- .../bootstrap/helpers/_stretched-link.scss | 21 +- .../bootstrap/helpers/_text-truncation.scss | 10 +- .../bootstrap/helpers/_theme-colors.scss | 6 + .../bootstrap/helpers/_visually-hidden.scss | 12 +- assets/stylesheets/bootstrap/helpers/_vr.scss | 15 +- .../stylesheets/bootstrap/helpers/index.scss | 9 + .../bootstrap/layout/_breakpoints.scss | 324 + .../bootstrap/layout/_containers.scss | 55 + .../stylesheets/bootstrap/layout/_grid.scss | 68 + .../stylesheets/bootstrap/layout/index.scss | 3 + .../stylesheets/bootstrap/mixins/_alert.scss | 18 - .../bootstrap/mixins/_backdrop.scss | 16 +- .../stylesheets/bootstrap/mixins/_banner.scss | 4 +- .../bootstrap/mixins/_border-radius.scss | 53 +- .../bootstrap/mixins/_box-shadow.scss | 7 +- .../bootstrap/mixins/_breakpoints.scss | 127 - .../bootstrap/mixins/_buttons.scss | 70 - .../stylesheets/bootstrap/mixins/_caret.scss | 44 +- .../bootstrap/mixins/_clearfix.scss | 9 - .../bootstrap/mixins/_color-mode.scss | 2 + .../bootstrap/mixins/_container.scss | 11 - .../bootstrap/mixins/_deprecate.scss | 2 + .../bootstrap/mixins/_dialog-shared.scss | 49 + .../bootstrap/mixins/_focus-ring.scss | 10 + .../bootstrap/mixins/_form-validation.scss | 33 + .../stylesheets/bootstrap/mixins/_forms.scss | 163 - .../bootstrap/mixins/_gradients.scss | 15 +- .../stylesheets/bootstrap/mixins/_grid.scss | 77 +- .../bootstrap/mixins/_list-group.scss | 26 - .../stylesheets/bootstrap/mixins/_lists.scss | 4 +- .../bootstrap/mixins/_mask-icon.scss | 21 + .../bootstrap/mixins/_pagination.scss | 10 - .../bootstrap/mixins/_reset-text.scss | 7 +- .../bootstrap/mixins/_table-variants.scss | 24 - .../stylesheets/bootstrap/mixins/_tokens.scss | 9 + .../bootstrap/mixins/_transition.scss | 11 +- .../bootstrap/mixins/_utilities.scss | 329 +- .../bootstrap/mixins/_visually-hidden.scss | 2 +- .../stylesheets/bootstrap/mixins/index.scss | 32 + .../stylesheets/bootstrap/utilities/_api.scss | 50 +- assets/stylesheets/bootstrap/vendor/_rfs.scss | 348 - bootstrap.gemspec | 8 +- lib/bootstrap.rb | 1 - lib/bootstrap/version.rb | 4 +- tasks/updater.rb | 22 +- tasks/updater/js.rb | 83 +- tasks/updater/network.rb | 9 +- tasks/updater/scss.rb | 18 +- .../app/assets/javascripts/application.js | 13 +- .../app/assets/stylesheets/application.sass | 8 +- test/javascript_test.rb | 147 + 198 files changed, 34784 insertions(+), 17269 deletions(-) delete mode 100644 assets/javascripts/bootstrap-global-this-define.js delete mode 100644 assets/javascripts/bootstrap-global-this-undefine.js delete mode 100644 assets/javascripts/bootstrap-sprockets.js create mode 100644 assets/javascripts/bootstrap.bundle.js create mode 100644 assets/javascripts/bootstrap.bundle.min.js create mode 100644 assets/javascripts/bootstrap/chips.js create mode 100644 assets/javascripts/bootstrap/combobox.js create mode 100644 assets/javascripts/bootstrap/datepicker.js create mode 100644 assets/javascripts/bootstrap/dialog-base.js create mode 100644 assets/javascripts/bootstrap/dialog.js create mode 100644 assets/javascripts/bootstrap/drawer.js delete mode 100644 assets/javascripts/bootstrap/dropdown.js create mode 100644 assets/javascripts/bootstrap/menu.js delete mode 100644 assets/javascripts/bootstrap/modal.js create mode 100644 assets/javascripts/bootstrap/nav-overflow.js delete mode 100644 assets/javascripts/bootstrap/offcanvas.js create mode 100644 assets/javascripts/bootstrap/otp-input.js create mode 100644 assets/javascripts/bootstrap/range.js create mode 100644 assets/javascripts/bootstrap/strength.js create mode 100644 assets/javascripts/bootstrap/toggler.js delete mode 100644 assets/javascripts/bootstrap/util/backdrop.js create mode 100644 assets/javascripts/bootstrap/util/floating-ui.js delete mode 100644 assets/javascripts/bootstrap/util/focustrap.js delete mode 100644 assets/javascripts/bootstrap/util/scrollbar.js create mode 100644 assets/javascripts/floating-ui.js delete mode 100644 assets/stylesheets/_bootstrap-grid.scss delete mode 100644 assets/stylesheets/_bootstrap-reboot.scss delete mode 100644 assets/stylesheets/_bootstrap-utilities.scss create mode 100644 assets/stylesheets/bootstrap/_avatar.scss create mode 100644 assets/stylesheets/bootstrap/_banner.scss delete mode 100644 assets/stylesheets/bootstrap/_button-group.scss delete mode 100644 assets/stylesheets/bootstrap/_buttons.scss create mode 100644 assets/stylesheets/bootstrap/_chip.scss delete mode 100644 assets/stylesheets/bootstrap/_close.scss create mode 100644 assets/stylesheets/bootstrap/_colors.scss create mode 100644 assets/stylesheets/bootstrap/_config.scss delete mode 100644 assets/stylesheets/bootstrap/_containers.scss create mode 100644 assets/stylesheets/bootstrap/_datepicker.scss create mode 100644 assets/stylesheets/bootstrap/_dialog.scss create mode 100644 assets/stylesheets/bootstrap/_drawer.scss delete mode 100644 assets/stylesheets/bootstrap/_dropdown.scss delete mode 100644 assets/stylesheets/bootstrap/_forms.scss delete mode 100644 assets/stylesheets/bootstrap/_grid.scss delete mode 100644 assets/stylesheets/bootstrap/_helpers.scss delete mode 100644 assets/stylesheets/bootstrap/_images.scss delete mode 100644 assets/stylesheets/bootstrap/_maps.scss create mode 100644 assets/stylesheets/bootstrap/_menu.scss delete mode 100644 assets/stylesheets/bootstrap/_mixins.scss delete mode 100644 assets/stylesheets/bootstrap/_modal.scss create mode 100644 assets/stylesheets/bootstrap/_nav-overflow.scss delete mode 100644 assets/stylesheets/bootstrap/_offcanvas.scss create mode 100644 assets/stylesheets/bootstrap/_placeholder.scss delete mode 100644 assets/stylesheets/bootstrap/_placeholders.scss delete mode 100644 assets/stylesheets/bootstrap/_reboot.scss create mode 100644 assets/stylesheets/bootstrap/_spinner.scss delete mode 100644 assets/stylesheets/bootstrap/_spinners.scss create mode 100644 assets/stylesheets/bootstrap/_stepper.scss delete mode 100644 assets/stylesheets/bootstrap/_tables.scss create mode 100644 assets/stylesheets/bootstrap/_theme.scss delete mode 100644 assets/stylesheets/bootstrap/_type.scss delete mode 100644 assets/stylesheets/bootstrap/_variables-dark.scss delete mode 100644 assets/stylesheets/bootstrap/_variables.scss create mode 100644 assets/stylesheets/bootstrap/bootstrap-grid.scss create mode 100644 assets/stylesheets/bootstrap/bootstrap-reboot.scss create mode 100644 assets/stylesheets/bootstrap/bootstrap-utilities.scss create mode 100644 assets/stylesheets/bootstrap/buttons/_button-group.scss create mode 100644 assets/stylesheets/bootstrap/buttons/_button.scss create mode 100644 assets/stylesheets/bootstrap/buttons/_close.scss create mode 100644 assets/stylesheets/bootstrap/buttons/index.scss create mode 100644 assets/stylesheets/bootstrap/content/_images.scss create mode 100644 assets/stylesheets/bootstrap/content/_prose.scss create mode 100644 assets/stylesheets/bootstrap/content/_reboot.scss create mode 100644 assets/stylesheets/bootstrap/content/_tables.scss create mode 100644 assets/stylesheets/bootstrap/content/_type.scss create mode 100644 assets/stylesheets/bootstrap/content/index.scss create mode 100644 assets/stylesheets/bootstrap/forms/_check.scss create mode 100644 assets/stylesheets/bootstrap/forms/_chip-input.scss create mode 100644 assets/stylesheets/bootstrap/forms/_combobox.scss create mode 100644 assets/stylesheets/bootstrap/forms/_form-adorn.scss delete mode 100644 assets/stylesheets/bootstrap/forms/_form-check.scss create mode 100644 assets/stylesheets/bootstrap/forms/_form-field.scss delete mode 100644 assets/stylesheets/bootstrap/forms/_form-select.scss create mode 100644 assets/stylesheets/bootstrap/forms/_otp-input.scss create mode 100644 assets/stylesheets/bootstrap/forms/_radio.scss create mode 100644 assets/stylesheets/bootstrap/forms/_strength.scss create mode 100644 assets/stylesheets/bootstrap/forms/_switch.scss create mode 100644 assets/stylesheets/bootstrap/forms/index.scss delete mode 100644 assets/stylesheets/bootstrap/helpers/_clearfix.scss delete mode 100644 assets/stylesheets/bootstrap/helpers/_color-bg.scss delete mode 100644 assets/stylesheets/bootstrap/helpers/_colored-links.scss delete mode 100644 assets/stylesheets/bootstrap/helpers/_ratio.scss create mode 100644 assets/stylesheets/bootstrap/helpers/_theme-colors.scss create mode 100644 assets/stylesheets/bootstrap/helpers/index.scss create mode 100644 assets/stylesheets/bootstrap/layout/_breakpoints.scss create mode 100644 assets/stylesheets/bootstrap/layout/_containers.scss create mode 100644 assets/stylesheets/bootstrap/layout/_grid.scss create mode 100644 assets/stylesheets/bootstrap/layout/index.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_alert.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_breakpoints.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_buttons.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_clearfix.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_container.scss create mode 100644 assets/stylesheets/bootstrap/mixins/_dialog-shared.scss create mode 100644 assets/stylesheets/bootstrap/mixins/_focus-ring.scss create mode 100644 assets/stylesheets/bootstrap/mixins/_form-validation.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_forms.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_list-group.scss create mode 100644 assets/stylesheets/bootstrap/mixins/_mask-icon.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_pagination.scss delete mode 100644 assets/stylesheets/bootstrap/mixins/_table-variants.scss create mode 100644 assets/stylesheets/bootstrap/mixins/_tokens.scss create mode 100644 assets/stylesheets/bootstrap/mixins/index.scss delete mode 100644 assets/stylesheets/bootstrap/vendor/_rfs.scss create mode 100644 test/javascript_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d48127..f439d9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ The changelog only includes changes specific to the RubyGem. The Bootstrap framework changes can be found in [the Releases section of twbs/bootstrap](https://github.com/twbs/bootstrap/releases). Release announcement posts on [the official Bootstrap blog](http://blog.getbootstrap.com) contain summaries of the most noteworthy changes made in each release of Bootstrap. +# 6.0.0.alpha1 + +First pre-release tracking Bootstrap 6 (upstream [`v6-dev`](https://github.com/twbs/bootstrap/tree/v6-dev)). **This is an alpha; expect breaking changes.** + +* **Sass module system.** Bootstrap 6 replaced `@import` with `@use`/`@forward`. + Import Bootstrap with `@use "bootstrap"` and customize variables via + `@use "bootstrap" with (...)`. See the [v5→v6 migration guide](https://github.com/twbs/bootstrap/blob/v6-dev/skills/bootstrap-v5-v6-migration/SKILL.md). +* **Dart Sass is required to compile the stylesheets.** LibSass/SassC + (`sassc-rails`) cannot compile the module system. `sassc-rails` remains a + supported Sass engine option for the gem, but Bootstrap 6's own stylesheets + will only compile under `dartsass-sprockets`, `dartsass-rails`, or + `cssbundling-rails`. +* The standalone `bootstrap-grid`, `bootstrap-reboot`, and `bootstrap-utilities` + Sass entry points were removed upstream; only `bootstrap` remains. +* **JavaScript is now ES-module only.** Bootstrap 6 removed the UMD bundle and + the `window.bootstrap` global, so the `bootstrap-sprockets` Sprockets manifest + and the `globalThis` shim are gone. Load Bootstrap via importmaps (see the + README). +* **Popper replaced by [Floating UI](https://floating-ui.com/).** The `popper_js` + runtime dependency was removed. The self-contained `bootstrap.bundle.{js,min.js}` + builds inline both `@floating-ui/dom` and `vanilla-calendar-pro`, so a single + importmap pin works with no extra dependencies (the recommended path). For + lighter-weight pinning of the non-bundled `bootstrap.{js,min.js}` or individual + component modules, a self-contained ESM build of `@floating-ui/dom` is also + vendored as `floating-ui.js`. + # 5.3.4 * Autoprefixer is now optional. diff --git a/Gemfile b/Gemfile index 9f4cb9a3..a7fb19d7 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source 'https://rubygems.org' gemspec group :development do - gem 'popper_js', '>= 1.12.3' gem 'dartsass-sprockets' end diff --git a/README.md b/README.md index 493a5c08..e3c552ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # Bootstrap Ruby Gem [![CI](https://github.com/twbs/bootstrap-rubygem/actions/workflows/ci.yml/badge.svg)](https://github.com/twbs/bootstrap-rubygem/actions/workflows/ci.yml) [![Gem](https://img.shields.io/gem/v/bootstrap.svg)](https://rubygems.org/gems/bootstrap) -[Bootstrap 5][bootstrap-home] ruby gem for Ruby on Rails (*Sprockets*/*Importmaps*) and Hanami (formerly Lotus). +[Bootstrap 6][bootstrap-home] ruby gem for Ruby on Rails (*Sprockets*/*Importmaps*) and Hanami (formerly Lotus). For Sass versions of Bootstrap 3 and 2 see [bootstrap-sass](https://github.com/twbs/bootstrap-sass) instead. +> **Bootstrap 6 (pre-release):** This is an alpha tracking the upstream +> [`v6-dev`](https://github.com/twbs/bootstrap/tree/v6-dev) branch. +> Bootstrap 6 moved its Sass to the [module system](https://sass-lang.com/documentation/at-rules/use) +> (`@use`/`@forward`), so its stylesheets require a **Dart Sass** engine to +> compile — LibSass/SassC (`sassc-rails`) cannot compile them. Its JavaScript is +> ES-module only and is loaded via importmaps. See the [CHANGELOG](CHANGELOG.md). +> For the previous stable release, use `gem 'bootstrap', '~> 5.3.8'`. + **Ruby on Rails Note**: Newer releases of Rails have added additional ways for assets to be processed. The `twbs/bootstrap-rubygem` is for use with Importmaps or Sprockets, but not Webpack. @@ -21,14 +29,16 @@ Please see the appropriate guide for your environment of choice: Add `bootstrap` to your Gemfile: ```ruby -gem 'bootstrap', '~> 5.3.8' +gem 'bootstrap', '~> 6.0.0.alpha1' ``` -This gem requires a Sass engine, so make sure you have **one** of these gems in your Gemfile: +This gem requires a Sass engine, so make sure you have **one** of these gems in your Gemfile. +Bootstrap 6 stylesheets use the Sass module system, so a **Dart Sass** engine is +required to compile them — `sassc-rails` (LibSass) can no longer compile Bootstrap: - [`dartsass-sprockets`](https://github.com/tablecheck/dartsass-sprockets): Dart Sass engine, recommended but only works for Ruby 2.6+ and Rails 5+ - [`dartsass-rails`](https://github.com/rails/dartsass-rails): Dart Sass engine, recommended for Rails projects that use Propshaft - [`cssbundling-rails`](https://github.com/rails/cssbundling-rails): External Sass engine -- [`sassc-rails`](https://github.com/sass/sassc-rails): SassC engine, deprecated but compatible with Ruby 2.3+ and Rails 4 +- [`sassc-rails`](https://github.com/sass/sassc-rails): SassC engine, deprecated and compatible with Ruby 2.3+ and Rails 4, but **cannot compile Bootstrap 6** stylesheets Also ensure that `sprockets-rails` is at least v2.3.2. @@ -37,14 +47,24 @@ If you are using Rails, add the `autoprefixer-rails` gem to your app and ensure `bundle install` and restart your server to make the files available through the pipeline. -Import Bootstrap styles in `app/assets/stylesheets/application.scss`: +Import Bootstrap styles in `app/assets/stylesheets/application.scss` with `@use` +(Bootstrap 6 no longer supports `@import`): + +```scss +@use "bootstrap"; +``` + +To customize Bootstrap's variables, configure them through the `@use ... with` +rule instead of setting globals before an `@import`: ```scss -// Custom bootstrap variables must be set or imported *before* bootstrap. -@import "bootstrap"; +@use "bootstrap" with ( + $primary: #c0ffee, + $enable-rounded: false +); ``` -The available variables can be found [here](assets/stylesheets/bootstrap/_variables.scss). +The available variables can be found in [`bootstrap/_config.scss`](assets/stylesheets/bootstrap/_config.scss). Make sure the file has `.scss` extension (or `.sass` for Sass syntax). If you have just generated a new Rails app, it may come with a `.css` file instead. If this file exists, it will be served instead of Sass, so rename it: @@ -53,57 +73,68 @@ it may come with a `.css` file instead. If this file exists, it will be served i $ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss ``` -Then, remove all the `*= require` and `*= require_tree` statements from the Sass file. Instead, use `@import` to import Sass files. +Then, remove all the `*= require` and `*= require_tree` statements from the Sass file. Instead, use `@use` to import Sass files. Do not use `*= require` in Sass or your other stylesheets will not be able to access the Bootstrap mixins and variables. -Bootstrap JavaScript can optionally use jQuery. -If you're using Rails 5.1+, you can add the `jquery-rails` gem to your Gemfile: +### JavaScript -```ruby -gem 'jquery-rails' -``` +Bootstrap 6's JavaScript is **ES-module only** — there is no UMD bundle and no +`window.bootstrap` global, so the old Sprockets `//= require bootstrap-sprockets` +concatenation is gone. Load it through **importmaps** instead. -Bootstrap tooltips and popovers depend on [popper.js] for positioning. -The `bootstrap` gem already depends on the -[popper_js](https://github.com/glebm/popper_js-rubygem) gem. +Bootstrap 6 uses [Floating UI](https://floating-ui.com/) (`@floating-ui/dom`) +for positioning tooltips, popovers, and menus, replacing Popper. A self-contained +ESM build of `@floating-ui/dom` is **vendored in this gem** as `floating-ui.js`, +so you do not need an external dependency for it. #### Importmaps -You can pin either `bootstrap.js` or `bootstrap.min.js` in `config/importmap.rb` -as well as `popper.js`: +The simplest option is the self-contained **bundle** +(`bootstrap.bundle.min.js`), which inlines Floating UI and `vanilla-calendar-pro`, +so it needs no other pins. In `config/importmap.rb`: ```ruby -pin "bootstrap", to: "bootstrap.min.js", preload: true -pin "@popperjs/core", to: "popper.js", preload: true +pin "bootstrap", to: "bootstrap.bundle.min.js", preload: true ``` -Whichever files you pin will need to be added to `config.assets.precompile`: +Then import the components you need from your application's entrypoint: -```ruby -# config/initializers/assets.rb -Rails.application.config.assets.precompile += %w(bootstrap.min.js popper.js) +```js +// app/javascript/application.js +import { Tooltip } from "bootstrap" + +for (const el of document.querySelectorAll('[data-bs-toggle="tooltip"]')) { + new Tooltip(el) +} ``` -#### Sprockets +The data-attribute APIs (`data-bs-toggle`, etc.) work automatically once the +module is loaded. -Add Bootstrap dependencies and Bootstrap to your `application.js`: +
+Lighter-weight pinning (without the bundle) -```js -//= require jquery3 -//= require popper -//= require bootstrap-sprockets +If you want to avoid the bundled Floating UI / Datepicker code, pin the +non-bundled `bootstrap.min.js` together with the gem's **vendored** +`@floating-ui/dom` build: + +```ruby +pin "bootstrap", to: "bootstrap.min.js", preload: true +pin "@floating-ui/dom", to: "floating-ui.js", preload: true ``` -While `bootstrap-sprockets` provides individual Bootstrap components -for ease of debugging, you may alternatively require -the concatenated `bootstrap` for faster compilation: +Individual components are also available as separate modules +(e.g. `pin "bootstrap/tooltip", to: "bootstrap/tooltip.js"`) for finer-grained +pinning. The [Datepicker](https://getbootstrap.com/) component additionally +depends on [`vanilla-calendar-pro`](https://www.npmjs.com/package/vanilla-calendar-pro), +which is **not** vendored — if you use it (or the non-bundled `bootstrap.min.js`, +which imports it), pin it from a CDN: -```js -//= require jquery3 -//= require popper -//= require bootstrap +```ruby +pin "vanilla-calendar-pro", to: "https://ga.jspm.io/npm:vanilla-calendar-pro@3.1.0/index.js" ``` +
### b. Other Ruby frameworks @@ -123,13 +154,11 @@ By default all of Bootstrap is imported. You can also import components explicitly. To start with a full list of modules copy [`_bootstrap.scss`](assets/stylesheets/_bootstrap.scss) file into your assets as `_bootstrap-custom.scss`. Then comment out components you do not want from `_bootstrap-custom`. -In the application Sass file, replace `@import 'bootstrap'` with: +In the application Sass file, replace `@use 'bootstrap'` with: ```scss -@import 'bootstrap-custom'; +@use 'bootstrap-custom'; ``` [bootstrap-home]: https://getbootstrap.com -[bootstrap-variables.scss]: https://github.com/twbs/bootstrap-rubygem/blob/master/templates/project/_bootstrap-variables.scss [autoprefixer]: https://github.com/ai/autoprefixer -[popper.js]: https://popper.js.org diff --git a/Rakefile b/Rakefile index 52402cce..116fc6dc 100644 --- a/Rakefile +++ b/Rakefile @@ -58,7 +58,8 @@ task :debug do require './lib/bootstrap' require 'term/ansicolor' path = Bootstrap.stylesheets_path - %w(_bootstrap _bootstrap-reboot _bootstrap-grid).each do |file| + # Bootstrap 6 exposes a single `bootstrap` entry point. + %w(_bootstrap).each do |file| filename = "#{path}/#{file}.scss" css = if defined?(SassC::Engine) SassC::Engine.new(File.read(filename), filename: filename, syntax: :scss).render @@ -77,6 +78,12 @@ task :update, :branch do |t, args| Updater.new(branch: args[:branch]).update_bootstrap end +desc 'Update only bootstrap stylesheets from upstream (leaves JS untouched)' +task :update_scss, :branch do |t, args| + require './tasks/updater' + Updater.new(branch: args[:branch], skip_js: true).update_bootstrap +end + desc 'Start a dummy Rails app server' task :rails_server do require 'rack' diff --git a/assets/javascripts/bootstrap-global-this-define.js b/assets/javascripts/bootstrap-global-this-define.js deleted file mode 100644 index f3a3cc13..00000000 --- a/assets/javascripts/bootstrap-global-this-define.js +++ /dev/null @@ -1,6 +0,0 @@ -// Set a `globalThis` so that bootstrap components are defined on window.bootstrap instead of window. -window['bootstrap'] = { - "@popperjs/core": window.Popper, - _originalGlobalThis: window['globalThis'] -}; -window['globalThis'] = window['bootstrap']; diff --git a/assets/javascripts/bootstrap-global-this-undefine.js b/assets/javascripts/bootstrap-global-this-undefine.js deleted file mode 100644 index 6f96eb1d..00000000 --- a/assets/javascripts/bootstrap-global-this-undefine.js +++ /dev/null @@ -1,2 +0,0 @@ -window['globalThis'] = window['bootstrap']._originalGlobalThis; -window['bootstrap']._originalGlobalThis = null; diff --git a/assets/javascripts/bootstrap-sprockets.js b/assets/javascripts/bootstrap-sprockets.js deleted file mode 100644 index 8235e211..00000000 --- a/assets/javascripts/bootstrap-sprockets.js +++ /dev/null @@ -1,28 +0,0 @@ -//= require ./bootstrap-global-this-define -//= require ./bootstrap/dom/data -//= require ./bootstrap/util/index -//= require ./bootstrap/dom/event-handler -//= require ./bootstrap/dom/manipulator -//= require ./bootstrap/util/config -//= require ./bootstrap/base-component -//= require ./bootstrap/util/sanitizer -//= require ./bootstrap/dom/selector-engine -//= require ./bootstrap/util/template-factory -//= require ./bootstrap/tooltip -//= require ./bootstrap/popover -//= require ./bootstrap/util/component-functions -//= require ./bootstrap/alert -//= require ./bootstrap/toast -//= require ./bootstrap/button -//= require ./bootstrap/collapse -//= require ./bootstrap/util/scrollbar -//= require ./bootstrap/tab -//= require ./bootstrap/util/focustrap -//= require ./bootstrap/util/backdrop -//= require ./bootstrap/scrollspy -//= require ./bootstrap/modal -//= require ./bootstrap/offcanvas -//= require ./bootstrap/util/swipe -//= require ./bootstrap/carousel -//= require ./bootstrap/dropdown -//= require ./bootstrap-global-this-undefine diff --git a/assets/javascripts/bootstrap.bundle.js b/assets/javascripts/bootstrap.bundle.js new file mode 100644 index 00000000..3dd59449 --- /dev/null +++ b/assets/javascripts/bootstrap.bundle.js @@ -0,0 +1,9237 @@ +/*! + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const elementMap = new Map(); +const Data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; + } + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; + } + return null; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; + } + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; + } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); + + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); + } + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); + +/** + * Private methods + */ + +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + return fn.apply(element, [event]); + }; +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); + } + } + }; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); + } + }; + }; + callable = wrapFunction(callable); + } + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; + } + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; + } + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; + const events = getElementEvents(element); + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); + return; + } + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } + } + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; + } +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; + } + }); + } + } + return obj; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +function normalizeData(value) { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; + } + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement$1 = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement$1(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement$1(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { + return false; + } + if (summary === null) { + return false; + } + } + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; + } + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { + return null; + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const isRTL$1 = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { + return; + } + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); + }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Class definition + */ + +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement$1(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement$1(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement$1(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); + } + } + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const VERSION = '6.0.0-alpha1'; + +/** + * Class definition + */ + +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; + } + this._element = element; + this._config = this._getConfig(config); + + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); + } + Data.set(this._element, this.constructor.DATA_KEY, this); + } + + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; + } + } + + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; + } + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; + } + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; + } + next = next.nextElementSibling; + } + return []; + }, + focusableChildren(element) { + const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map(selector => `${selector}:not([tabindex^="-"])`).join(','); + return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); + } + for (const instance of instances) { + instance[method](); + } + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event + }); + }); +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$l = 'alert'; +const DATA_KEY$h = 'bs.alert'; +const EVENT_KEY$i = `.${DATA_KEY$h}`; +const EVENT_CLOSE = `close${EVENT_KEY$i}`; +const EVENT_CLOSED = `closed${EVENT_KEY$i}`; +const CLASS_NAME_FADE$4 = 'fade'; +const CLASS_NAME_SHOW$6 = 'show'; + +/** + * Class definition + */ + +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME$l; + } + + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; + } + this._element.classList.remove(CLASS_NAME_SHOW$6); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$4); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + } + + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Alert, 'close'); + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$k = 'button'; +const DATA_KEY$g = 'bs.button'; +const EVENT_KEY$h = `.${DATA_KEY$g}`; +const DATA_API_KEY$c = '.data-api'; +const CLASS_NAME_ACTIVE$4 = 'active'; +const SELECTOR_DATA_TOGGLE$a = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API$8 = `click${EVENT_KEY$h}${DATA_API_KEY$c}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME$k; + } + + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$4)); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$8, SELECTOR_DATA_TOGGLE$a, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE$a); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$j = 'carousel'; +const DATA_KEY$f = 'bs.carousel'; +const EVENT_KEY$g = `.${DATA_KEY$f}`; +const DATA_API_KEY$b = '.data-api'; +const ARROW_LEFT_KEY$2 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$2 = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY$g}`; +const EVENT_SLID = `slid${EVENT_KEY$g}`; +const EVENT_KEYDOWN$2 = `keydown${EVENT_KEY$g}`; +const EVENT_MOUSEENTER$2 = `mouseenter${EVENT_KEY$g}`; +const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$g}`; +const EVENT_POINTERDOWN$1 = `pointerdown${EVENT_KEY$g}`; +const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$g}${DATA_API_KEY$b}`; +const EVENT_CLICK_DATA_API$7 = `click${EVENT_KEY$g}${DATA_API_KEY$b}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE$3 = 'active'; +const CLASS_NAME_FADE$3 = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER$1 = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY$2]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY$2]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default$i = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType$i = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER$1, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); + } + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default$i; + } + static get DefaultType() { + return DefaultType$i; + } + static get NAME() { + return NAME$j; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); + } + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); + + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; + } + if (rawIndex < 0) { + this._loopTransition(false); + return; + } + } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + if (this._isFade()) { + this._fadeTo(targetIndex); + return; + } + + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); + } + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); + } + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + } + + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); + } + this._viewport.style.scrollSnapType = ''; + + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY$g); + super.dispose(); + } + + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default$i.ends; + } + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN$2, event => this._keydown(event)); + } + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER$2, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); + } + + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN$1, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { + this.next(); + } + } + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; + } + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); + } + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; + } + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); + } + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; + } + } + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; + } + + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); + } + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } + + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; + } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; + + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; + } + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; + } + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; + } + + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); + } + + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL$1() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE$3); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); + } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); + } + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL$1()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } + + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; + } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE$3, index === this._activeIndex); + } + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; + } + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true + }); + } + control.disabled = disabled; + } + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; + } + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE$3); + active.removeAttribute('aria-current'); + } + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE$3); + newActive.setAttribute('aria-current', 'true'); + } + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; + } + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; + } + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; + } + return index; + } + + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } + + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; + } + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL$1()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); + return; + } + this._scheduleAutoplay(upcoming); + }, interval); + } + + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { + return; + } + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); + return; + } + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { + return; + } + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); + } + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE$3); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); + + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$i = 'collapse'; +const DATA_KEY$e = 'bs.collapse'; +const EVENT_KEY$f = `.${DATA_KEY$e}`; +const DATA_API_KEY$a = '.data-api'; +const EVENT_SHOW$7 = `show${EVENT_KEY$f}`; +const EVENT_SHOWN$6 = `shown${EVENT_KEY$f}`; +const EVENT_HIDE$6 = `hide${EVENT_KEY$f}`; +const EVENT_HIDDEN$8 = `hidden${EVENT_KEY$f}`; +const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$f}${DATA_API_KEY$a}`; +const CLASS_NAME_SHOW$5 = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE$9 = '[data-bs-toggle="collapse"]'; +const Default$h = { + parent: null, + toggle: true +}; +const DefaultType$h = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$9); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); + } + } + + // Getters + static get Default() { + return Default$h; + } + static get DefaultType() { + return DefaultType$h; + } + static get NAME() { + return NAME$i; + } + + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); + } + } + show() { + if (this._isTransitioning || this._isShown()) { + return; + } + let activeChildren = []; + + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); + } + if (activeChildren.length && activeChildren[0]._isTransitioning) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$7); + if (startEvent.defaultPrevented) { + return; + } + for (const activeInstance of activeChildren) { + activeInstance.hide(); + } + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN$6); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); + } + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN$8); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } + + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW$5); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); + } + } + } + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; + } + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$9, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); + +/** + * Custom positioning reference element. + * @see https://floating-ui.com/docs/virtual-elements + */ + +const min = Math.min; +const max = Math.max; +const round = Math.round; +const floor = Math.floor; +const createCoords = v => ({ + x: v, + y: v +}); +const oppositeSideMap = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' +}; +function clamp(start, value, end) { + return max(start, min(value, end)); +} +function evaluate(value, param) { + return typeof value === 'function' ? value(param) : value; +} +function getSide(placement) { + return placement.split('-')[0]; +} +function getAlignment(placement) { + return placement.split('-')[1]; +} +function getOppositeAxis(axis) { + return axis === 'x' ? 'y' : 'x'; +} +function getAxisLength(axis) { + return axis === 'y' ? 'height' : 'width'; +} +function getSideAxis(placement) { + const firstChar = placement[0]; + return firstChar === 't' || firstChar === 'b' ? 'y' : 'x'; +} +function getAlignmentAxis(placement) { + return getOppositeAxis(getSideAxis(placement)); +} +function getAlignmentSides(placement, rects, rtl) { + if (rtl === void 0) { + rtl = false; + } + const alignment = getAlignment(placement); + const alignmentAxis = getAlignmentAxis(placement); + const length = getAxisLength(alignmentAxis); + let mainAlignmentSide = alignmentAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top'; + if (rects.reference[length] > rects.floating[length]) { + mainAlignmentSide = getOppositePlacement(mainAlignmentSide); + } + return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)]; +} +function getExpandedPlacements(placement) { + const oppositePlacement = getOppositePlacement(placement); + return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; +} +function getOppositeAlignmentPlacement(placement) { + return placement.includes('start') ? placement.replace('start', 'end') : placement.replace('end', 'start'); +} +const lrPlacement = ['left', 'right']; +const rlPlacement = ['right', 'left']; +const tbPlacement = ['top', 'bottom']; +const btPlacement = ['bottom', 'top']; +function getSideList(side, isStart, rtl) { + switch (side) { + case 'top': + case 'bottom': + if (rtl) return isStart ? rlPlacement : lrPlacement; + return isStart ? lrPlacement : rlPlacement; + case 'left': + case 'right': + return isStart ? tbPlacement : btPlacement; + default: + return []; + } +} +function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { + const alignment = getAlignment(placement); + let list = getSideList(getSide(placement), direction === 'start', rtl); + if (alignment) { + list = list.map(side => side + "-" + alignment); + if (flipAlignment) { + list = list.concat(list.map(getOppositeAlignmentPlacement)); + } + } + return list; +} +function getOppositePlacement(placement) { + const side = getSide(placement); + return oppositeSideMap[side] + placement.slice(side.length); +} +function expandPaddingObject(padding) { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + ...padding + }; +} +function getPaddingObject(padding) { + return typeof padding !== 'number' ? expandPaddingObject(padding) : { + top: padding, + right: padding, + bottom: padding, + left: padding + }; +} +function rectToClientRect(rect) { + const { + x, + y, + width, + height + } = rect; + return { + width, + height, + top: y, + left: x, + right: x + width, + bottom: y + height, + x, + y + }; +} + +function computeCoordsFromPlacement(_ref, placement, rtl) { + let { + reference, + floating + } = _ref; + const sideAxis = getSideAxis(placement); + const alignmentAxis = getAlignmentAxis(placement); + const alignLength = getAxisLength(alignmentAxis); + const side = getSide(placement); + const isVertical = sideAxis === 'y'; + const commonX = reference.x + reference.width / 2 - floating.width / 2; + const commonY = reference.y + reference.height / 2 - floating.height / 2; + const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; + let coords; + switch (side) { + case 'top': + coords = { + x: commonX, + y: reference.y - floating.height + }; + break; + case 'bottom': + coords = { + x: commonX, + y: reference.y + reference.height + }; + break; + case 'right': + coords = { + x: reference.x + reference.width, + y: commonY + }; + break; + case 'left': + coords = { + x: reference.x - floating.width, + y: commonY + }; + break; + default: + coords = { + x: reference.x, + y: reference.y + }; + } + switch (getAlignment(placement)) { + case 'start': + coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); + break; + case 'end': + coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); + break; + } + return coords; +} + +/** + * Resolves with an object of overflow side offsets that determine how much the + * element is overflowing a given clipping boundary on each side. + * - positive = overflowing the boundary by that number of pixels + * - negative = how many pixels left before it will overflow + * - 0 = lies flush with the boundary + * @see https://floating-ui.com/docs/detectOverflow + */ +async function detectOverflow(state, options) { + var _await$platform$isEle; + if (options === void 0) { + options = {}; + } + const { + x, + y, + platform, + rects, + elements, + strategy + } = state; + const { + boundary = 'clippingAncestors', + rootBoundary = 'viewport', + elementContext = 'floating', + altBoundary = false, + padding = 0 + } = evaluate(options, state); + const paddingObject = getPaddingObject(padding); + const altContext = elementContext === 'floating' ? 'reference' : 'floating'; + const element = elements[altBoundary ? altContext : elementContext]; + const clippingClientRect = rectToClientRect(await platform.getClippingRect({ + element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))), + boundary, + rootBoundary, + strategy + })); + const rect = elementContext === 'floating' ? { + x, + y, + width: rects.floating.width, + height: rects.floating.height + } : rects.reference; + const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating)); + const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || { + x: 1, + y: 1 + } : { + x: 1, + y: 1 + }; + const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ + elements, + rect, + offsetParent, + strategy + }) : rect); + return { + top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, + bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, + left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, + right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x + }; +} + +// Maximum number of resets that can occur before bailing to avoid infinite reset loops. +const MAX_RESET_COUNT = 50; + +/** + * Computes the `x` and `y` coordinates that will place the floating element + * next to a given reference element. + * + * This export does not have any `platform` interface logic. You will need to + * write one for the platform you are using Floating UI with. + */ +const computePosition$1 = async (reference, floating, config) => { + const { + placement = 'bottom', + strategy = 'absolute', + middleware = [], + platform + } = config; + const platformWithDetectOverflow = platform.detectOverflow ? platform : { + ...platform, + detectOverflow + }; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating)); + let rects = await platform.getElementRects({ + reference, + floating, + strategy + }); + let { + x, + y + } = computeCoordsFromPlacement(rects, placement, rtl); + let statefulPlacement = placement; + let resetCount = 0; + const middlewareData = {}; + for (let i = 0; i < middleware.length; i++) { + const currentMiddleware = middleware[i]; + if (!currentMiddleware) { + continue; + } + const { + name, + fn + } = currentMiddleware; + const { + x: nextX, + y: nextY, + data, + reset + } = await fn({ + x, + y, + initialPlacement: placement, + placement: statefulPlacement, + strategy, + middlewareData, + rects, + platform: platformWithDetectOverflow, + elements: { + reference, + floating + } + }); + x = nextX != null ? nextX : x; + y = nextY != null ? nextY : y; + middlewareData[name] = { + ...middlewareData[name], + ...data + }; + if (reset && resetCount < MAX_RESET_COUNT) { + resetCount++; + if (typeof reset === 'object') { + if (reset.placement) { + statefulPlacement = reset.placement; + } + if (reset.rects) { + rects = reset.rects === true ? await platform.getElementRects({ + reference, + floating, + strategy + }) : reset.rects; + } + ({ + x, + y + } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); + } + i = -1; + } + } + return { + x, + y, + placement: statefulPlacement, + strategy, + middlewareData + }; +}; + +/** + * Provides data to position an inner element of the floating element so that it + * appears centered to the reference element. + * @see https://floating-ui.com/docs/arrow + */ +const arrow$1 = options => ({ + name: 'arrow', + options, + async fn(state) { + const { + x, + y, + placement, + rects, + platform, + elements, + middlewareData + } = state; + // Since `element` is required, we don't Partial<> the type. + const { + element, + padding = 0 + } = evaluate(options, state) || {}; + if (element == null) { + return {}; + } + const paddingObject = getPaddingObject(padding); + const coords = { + x, + y + }; + const axis = getAlignmentAxis(placement); + const length = getAxisLength(axis); + const arrowDimensions = await platform.getDimensions(element); + const isYAxis = axis === 'y'; + const minProp = isYAxis ? 'top' : 'left'; + const maxProp = isYAxis ? 'bottom' : 'right'; + const clientProp = isYAxis ? 'clientHeight' : 'clientWidth'; + const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; + const startDiff = coords[axis] - rects.reference[axis]; + const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element)); + let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; + + // DOM platform can return `window` as the `offsetParent`. + if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) { + clientSize = elements.floating[clientProp] || rects.floating[length]; + } + const centerToReference = endDiff / 2 - startDiff / 2; + + // If the padding is large enough that it causes the arrow to no longer be + // centered, modify the padding so that it is centered. + const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; + const minPadding = min(paddingObject[minProp], largestPossiblePadding); + const maxPadding = min(paddingObject[maxProp], largestPossiblePadding); + + // Make sure the arrow doesn't overflow the floating element if the center + // point is outside the floating element's bounds. + const min$1 = minPadding; + const max = clientSize - arrowDimensions[length] - maxPadding; + const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; + const offset = clamp(min$1, center, max); + + // If the reference is small enough that the arrow's padding causes it to + // to point to nothing for an aligned placement, adjust the offset of the + // floating element itself. To ensure `shift()` continues to take action, + // a single reset is performed when this is true. + const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; + const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0; + return { + [axis]: coords[axis] + alignmentOffset, + data: { + [axis]: offset, + centerOffset: center - offset - alignmentOffset, + ...(shouldAddOffset && { + alignmentOffset + }) + }, + reset: shouldAddOffset + }; + } +}); + +/** + * Optimizes the visibility of the floating element by flipping the `placement` + * in order to keep it in view when the preferred placement(s) will overflow the + * clipping boundary. Alternative to `autoPlacement`. + * @see https://floating-ui.com/docs/flip + */ +const flip$1 = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'flip', + options, + async fn(state) { + var _middlewareData$arrow, _middlewareData$flip; + const { + placement, + middlewareData, + rects, + initialPlacement, + platform, + elements + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = true, + fallbackPlacements: specifiedFallbackPlacements, + fallbackStrategy = 'bestFit', + fallbackAxisSideDirection = 'none', + flipAlignment = true, + ...detectOverflowOptions + } = evaluate(options, state); + + // If a reset by the arrow was caused due to an alignment offset being + // added, we should skip any logic now since `flip()` has already done its + // work. + // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643 + if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + const side = getSide(placement); + const initialSideAxis = getSideAxis(initialPlacement); + const isBasePlacement = getSide(initialPlacement) === initialPlacement; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); + const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none'; + if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { + fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); + } + const placements = [initialPlacement, ...fallbackPlacements]; + const overflow = await platform.detectOverflow(state, detectOverflowOptions); + const overflows = []; + let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; + if (checkMainAxis) { + overflows.push(overflow[side]); + } + if (checkCrossAxis) { + const sides = getAlignmentSides(placement, rects, rtl); + overflows.push(overflow[sides[0]], overflow[sides[1]]); + } + overflowsData = [...overflowsData, { + placement, + overflows + }]; + + // One or more sides is overflowing. + if (!overflows.every(side => side <= 0)) { + var _middlewareData$flip2, _overflowsData$filter; + const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; + const nextPlacement = placements[nextIndex]; + if (nextPlacement) { + const ignoreCrossAxisOverflow = checkCrossAxis === 'alignment' ? initialSideAxis !== getSideAxis(nextPlacement) : false; + if (!ignoreCrossAxisOverflow || + // We leave the current main axis only if every placement on that axis + // overflows the main axis. + overflowsData.every(d => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { + // Try next placement and re-run the lifecycle. + return { + data: { + index: nextIndex, + overflows: overflowsData + }, + reset: { + placement: nextPlacement + } + }; + } + } + + // First, find the candidates that fit on the mainAxis side of overflow, + // then find the placement that fits the best on the main crossAxis side. + let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; + + // Otherwise fallback. + if (!resetPlacement) { + switch (fallbackStrategy) { + case 'bestFit': + { + var _overflowsData$filter2; + const placement = (_overflowsData$filter2 = overflowsData.filter(d => { + if (hasFallbackAxisSideDirection) { + const currentSideAxis = getSideAxis(d.placement); + return currentSideAxis === initialSideAxis || + // Create a bias to the `y` side axis due to horizontal + // reading directions favoring greater width. + currentSideAxis === 'y'; + } + return true; + }).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; + if (placement) { + resetPlacement = placement; + } + break; + } + case 'initialPlacement': + resetPlacement = initialPlacement; + break; + } + } + if (placement !== resetPlacement) { + return { + reset: { + placement: resetPlacement + } + }; + } + } + return {}; + } + }; +}; + +const originSides = /*#__PURE__*/new Set(['left', 'top']); + +// For type backwards-compatibility, the `OffsetOptions` type was also +// Derivable. + +async function convertValueToCoords(state, options) { + const { + placement, + platform, + elements + } = state; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const side = getSide(placement); + const alignment = getAlignment(placement); + const isVertical = getSideAxis(placement) === 'y'; + const mainAxisMulti = originSides.has(side) ? -1 : 1; + const crossAxisMulti = rtl && isVertical ? -1 : 1; + const rawValue = evaluate(options, state); + + // eslint-disable-next-line prefer-const + let { + mainAxis, + crossAxis, + alignmentAxis + } = typeof rawValue === 'number' ? { + mainAxis: rawValue, + crossAxis: 0, + alignmentAxis: null + } : { + mainAxis: rawValue.mainAxis || 0, + crossAxis: rawValue.crossAxis || 0, + alignmentAxis: rawValue.alignmentAxis + }; + if (alignment && typeof alignmentAxis === 'number') { + crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis; + } + return isVertical ? { + x: crossAxis * crossAxisMulti, + y: mainAxis * mainAxisMulti + } : { + x: mainAxis * mainAxisMulti, + y: crossAxis * crossAxisMulti + }; +} + +/** + * Modifies the placement by translating the floating element along the + * specified axes. + * A number (shorthand for `mainAxis` or distance), or an axes configuration + * object may be passed. + * @see https://floating-ui.com/docs/offset + */ +const offset$1 = function (options) { + if (options === void 0) { + options = 0; + } + return { + name: 'offset', + options, + async fn(state) { + var _middlewareData$offse, _middlewareData$arrow; + const { + x, + y, + placement, + middlewareData + } = state; + const diffCoords = await convertValueToCoords(state, options); + + // If the placement is the same and the arrow caused an alignment offset + // then we don't need to change the positioning coordinates. + if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + return { + x: x + diffCoords.x, + y: y + diffCoords.y, + data: { + ...diffCoords, + placement + } + }; + } + }; +}; + +/** + * Optimizes the visibility of the floating element by shifting it in order to + * keep it in view when it will overflow the clipping boundary. + * @see https://floating-ui.com/docs/shift + */ +const shift$1 = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'shift', + options, + async fn(state) { + const { + x, + y, + placement, + platform + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = false, + limiter = { + fn: _ref => { + let { + x, + y + } = _ref; + return { + x, + y + }; + } + }, + ...detectOverflowOptions + } = evaluate(options, state); + const coords = { + x, + y + }; + const overflow = await platform.detectOverflow(state, detectOverflowOptions); + const crossAxis = getSideAxis(getSide(placement)); + const mainAxis = getOppositeAxis(crossAxis); + let mainAxisCoord = coords[mainAxis]; + let crossAxisCoord = coords[crossAxis]; + if (checkMainAxis) { + const minSide = mainAxis === 'y' ? 'top' : 'left'; + const maxSide = mainAxis === 'y' ? 'bottom' : 'right'; + const min = mainAxisCoord + overflow[minSide]; + const max = mainAxisCoord - overflow[maxSide]; + mainAxisCoord = clamp(min, mainAxisCoord, max); + } + if (checkCrossAxis) { + const minSide = crossAxis === 'y' ? 'top' : 'left'; + const maxSide = crossAxis === 'y' ? 'bottom' : 'right'; + const min = crossAxisCoord + overflow[minSide]; + const max = crossAxisCoord - overflow[maxSide]; + crossAxisCoord = clamp(min, crossAxisCoord, max); + } + const limitedCoords = limiter.fn({ + ...state, + [mainAxis]: mainAxisCoord, + [crossAxis]: crossAxisCoord + }); + return { + ...limitedCoords, + data: { + x: limitedCoords.x - x, + y: limitedCoords.y - y, + enabled: { + [mainAxis]: checkMainAxis, + [crossAxis]: checkCrossAxis + } + } + }; + } + }; +}; + +function hasWindow() { + return typeof window !== 'undefined'; +} +function getNodeName(node) { + if (isNode(node)) { + return (node.nodeName || '').toLowerCase(); + } + // Mocked nodes in testing environments may not be instances of Node. By + // returning `#document` an infinite loop won't occur. + // https://github.com/floating-ui/floating-ui/issues/2317 + return '#document'; +} +function getWindow(node) { + var _node$ownerDocument; + return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; +} +function getDocumentElement(node) { + var _ref; + return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; +} +function isNode(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Node || value instanceof getWindow(value).Node; +} +function isElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Element || value instanceof getWindow(value).Element; +} +function isHTMLElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement; +} +function isShadowRoot(value) { + if (!hasWindow() || typeof ShadowRoot === 'undefined') { + return false; + } + return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot; +} +function isOverflowElement(element) { + const { + overflow, + overflowX, + overflowY, + display + } = getComputedStyle$1(element); + return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && display !== 'inline' && display !== 'contents'; +} +function isTableElement(element) { + return /^(table|td|th)$/.test(getNodeName(element)); +} +function isTopLayer(element) { + try { + if (element.matches(':popover-open')) { + return true; + } + } catch (_e) { + // no-op + } + try { + return element.matches(':modal'); + } catch (_e) { + return false; + } +} +const willChangeRe = /transform|translate|scale|rotate|perspective|filter/; +const containRe = /paint|layout|strict|content/; +const isNotNone = value => !!value && value !== 'none'; +let isWebKitValue; +function isContainingBlock(elementOrCss) { + const css = isElement(elementOrCss) ? getComputedStyle$1(elementOrCss) : elementOrCss; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + return isNotNone(css.transform) || isNotNone(css.translate) || isNotNone(css.scale) || isNotNone(css.rotate) || isNotNone(css.perspective) || !isWebKit() && (isNotNone(css.backdropFilter) || isNotNone(css.filter)) || willChangeRe.test(css.willChange || '') || containRe.test(css.contain || ''); +} +function getContainingBlock(element) { + let currentNode = getParentNode(element); + while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { + if (isContainingBlock(currentNode)) { + return currentNode; + } else if (isTopLayer(currentNode)) { + return null; + } + currentNode = getParentNode(currentNode); + } + return null; +} +function isWebKit() { + if (isWebKitValue == null) { + isWebKitValue = typeof CSS !== 'undefined' && CSS.supports && CSS.supports('-webkit-backdrop-filter', 'none'); + } + return isWebKitValue; +} +function isLastTraversableNode(node) { + return /^(html|body|#document)$/.test(getNodeName(node)); +} +function getComputedStyle$1(element) { + return getWindow(element).getComputedStyle(element); +} +function getNodeScroll(element) { + if (isElement(element)) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + return { + scrollLeft: element.scrollX, + scrollTop: element.scrollY + }; +} +function getParentNode(node) { + if (getNodeName(node) === 'html') { + return node; + } + const result = + // Step into the shadow DOM of the parent of a slotted node. + node.assignedSlot || + // DOM Element detected. + node.parentNode || + // ShadowRoot detected. + isShadowRoot(node) && node.host || + // Fallback. + getDocumentElement(node); + return isShadowRoot(result) ? result.host : result; +} +function getNearestOverflowAncestor(node) { + const parentNode = getParentNode(node); + if (isLastTraversableNode(parentNode)) { + return node.ownerDocument ? node.ownerDocument.body : node.body; + } + if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { + return parentNode; + } + return getNearestOverflowAncestor(parentNode); +} +function getOverflowAncestors(node, list, traverseIframes) { + var _node$ownerDocument2; + if (list === void 0) { + list = []; + } + if (traverseIframes === void 0) { + traverseIframes = true; + } + const scrollableAncestor = getNearestOverflowAncestor(node); + const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); + const win = getWindow(scrollableAncestor); + if (isBody) { + const frameElement = getFrameElement(win); + return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []); + } else { + return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); + } +} +function getFrameElement(win) { + return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; +} + +function getCssDimensions(element) { + const css = getComputedStyle$1(element); + // In testing environments, the `width` and `height` properties are empty + // strings for SVG elements, returning NaN. Fallback to `0` in this case. + let width = parseFloat(css.width) || 0; + let height = parseFloat(css.height) || 0; + const hasOffset = isHTMLElement(element); + const offsetWidth = hasOffset ? element.offsetWidth : width; + const offsetHeight = hasOffset ? element.offsetHeight : height; + const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; + if (shouldFallback) { + width = offsetWidth; + height = offsetHeight; + } + return { + width, + height, + $: shouldFallback + }; +} + +function unwrapElement(element) { + return !isElement(element) ? element.contextElement : element; +} + +function getScale(element) { + const domElement = unwrapElement(element); + if (!isHTMLElement(domElement)) { + return createCoords(1); + } + const rect = domElement.getBoundingClientRect(); + const { + width, + height, + $ + } = getCssDimensions(domElement); + let x = ($ ? round(rect.width) : rect.width) / width; + let y = ($ ? round(rect.height) : rect.height) / height; + + // 0, NaN, or Infinity should always fallback to 1. + + if (!x || !Number.isFinite(x)) { + x = 1; + } + if (!y || !Number.isFinite(y)) { + y = 1; + } + return { + x, + y + }; +} + +const noOffsets = /*#__PURE__*/createCoords(0); +function getVisualOffsets(element) { + const win = getWindow(element); + if (!isWebKit() || !win.visualViewport) { + return noOffsets; + } + return { + x: win.visualViewport.offsetLeft, + y: win.visualViewport.offsetTop + }; +} +function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { + if (isFixed === void 0) { + isFixed = false; + } + if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) { + return false; + } + return isFixed; +} + +function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { + if (includeScale === void 0) { + includeScale = false; + } + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + const clientRect = element.getBoundingClientRect(); + const domElement = unwrapElement(element); + let scale = createCoords(1); + if (includeScale) { + if (offsetParent) { + if (isElement(offsetParent)) { + scale = getScale(offsetParent); + } + } else { + scale = getScale(element); + } + } + const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); + let x = (clientRect.left + visualOffsets.x) / scale.x; + let y = (clientRect.top + visualOffsets.y) / scale.y; + let width = clientRect.width / scale.x; + let height = clientRect.height / scale.y; + if (domElement) { + const win = getWindow(domElement); + const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; + let currentWin = win; + let currentIFrame = getFrameElement(currentWin); + while (currentIFrame && offsetParent && offsetWin !== currentWin) { + const iframeScale = getScale(currentIFrame); + const iframeRect = currentIFrame.getBoundingClientRect(); + const css = getComputedStyle$1(currentIFrame); + const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; + const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; + x *= iframeScale.x; + y *= iframeScale.y; + width *= iframeScale.x; + height *= iframeScale.y; + x += left; + y += top; + currentWin = getWindow(currentIFrame); + currentIFrame = getFrameElement(currentWin); + } + } + return rectToClientRect({ + width, + height, + x, + y + }); +} + +// If has a CSS width greater than the viewport, then this will be +// incorrect for RTL. +function getWindowScrollBarX(element, rect) { + const leftScroll = getNodeScroll(element).scrollLeft; + if (!rect) { + return getBoundingClientRect(getDocumentElement(element)).left + leftScroll; + } + return rect.left + leftScroll; +} + +function getHTMLOffset(documentElement, scroll) { + const htmlRect = documentElement.getBoundingClientRect(); + const x = htmlRect.left + scroll.scrollLeft - getWindowScrollBarX(documentElement, htmlRect); + const y = htmlRect.top + scroll.scrollTop; + return { + x, + y + }; +} + +function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { + let { + elements, + rect, + offsetParent, + strategy + } = _ref; + const isFixed = strategy === 'fixed'; + const documentElement = getDocumentElement(offsetParent); + const topLayer = elements ? isTopLayer(elements.floating) : false; + if (offsetParent === documentElement || topLayer && isFixed) { + return rect; + } + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + let scale = createCoords(1); + const offsets = createCoords(0); + const isOffsetParentAnElement = isHTMLElement(offsetParent); + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isOffsetParentAnElement) { + const offsetRect = getBoundingClientRect(offsetParent); + scale = getScale(offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); + return { + width: rect.width * scale.x, + height: rect.height * scale.y, + x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, + y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y + }; +} + +function getClientRects(element) { + return Array.from(element.getClientRects()); +} + +// Gets the entire size of the scrollable document area, even extending outside +// of the `` and `` rect bounds if horizontally scrollable. +function getDocumentRect(element) { + const html = getDocumentElement(element); + const scroll = getNodeScroll(element); + const body = element.ownerDocument.body; + const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); + const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); + let x = -scroll.scrollLeft + getWindowScrollBarX(element); + const y = -scroll.scrollTop; + if (getComputedStyle$1(body).direction === 'rtl') { + x += max(html.clientWidth, body.clientWidth) - width; + } + return { + width, + height, + x, + y + }; +} + +// Safety check: ensure the scrollbar space is reasonable in case this +// calculation is affected by unusual styles. +// Most scrollbars leave 15-18px of space. +const SCROLLBAR_MAX = 25; +function getViewportRect(element, strategy) { + const win = getWindow(element); + const html = getDocumentElement(element); + const visualViewport = win.visualViewport; + let width = html.clientWidth; + let height = html.clientHeight; + let x = 0; + let y = 0; + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + const visualViewportBased = isWebKit(); + if (!visualViewportBased || visualViewportBased && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + const windowScrollbarX = getWindowScrollBarX(html); + // `overflow: hidden` + `scrollbar-gutter: stable` reduces the + // visual width of the but this is not considered in the size + // of `html.clientWidth`. + if (windowScrollbarX <= 0) { + const doc = html.ownerDocument; + const body = doc.body; + const bodyStyles = getComputedStyle(body); + const bodyMarginInline = doc.compatMode === 'CSS1Compat' ? parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight) || 0 : 0; + const clippingStableScrollbarWidth = Math.abs(html.clientWidth - body.clientWidth - bodyMarginInline); + if (clippingStableScrollbarWidth <= SCROLLBAR_MAX) { + width -= clippingStableScrollbarWidth; + } + } else if (windowScrollbarX <= SCROLLBAR_MAX) { + // If the scrollbar is on the left, the width needs to be extended + // by the scrollbar amount so there isn't extra space on the right. + width += windowScrollbarX; + } + return { + width, + height, + x, + y + }; +} + +// Returns the inner client rect, subtracting scrollbars if present. +function getInnerBoundingClientRect(element, strategy) { + const clientRect = getBoundingClientRect(element, true, strategy === 'fixed'); + const top = clientRect.top + element.clientTop; + const left = clientRect.left + element.clientLeft; + const scale = isHTMLElement(element) ? getScale(element) : createCoords(1); + const width = element.clientWidth * scale.x; + const height = element.clientHeight * scale.y; + const x = left * scale.x; + const y = top * scale.y; + return { + width, + height, + x, + y + }; +} +function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { + let rect; + if (clippingAncestor === 'viewport') { + rect = getViewportRect(element, strategy); + } else if (clippingAncestor === 'document') { + rect = getDocumentRect(getDocumentElement(element)); + } else if (isElement(clippingAncestor)) { + rect = getInnerBoundingClientRect(clippingAncestor, strategy); + } else { + const visualOffsets = getVisualOffsets(element); + rect = { + x: clippingAncestor.x - visualOffsets.x, + y: clippingAncestor.y - visualOffsets.y, + width: clippingAncestor.width, + height: clippingAncestor.height + }; + } + return rectToClientRect(rect); +} +function hasFixedPositionAncestor(element, stopNode) { + const parentNode = getParentNode(element); + if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { + return false; + } + return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode); +} + +// A "clipping ancestor" is an `overflow` element with the characteristic of +// clipping (or hiding) child elements. This returns all clipping ancestors +// of the given element up the tree. +function getClippingElementAncestors(element, cache) { + const cachedResult = cache.get(element); + if (cachedResult) { + return cachedResult; + } + let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body'); + let currentContainingBlockComputedStyle = null; + const elementIsFixed = getComputedStyle$1(element).position === 'fixed'; + let currentNode = elementIsFixed ? getParentNode(element) : element; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { + const computedStyle = getComputedStyle$1(currentNode); + const currentNodeIsContaining = isContainingBlock(currentNode); + if (!currentNodeIsContaining && computedStyle.position === 'fixed') { + currentContainingBlockComputedStyle = null; + } + const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && (currentContainingBlockComputedStyle.position === 'absolute' || currentContainingBlockComputedStyle.position === 'fixed') || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); + if (shouldDropCurrentNode) { + // Drop non-containing blocks. + result = result.filter(ancestor => ancestor !== currentNode); + } else { + // Record last containing block for next iteration. + currentContainingBlockComputedStyle = computedStyle; + } + currentNode = getParentNode(currentNode); + } + cache.set(element, result); + return result; +} + +// Gets the maximum area that the element is visible in due to any number of +// clipping ancestors. +function getClippingRect(_ref) { + let { + element, + boundary, + rootBoundary, + strategy + } = _ref; + const elementClippingAncestors = boundary === 'clippingAncestors' ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary); + const clippingAncestors = [...elementClippingAncestors, rootBoundary]; + const firstRect = getClientRectFromClippingAncestor(element, clippingAncestors[0], strategy); + let top = firstRect.top; + let right = firstRect.right; + let bottom = firstRect.bottom; + let left = firstRect.left; + for (let i = 1; i < clippingAncestors.length; i++) { + const rect = getClientRectFromClippingAncestor(element, clippingAncestors[i], strategy); + top = max(rect.top, top); + right = min(rect.right, right); + bottom = min(rect.bottom, bottom); + left = max(rect.left, left); + } + return { + width: right - left, + height: bottom - top, + x: left, + y: top + }; +} + +function getDimensions(element) { + const { + width, + height + } = getCssDimensions(element); + return { + width, + height + }; +} + +function getRectRelativeToOffsetParent(element, offsetParent, strategy) { + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + const isFixed = strategy === 'fixed'; + const rect = getBoundingClientRect(element, true, isFixed, offsetParent); + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + const offsets = createCoords(0); + + // If the scrollbar appears on the left (e.g. RTL systems). Use + // Firefox with layout.scrollbar.side = 3 in about:config to test this. + function setLeftRTLScrollbarOffset() { + offsets.x = getWindowScrollBarX(documentElement); + } + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isOffsetParentAnElement) { + const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } else if (documentElement) { + setLeftRTLScrollbarOffset(); + } + } + if (isFixed && !isOffsetParentAnElement && documentElement) { + setLeftRTLScrollbarOffset(); + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); + const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; + const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; + return { + x, + y, + width: rect.width, + height: rect.height + }; +} + +function isStaticPositioned(element) { + return getComputedStyle$1(element).position === 'static'; +} + +function getTrueOffsetParent(element, polyfill) { + if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') { + return null; + } + if (polyfill) { + return polyfill(element); + } + let rawOffsetParent = element.offsetParent; + + // Firefox returns the element as the offsetParent if it's non-static, + // while Chrome and Safari return the element. The element must + // be used to perform the correct calculations even if the element is + // non-static. + if (getDocumentElement(element) === rawOffsetParent) { + rawOffsetParent = rawOffsetParent.ownerDocument.body; + } + return rawOffsetParent; +} + +// Gets the closest ancestor positioned element. Handles some edge cases, +// such as table ancestors and cross browser bugs. +function getOffsetParent(element, polyfill) { + const win = getWindow(element); + if (isTopLayer(element)) { + return win; + } + if (!isHTMLElement(element)) { + let svgOffsetParent = getParentNode(element); + while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { + if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { + return svgOffsetParent; + } + svgOffsetParent = getParentNode(svgOffsetParent); + } + return win; + } + let offsetParent = getTrueOffsetParent(element, polyfill); + while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) { + offsetParent = getTrueOffsetParent(offsetParent, polyfill); + } + if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { + return win; + } + return offsetParent || getContainingBlock(element) || win; +} + +const getElementRects = async function (data) { + const getOffsetParentFn = this.getOffsetParent || getOffsetParent; + const getDimensionsFn = this.getDimensions; + const floatingDimensions = await getDimensionsFn(data.floating); + return { + reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), + floating: { + x: 0, + y: 0, + width: floatingDimensions.width, + height: floatingDimensions.height + } + }; +}; + +function isRTL(element) { + return getComputedStyle$1(element).direction === 'rtl'; +} + +const platform = { + convertOffsetParentRelativeRectToViewportRelativeRect, + getDocumentElement, + getClippingRect, + getOffsetParent, + getElementRects, + getClientRects, + getDimensions, + getScale, + isElement, + isRTL +}; + +function rectsAreEqual(a, b) { + return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; +} + +// https://samthor.au/2021/observing-dom/ +function observeMove(element, onMove) { + let io = null; + let timeoutId; + const root = getDocumentElement(element); + function cleanup() { + var _io; + clearTimeout(timeoutId); + (_io = io) == null || _io.disconnect(); + io = null; + } + function refresh(skip, threshold) { + if (skip === void 0) { + skip = false; + } + if (threshold === void 0) { + threshold = 1; + } + cleanup(); + const elementRectForRootMargin = element.getBoundingClientRect(); + const { + left, + top, + width, + height + } = elementRectForRootMargin; + if (!skip) { + onMove(); + } + if (!width || !height) { + return; + } + const insetTop = floor(top); + const insetRight = floor(root.clientWidth - (left + width)); + const insetBottom = floor(root.clientHeight - (top + height)); + const insetLeft = floor(left); + const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; + const options = { + rootMargin, + threshold: max(0, min(1, threshold)) || 1 + }; + let isFirstUpdate = true; + function handleObserve(entries) { + const ratio = entries[0].intersectionRatio; + if (ratio !== threshold) { + if (!isFirstUpdate) { + return refresh(); + } + if (!ratio) { + // If the reference is clipped, the ratio is 0. Throttle the refresh + // to prevent an infinite loop of updates. + timeoutId = setTimeout(() => { + refresh(false, 1e-7); + }, 1000); + } else { + refresh(false, ratio); + } + } + if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) { + // It's possible that even though the ratio is reported as 1, the + // element is not actually fully within the IntersectionObserver's root + // area anymore. This can happen under performance constraints. This may + // be a bug in the browser's IntersectionObserver implementation. To + // work around this, we compare the element's bounding rect now with + // what it was at the time we created the IntersectionObserver. If they + // are not equal then the element moved, so we refresh. + refresh(); + } + isFirstUpdate = false; + } + + // Older browsers don't support a `document` as the root and will throw an + // error. + try { + io = new IntersectionObserver(handleObserve, { + ...options, + // Handle