Compare commits

...

262 Commits

Author SHA1 Message Date
Mark Otto e12dddc50e Add rudimentary nested dropdowns 2022-02-23 14:04:38 -08:00
Mark Otto 88437bd464 Split up padding CSS vars for dropdowns 2022-02-23 14:01:58 -08:00
Julien Déramond 333d89e498 Docs: replace CSS by utilities in examples (#35699)
* Drop .opacity-50 and .opacity-75 redefinition in examples

* Drop unused .card-img-right from blog example CSS files

* Use line-height utilities when possible

* Use rounded-* utilities in examples

* Replace .nav-underline by .nav-scroller and use it in examples.html default

* Use .mb-1 for .blog-post-title

* Remove unused CSS rule and use .fw-* utilities for carousels examples

* Use utilities for cheatsheet examples

* Extract some CSS to utilities for .nav-masthead .nav-link in cover example

* Dashboard group of minor modifications

* Dropdowns example: refactoring

* Dropdowns example refactoring: fix linting by removing selector by id

* Features example refactoring

* Headers example refactoring

* List groups example refactoring

* Sidebars example refactoring

* Sign-in example refactoring

* Starter template refactoring

* Fix RTL examples

Co-authored-by: Mark Otto <markd.otto@gmail.com>
2022-02-23 10:31:18 -08:00
Julien Déramond 36765912c6 Replace non-existent --bs-btn-padding by --bs-btn-padding-{x|y} 2022-02-23 10:24:50 -08:00
chefarbeiter a381ee320b Fix modal doc 2022-02-22 15:12:20 +02:00
Gaël Poupard 37f3977e6d Rely on border-width for <hr> size (#35491)
* fix(reboot): revert hr styles to v4 implementation

* docs(cheatsheet): add a hr example

* fix(reboot): currentColor is the initial border-color value

* Document hr element in Reboot docs

* Update migration guide

* Update scss/_variables.scss

Co-authored-by: Mark Otto <markd.otto@gmail.com>
2022-02-22 10:02:47 +02:00
xrkffgg 0804c0043f CI: add issues-helper (#35846) 2022-02-22 09:55:29 +02:00
Christian Oliff 9ac8e80660 remove opacity utlitilies from example CSS (#35877)
These are included in Bootstrap itself now

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-02-22 09:39:57 +02:00
XhmikosR 3a9b0b8b6f Update devDependencies and pin karma-rollup-preprocessor (#35868)
* @babel/cli                 ^7.17.0  →  ^7.17.6
* @babel/core                ^7.17.2  →  ^7.17.5
* eslint-plugin-unicorn      ^40.1.0  →  ^41.0.0
* rollup                     ^2.67.2  →  ^2.68.0
* sass                       ^1.49.7  →  ^1.49.8
* terser                     ^5.10.0  →  ^5.11.0
2022-02-22 09:35:36 +02:00
GeoSot cb8726d9e7 Dropdown: use a better selector to avoid triggering click if button is disabled (#35866) 2022-02-19 16:22:32 +02:00
GeoSot 353ad45b4b Dropdown: use a combined selector to filter foreign not shown instances iteration (#35766) 2022-02-19 16:16:51 +02:00
GeoSot 4b17868fb4 tests: revisit all tests using Promise.reject instead of throwing an error (#35765) 2022-02-19 16:08:16 +02:00
XhmikosR ae12d645ef Replace AnchorJS with a Hugo render hook (#32953)
* Replace AnchorJS with a Hugo render hook

* docs(anchors): improve aria-label on anchor links

* docs(anchors): show anchor link when ed

* docs(anchors): add hash in pseudo-element

Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-02-19 15:16:23 +02:00
GeoSot 407af8ac7f Make event name helper and use it on tooltip & popover to reduce dist sizes (#35856)
* feat: create eventName getter function in baseComponent

* refactor: use `eventName` getter on tooltip & popover
2022-02-19 15:10:47 +02:00
GeoSot 642d756eea Carousel: remove one more call to ActiveIndex 2022-02-19 14:52:36 +02:00
GeoSot d52f6c9de1 Carousel: change argument to _setActiveIndicatorElement, from element to index 2022-02-19 14:52:36 +02:00
GeoSot 928bdcadc5 Carousel: make direct triggering of slid event, instead of using a callback 2022-02-19 14:52:36 +02:00
GeoSot d97125475b Carousel: merge slide functionality, regardless of whether it is animated or not 2022-02-19 14:52:36 +02:00
GeoSot a247fe9b27 Carousel: simplify initialization on document load, using getOrCreateInstance 2022-02-19 14:52:36 +02:00
GeoSot ccba6a3589 Carousel: remove redundant config merge on dataApiClickHandler, as it is done by default in the constructor 2022-02-19 14:52:36 +02:00
Jann Westermann 546e34cf85 Apply list group numbering to all items (#35822) 2022-02-18 01:17:37 +02:00
Mark Otto f7f450ec59 Add null modal-footer-bg variable (#35858)
Fixes #35782
2022-02-18 01:14:18 +02:00
Louis-Maxime Piton d82602063a New CSS variable for Navbars (#35829)
* Adding a brand new CSS var

* Update scss/_variables.scss

Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-02-17 11:25:01 -08:00
Nudasoft 999bfaa036 Add !important property to colored links. (#35740)
* Add !important property to colored links.

* Apply suggestions from code review

Co-authored-by: Mark Otto <otto@github.com>
2022-02-17 11:20:44 -08:00
Alan Christian 5565c64bde code indentation in readme 2022-02-17 10:23:18 -08:00
Mark Otto 63f75bccd8 Rename dropdown-shadow to dropdown-box-shadow for consistency 2022-02-16 10:36:01 -08:00
Mark Otto 48a7160cf4 Convert pagination component to CSS variables (#35399)
* Convert pagination component to CSS variables

* Use RFS mixin

* Remove the useless fallback
2022-02-16 10:26:24 -08:00
Eugene Kopyov 2a09f10676 Fixed border radius var name and .show target 2022-02-16 08:48:54 -08:00
XhmikosR 42da2b9556 Update devDependencies (#35841)
* Update devDependencies

* @babel/core             ^7.17.0  →  ^7.17.2
* @rollup/plugin-replace   ^3.0.1  →   ^3.1.0
* eslint                   ^8.8.0  →   ^8.9.0
* eslint-config-xo        ^0.39.0  →  ^0.40.0
* hugo-bin                ^0.80.1  →  ^0.80.2
* karma                   ^6.3.15  →  ^6.3.16
* rollup                  ^2.67.1  →  ^2.67.2

* Fix new ESLint errors
2022-02-15 08:50:37 +02:00
Mark Otto d568e029da Convert dropdowns to CSS variables 2022-02-14 19:11:35 -08:00
Mark Otto 3e6265ac55 Rewrite custom docs buttons
- Use CSS variables wherever possible
- Rename purple-bright to violet for brevity, add CSS vars versions
- Rename download to accent
2022-02-14 19:00:59 -08:00
Mark Otto 85938bb2ed Tighten bundlewatch 2022-02-14 19:00:59 -08:00
Mark Otto ef9d8538a0 Convert .btn and mixins to use CSS variables 2022-02-14 19:00:59 -08:00
Mark Otto 918a86b425 Correct the horizontal padding on grid containers (#35825)
* Correct the horizontal padding on grid containers

* Don't halve the container-padding-x variable
2022-02-14 15:07:17 -08:00
vpakati a163ed76d4 Update reboot.md
Add the missing right parenthesis.
2022-02-14 15:03:05 -08:00
Julien Déramond daefd34693 Fix typo in Badges CSS variables description 2022-02-14 16:10:42 +02:00
Mark Otto 48807758af Fixes #32082
Remove sentence in docs that's no longer needed. Fixes #32082.
2022-02-11 10:06:15 -08:00
Mark Otto a7942190c7 Bump version callout 2022-02-10 19:51:23 -08:00
Mark Otto f2d33b2a1e Bump bundlewatch 2022-02-10 19:51:23 -08:00
Mark Otto 7e71fe7bae Convert .badge to CSS variables 2022-02-10 19:51:23 -08:00
Julien Déramond d3babf7d7f Remove remaining .navbar-light classes (#35814)
* Fix typo in CSS variables documentation

* Drop remaining .navbar-light classes
2022-02-09 21:12:08 +02:00
Fabián Karaben d19c635f16 Add Spanish translation
The translation of version 5 of Bootstrap is added, by the Esdocu project.
2022-02-08 17:14:29 -08:00
Anton 761c4ff235 Fix overriding styles of .list-group-item-action
~~~html
<style>
.list-group-item-action {color: blue;}
.list-group-item {color: red;}
</style>
<p class="list-group-item">red text!</p>
<p class="list-group-item list-group-item-action">still red text!</p>
~~~
because `.list-group-item` declared after `.list-group-item-action` (order in attribute `class` no effect)
2022-02-08 17:11:46 -08:00
Christopher Yeleighton da9c007139 README: About release planning 2022-02-08 17:03:45 -08:00
Sebastian Podjasek 0853778eba Support floating labels on .form-control-plaintext (#32840)
* Support floating labels on `.form-control-plaintext`

* Update floating-labels.md

* Apply suggestions from code review

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
Co-authored-by: Mark Otto <otto@github.com>
2022-02-08 12:38:29 -08:00
Mark Otto 4308b67e59 Add .form-check-reverse modifier class (#33606)
* Add .form-check-reverse modifier class

* Update checks-radios.md

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-02-08 12:33:39 -08:00
Mark Otto 71a211f2bd bundlewatch 2022-02-08 10:39:58 -08:00
Mark Otto 27a0f40dc8 Add deprecation notice for .navbar-light 2022-02-08 10:39:58 -08:00
Mark Otto c9cec89764 Convert navbar to CSS variables
Co-Authored-By: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-02-08 10:39:58 -08:00
XhmikosR 8f11c52919 Update devDependencies (#35798)
* @babel/cli                  ^7.16.8  →  ^7.17.0
* @babel/core                ^7.16.12  →  ^7.17.0
* karma                       ^6.3.13  →  ^6.3.15
* karma-rollup-preprocessor    ^7.0.7  →   ^7.0.8
* postcss                      ^8.4.5  →   ^8.4.6
* rollup                      ^2.66.1  →  ^2.67.1
* sass                        ^1.49.4  →  ^1.49.7
2022-02-08 09:21:59 +02:00
Anton a805330f63 Optimize jQueryInterface in Collapse (#35689)
extracts config initialization from cycle
2022-02-07 10:50:26 +02:00
XhmikosR 77e02a07c7 package.json: add GitHub Sponsors and reorder properties (#35451) 2022-02-07 10:15:17 +02:00
GeoSot 96c67a7ff7 Fix empty content of tooltip after 'copy' action in docs (#35773)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-02-07 10:05:43 +02:00
The Fake Cake ff2472becc Set cursor: default on disabled .form-check-label (#35082)
Fixes #35056
2022-02-07 09:55:44 +02:00
Macinto5h d2986daa12 Add zebra striping for table columns
Co-Authored-By: Macallan Camara <44030647+Macinto5h@users.noreply.github.com>
Co-Authored-By: XhmikosR <xhmikosr@gmail.com>
2022-02-06 15:59:18 -08:00
Ty Mick 43a9216a7f Move gap utility API from "Flex" to "Spacing"
The `gap` utility is described on the [Spacing page][1] but is not
actually mentioned on the [Flex page][2] (apart from the [API
section][3]).

[1]: https://getbootstrap.com/docs/5.1/utilities/spacing/
[2]: https://getbootstrap.com/docs/5.1/utilities/flex/
[3]: https://getbootstrap.com/docs/5.1/utilities/flex/#utilities-api
2022-02-06 15:45:22 -08:00
Sigurd Moland Wahl 4dc4108460 Fixed typo in docs 2022-02-06 14:31:59 -08:00
Florian Lacreuse 957c1dd6ea Fix dropdown docs about autoclose and esc key. 2022-02-03 18:20:47 +02:00
Gaël Poupard a5483a8a96 Breadcrumb docs: drop confusing currentColor and add new callout (#35434) 2022-02-01 15:37:33 +02:00
XhmikosR dc09509193 Update find-unused-sass-variables to v4.0.1 (#35761) 2022-02-01 13:03:48 +02:00
XhmikosR cf7fec8a2e event-handler.js: remove unneeded return statement 2022-02-01 12:43:19 +02:00
GeoSot a1e924c4da Event-handler: use Array.find instead of for 2022-02-01 12:43:19 +02:00
GeoSot fc7c5fcb7a Event-handler: initialize variable properly 2022-02-01 12:43:19 +02:00
GeoSot 3f7b31e0e0 Fix Popover test that randomly fails on BrowserStack (#35757)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-02-01 10:33:18 +02:00
GeoSot c44d99f55c Dropdown: use destructured variables in dataApyKeydownHandler 2022-02-01 08:58:46 +02:00
GeoSot c14fc989df Dropdown: dropdown doesn't document data-bs-target option & parentNode is ALWAYS the wrapper for toggle & menu 2022-02-01 08:58:46 +02:00
GeoSot d105439235 Dropdown: merge instance identification in dataApiKeydownHandler
As we use the `dataApiKeydownHandler` only for events that are triggered on `[data-bs-toggle="dropdown"]` or on `.dropdown-menu`, we can ensure that their `parentNode` will ALWAYS be the `.dropdown` wrapper
2022-02-01 08:58:46 +02:00
Toby Zerner 22bbff0b03 Remove incorrect statement about the disabled attribute on <form> (#35713)
> You can disable every form element within a form with the `disabled` attribute on the `<form>`.

I really want to be mistaken, because this would be a very useful feature! But I don't believe it's true. I can't find anything about this on MDN Web Docs, and adding the `disabled` attribute to a `<form>` does nothing on any browser in my testing.

The `disabled` attribute on a `<fieldset>` does disable all descendant form controls – perhaps that's where the mixup has come from.
2022-02-01 08:40:59 +02:00
XhmikosR 9d44b603d5 Update devDependencies (#35743)
* clean-css-cli   ^5.5.0  →   ^5.5.2
* cspell         ^5.16.0  →  ^5.17.0
* eslint          ^8.7.0  →   ^8.8.0
* hugo-bin       ^0.80.0  →  ^0.80.1
* karma          ^6.3.12  →  ^6.3.13
* rollup         ^2.66.0  →  ^2.66.1
* sass           ^1.49.0  →  ^1.49.4
2022-02-01 08:35:01 +02:00
Quy 76802e2d47 Change X to Extra 2022-01-30 18:09:48 -08:00
GeoSot 7f04f84bf8 Dropdown: use only one check for shown state 2022-01-30 18:01:09 +02:00
GeoSot 5f1c542d67 Dropdown: get dropdown's parent in one place 2022-01-30 18:01:09 +02:00
GeoSot 74f24cdf24 More tooltip refactoring (#35546)
* Tooltip.js: move `shown` check to method

* Tooltip.js: move Popper's creation to method

* Tooltip.js: merge checks before `hide`

* Tooltip.js: minor refactoring on `toggle` method
2022-01-30 17:39:03 +02:00
XhmikosR e1020a43a5 Move cspell to Actions (#35593)
* Move cspell to Actions

* Remove the now unused `docs-spellcheck` npm script
2022-01-30 16:39:30 +02:00
GeoSot 882185bbde Change selector-engine.js parents method to utilize better js native methods (#35684)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-01-30 16:24:03 +02:00
XhmikosR 89f88762c5 Fix visual tests (#35585)
* Fix visual tests

They broke in #34509

* load bundle.js in visual tests

Co-authored-by: GeoSot <geo.sotis@gmail.com>
2022-01-30 16:12:24 +02:00
XhmikosR f77a58b381 Update cspell config (#35680)
* use `en-US`
* scan all markdown files
* ignore dist, rtl and tests files
2022-01-30 16:11:38 +02:00
GeoSot dcbe7b6f31 Modal.js: remove unnecessary checks from test 2022-01-30 15:39:34 +02:00
GeoSot 28c9002573 Modal: handle click event from backdrop callback 2022-01-30 15:39:34 +02:00
GeoSot aa650f0f1e tests: replace 'done' callback with 'Promise' to fix deprecation errors (#35659)
Reference:

https://jasmine.github.io/tutorials/async

'DEPRECATION: An asynchronous function called its 'done' callback more than once. This is a bug in the spec, beforeAll, beforeEach, afterAll, or afterEach function in question. This will be treated as an error in a future version. See<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> for more information.
2022-01-30 14:30:04 +02:00
GeoSot d092817059 Event handler: merge new Event with new CustomEvent 2022-01-30 14:15:17 +02:00
GeoSot fa93995123 Event handler: replace deprecated initEvent 2022-01-30 14:15:17 +02:00
XhmikosR 640542e606 Move linkinator to GitHub Actions. (#35573)
* Move linkinator to GitHub Actions.

* Remove `docs-linkinator` npm script since it's no longer used
2022-01-29 14:42:56 +02:00
Dimitri Papadopoulos Orfanos eb2fda2110 docs: fix a couple typos found with codespell (#35733)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2022-01-29 13:30:02 +02:00
XhmikosR 0840105d7f SelectorEngine: remove moot space 2022-01-29 13:25:30 +02:00
XhmikosR 558002f3dc Return early in more places 2022-01-29 13:25:30 +02:00
XhmikosR 7d3bc44bb0 dropdown: Move constant 2022-01-29 13:25:30 +02:00
GeoSot a8887ea8a8 collapse: merge class toggling 2022-01-29 13:25:30 +02:00
XhmikosR b5147ec218 event-handler.js: use for...of 2022-01-29 13:25:30 +02:00
XhmikosR 62d86c07f8 Rename variables 2022-01-29 13:25:30 +02:00
XhmikosR 3ac4451d47 backdrop.js: cache _getElement calls 2022-01-29 13:25:30 +02:00
XhmikosR 0c3dfe104b Remove a few unneeded variables 2022-01-29 13:25:30 +02:00
A Web Artisan 2964c12bb9 docs: remove moot autocomplete attributes (#35741) 2022-01-29 13:20:01 +02:00
Neeraj Kumar Das d122f40526 Remove duplicate Octicons entry in icons list
Update Octicons website
2022-01-25 15:22:00 -08:00
Kirill Zdornyy c1222d6952 Fix spelling mistake in footer example (#35704) 2022-01-25 12:06:50 +02:00
XhmikosR 1411181b1b Update devDependencies (#35712)
* @babel/core        ^7.16.7  →  ^7.16.12
* @babel/preset-env  ^7.16.8  →  ^7.16.11
* cspell             ^5.15.2  →   ^5.16.0
* karma              ^6.3.11  →   ^6.3.12
* rollup             ^2.64.0  →   ^2.66.0
* sass               ^1.48.0  →   ^1.49.0
2022-01-25 12:03:42 +02:00
Julien Déramond 520cc8de92 Docs: group together reusable CSS for examples in a single stylesheet (#35649)
* Docs: group together examples reusable CSS in a stylesheet

* Use pointer-events utility in sidebars example

* Remove @import and move the content into _default/examples.html. Handle 2 sorts of dividers

* Remove footers.css extra css declaration

* Fix modals example

* Review: remove .b-example-hr

Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-01-19 13:27:57 +02:00
XhmikosR cd208341a1 Update devDependencies (#35686)
* bundlewatch             ^0.3.2  →   ^0.3.3
* cspell                 ^5.15.1  →  ^5.15.2
* eslint                  ^8.6.0  →   ^8.7.0
* eslint-plugin-unicorn  ^40.0.0  →  ^40.1.0
* hugo-bin               ^0.79.2  →  ^0.80.0
* karma                  ^6.3.10  →  ^6.3.11
* rollup                 ^2.63.0  →  ^2.64.0
* sass                   ^1.47.0  →  ^1.48.0
2022-01-18 07:09:40 +02:00
XhmikosR f0e1220970 README.md: mention Netlify (#35683)
* README.md: mention Netlify

* Update README.md

Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-01-13 14:52:07 +02:00
XhmikosR c99fa6ca26 build-plugins.js: use globby package (#35586)
We already use it in the change-version.js file
2022-01-13 13:12:10 +02:00
Ryan Berliner 14c7dc1e88 Fix: isVisible function behavior in case of a <details> element, on chrome 97 (#35682) 2022-01-13 10:55:05 +02:00
XhmikosR d581737f78 Update cspell config (#35647)
* specify the files to scan in the config
* enable `useGitignore` option
* remove a few unneeded suppressions
2022-01-11 11:17:12 +02:00
XhmikosR 17260410ba Update devDependencies (#35678)
* @babel/cli                   ^7.16.7  →  ^7.16.8
* @babel/preset-env            ^7.16.7  →  ^7.16.8
* @popperjs/core               ^2.11.0  →  ^2.11.2
* @rollup/plugin-node-resolve  ^13.1.2  →  ^13.1.3
* autoprefixer                 ^10.4.1  →  ^10.4.2
* cspell                       ^5.14.0  →  ^5.15.1
* karma                         ^6.3.9  →  ^6.3.10
* rollup                       ^2.62.0  →  ^2.63.0
* sass                         ^1.45.2  →  ^1.47.0
* shelljs                       ^0.8.4  →   ^0.8.5
2022-01-11 10:32:18 +02:00
Arslan Kalwar e8f4cba9a0 fixed button text (#35656)
Added button missing text
2022-01-05 19:44:22 +02:00
Sakurai Kenji 0dbec67514 Fix custom-checkbox and custom-radio in migration doc (#35609) 2022-01-05 19:24:59 +02:00
Patrick H. Lauke 0d054bb0f1 Remove explicit use of aria-hidden for offcanvas when closed (#35589)
Remove explicit use of aria-hidden & visibility for offcanvas when closed, handling it with css

Co-authored-by: GeoSot <geo.sotis@gmail.com>
Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2022-01-05 19:20:15 +02:00
Neeraj Kumar Das f7a1b18320 Fix offcanvas title in 'Enable body scrolling' example (#35621) 2022-01-05 19:01:05 +02:00
zhangchenglin 670e12dd0d Remove the useless class of the close button .text-reset 2022-01-05 18:40:15 +02:00
XhmikosR cebb8436c7 Update nuget/bootstrap.png (#35641) 2022-01-04 09:40:53 +02:00
XhmikosR 0b57c44056 Update devDependencies (#35640)
* @babel/cli                   ^7.16.0  →  ^7.16.7
* @babel/core                  ^7.16.5  →  ^7.16.7
* @babel/preset-env            ^7.16.5  →  ^7.16.7
* @rollup/plugin-node-resolve  ^13.1.1  →  ^13.1.2
* @rollup/plugin-replace        ^3.0.0  →   ^3.0.1
* autoprefixer                 ^10.4.0  →  ^10.4.1
* cspell                       ^5.13.4  →  ^5.14.0
* eslint                        ^8.5.0  →   ^8.6.0
* eslint-plugin-import         ^2.25.3  →  ^2.25.4
* eslint-plugin-unicorn        ^39.0.0  →  ^40.0.0
* sass                         ^1.45.1  →  ^1.45.2
2022-01-04 09:26:50 +02:00
XhmikosR 9680e17456 Bump copyright year to 2022 (#35639) 2022-01-03 15:03:42 +02:00
Anton 2c9ecd0b8d Update api.md (#35486)
* Wording tweak

* Tweak wording

Co-authored-by: Patrick H. Lauke <redux@splintered.co.uk>
2021-12-28 14:38:35 +02:00
XhmikosR 33c7f9fe69 Update devDependencies (#35591)
* hugo-bin                         ^0.79.0  →  ^0.79.2
* linkinator                        ^3.0.2  →   ^3.0.3
* rollup                           ^2.61.1  →  ^2.62.0
* stylelint                        ^14.1.0  →  ^14.2.0
* stylelint-config-twbs-bootstrap   ^3.0.0  →   ^3.0.1
2021-12-28 08:16:16 +02:00
XhmikosR deb8e3febd Add note to carousel crossfade docs about bg color (#35587)
Fixes #35181

Co-authored-by: Mark Otto <markdotto@gmail.com>
Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
2021-12-23 14:38:43 +02:00
Patrick H. Lauke f171d3e9f7 Strengthen and expand note about dynamic tabs with dropdown menus (#35588)
following on from https://github.com/twbs/bootstrap/pull/35213 this

- expands the note, making it clear it's not supported
- makes it a callout
2021-12-23 09:15:36 +02:00
GeoSot 0d4213bde3 Carousel: move repeated code to a method 2021-12-21 17:37:24 +02:00
GeoSot b8ee68cfa0 Carousel: remove always true visibilityState check
According to https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState `visibilityState` is always a string, so the check was always true
2021-12-21 17:37:24 +02:00
GeoSot ff4bf4a458 Carousel: move carousel default interval to _getConfig() and simplify it 2021-12-21 17:37:24 +02:00
GeoSot 6f79721c82 Carousel: return early and drop a loop.
We can achieve the same thing by querying the specific selector directly
2021-12-21 17:37:24 +02:00
GeoSot d60f146507 Carousel: add a helper to get the active element 2021-12-21 17:37:24 +02:00
GeoSot 65cf77ae3e Popover/Tooltip: Fix vertical alignment on arrow of tip elements (#35527)
Regression of #32692

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-21 17:19:29 +02:00
Kyle Tsang aec213711a docs: add reference to sticky-xxl utils (#35579)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-21 09:22:58 +02:00
Julien Déramond 32401fdc37 fix(docs): typo in Alerts CSS Variables description (#35575)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-21 09:09:43 +02:00
XhmikosR 8ccb27a213 Update devDependencies (#35545)
* cspell                                ^5.13.3  →  ^5.13.4
* eslint                                 ^8.4.1  →   ^8.5.0
* hugo-bin                              ^0.78.1  →  ^0.79.0
* linkinator                            ^2.16.2  →   ^3.0.2
* sass                                  ^1.45.0  →  ^1.45.1
* stylelint-config-twbs-bootstrap  ^3.0.0-beta1  →   ^3.0.0
2021-12-21 08:39:43 +02:00
Gaël Poupard a2c056e1f6 fix(alerts): ensure color is set and used (#35571) 2021-12-20 14:22:29 +02:00
Louis-Maxime Piton 42162546f2 docs: A fix for CSS Variables and some proposal (#35563)
* docs: Fix CSS variables sections

* Minor fix for dropdowns

* Minor fixes for URLs

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-18 13:53:36 +02:00
Phil E. Taylor c2db7108c2 Fix typo in comment (#35564)
s/moddal/modal
2021-12-18 07:58:31 +02:00
Mark Otto f729e4c7d1 Convert alerts to CSS variables (#35401)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-17 07:16:24 +02:00
Andy Jiang 4a66f229ed Add missing border-radius for btn-group (#35467)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-16 10:43:30 +02:00
Florian Lacreuse d17801265e Add sticky bottom utility (#35518)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-16 10:27:00 +02:00
Julien Déramond 2d07383e32 docs: Use param to set Bootstrap version in Contents page (#35556) 2021-12-16 10:20:48 +02:00
XhmikosR a22694da13 build/postcss.config.js: minor tweaks (#35506) 2021-12-15 10:52:49 +02:00
GeoSot e0960b08e0 Tooltip: remove extraneous call to _getConfig() (#35540)
BaseClass already initializes the config

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-15 10:47:32 +02:00
GeoSot d40fae456e Popover.js: Accept empty content through data-bs-content (#35514)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-15 10:41:31 +02:00
XhmikosR cd04fe015f Scrollspy: minor refactoring (#35512)
* reorder variables
* join lines
* use `filter(Boolean)` since it's clearer
* use `for...of`
2021-12-15 09:38:06 +02:00
XhmikosR cb46ad633c Reprocess inline SVGs with the latest SVGO (#35484) 2021-12-15 09:29:17 +02:00
Ty Mick 28f7c94475 docs: Add responsive variations for align-content-between (#35532)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-14 22:29:35 +02:00
XhmikosR 8cdb6c40bd Update stylelint and stylelint-config-twbs-bootstrap (#35438) 2021-12-14 09:51:31 +02:00
XhmikosR cd50942831 Update devDependencies (#35498)
* @babel/core                  ^7.16.0  →  ^7.16.5
* @babel/preset-env            ^7.16.4  →  ^7.16.5
* @rollup/plugin-node-resolve  ^13.0.6  →  ^13.1.1
* clean-css-cli                 ^5.4.2  →   ^5.5.0
* cspell                       ^5.13.2  →  ^5.13.3
* hugo-bin                     ^0.77.4  →  ^0.78.1
* postcss                       ^8.4.4  →   ^8.4.5
* postcss-cli                   ^9.0.2  →   ^9.1.0
* rollup                       ^2.60.2  →  ^2.61.1
* sass                         ^1.44.0  →  ^1.45.0
2021-12-14 09:26:43 +02:00
GeoSot 886b940796 Extract Component config functionality to a separate class (#33872)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-10 18:18:18 +02:00
XhmikosR 68f226750d JS tests: only test one Node.js version (#35481) 2021-12-10 16:40:32 +02:00
XhmikosR 63d38b1974 Tab: minor refactoring (#35511)
* remove unneeded parentheses
* move variable
2021-12-10 07:51:57 +02:00
XhmikosR 871c8bdd3f util/index.js: minor refactoring (#35510)
* rename variables
* remove an unused variable
* be more explicit
* reuse variable
2021-12-10 07:48:04 +02:00
XhmikosR eaa801c899 Toast: join multiple classList calls (#35507) 2021-12-10 07:42:08 +02:00
XhmikosR 94d4fa3b10 Fix tests fixture type (#35501)
Previously we were adding an Array instead of a String
2021-12-09 16:01:29 +02:00
GeoSot 28a5a72ed5 Scrollbar - remove margin/padding properties properly (#35388)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-09 15:49:28 +02:00
GeoSot c376cb0763 Dropdown: fix toggle focus after dropdown is hidden using the ESC button (#35500) 2021-12-09 15:34:17 +02:00
GeoSot 4fd5539c75 ScrollBar.js. Minor refactoring and add test (#35492) 2021-12-09 15:05:50 +02:00
Alexander Gitter 2a7015e630 Fix variable name in form overview docs (#35468)
These variables are called $input-btn-*, the documentation was erroneously talking about $btn-input-*.

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-07 20:51:50 +02:00
Anton e45111fb74 Replace inline styles in example "Dropdowns" (#35483) 2021-12-07 20:45:56 +02:00
GeoSot 328f723008 Tooltip: remove title attribute before show & add tests (#35456) 2021-12-07 15:51:56 +02:00
Alexander Gitter ba7863a5bb Fix typo in $purples (#35466)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-07 12:34:16 +02:00
dependabot[bot] b2200c3b24 Bump cspell from 5.13.1 to 5.13.2 (#35473)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v5.13.1...v5.13.2)

---
updated-dependencies:
- dependency-name: cspell
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-07 12:06:12 +02:00
XhmikosR a75d2098eb Update devDependencies (#35472) 2021-12-07 10:39:09 +02:00
Mark Otto a780d74b28 Docs offcanvas navbar (#34487)
* Redesign docs navbar to use offcanvas, tweak overall padding while I'm here

* Match code snippets to new gutter width

* Replace value with docs-specific variable

Opted not to do a CSS variable here since on .bd-content it would be inherited into our examples

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-03 10:08:01 +02:00
XhmikosR 6eef480e1e CI: add workflow_dispatch (#35454) 2021-12-02 23:02:11 +02:00
XhmikosR 2ff1eb0daa README.md: wrap file contents in a details element (#35452)
Co-authored-by: Mark Otto <markd.otto@gmail.com>
2021-12-02 22:53:12 +02:00
Louis-Maxime Piton d8999dd566 Tests: Minor fixes (#35455) 2021-12-02 12:01:11 +02:00
GeoSot 385fea49e8 Tooltip/Popover: add underscore prefix to protected functions 2021-12-01 18:00:36 +02:00
GeoSot bd79d69a73 Tooltip: a simple code-block position change 2021-12-01 18:00:36 +02:00
GeoSot 1f7b83203d Tooltip: simplify popper check 2021-12-01 18:00:36 +02:00
GeoSot 9b9372e8dd Tooltip: refactor _hoverState to Boolean to achieve better control 2021-12-01 18:00:36 +02:00
GeoSot 8eacbaa08b Tooltip: merge timeout functionality 2021-12-01 18:00:36 +02:00
GeoSot c69ccba08c Tooltip: Change _enter & _leave to work without arguments 2021-12-01 18:00:36 +02:00
GeoSot a20e4203fe Tooltip: Remove redundant config.delay check
`config.delay` is always an object after initialization
2021-12-01 18:00:36 +02:00
GeoSot 724663b3cd Tooltip: Remove Data.set usage for dynamically created tip
This is not used any further, so we were just setting it.
2021-12-01 18:00:36 +02:00
GeoSot 3baeb0a5c1 Tooltip: merge isAnimated checks 2021-12-01 18:00:36 +02:00
GeoSot 53c77c0203 Tooltip: refactor jQueryInterface 2021-12-01 18:00:36 +02:00
GeoSot a5945369bd Dropdown: change doc for data-bs-popper=none 2021-12-01 17:10:39 +02:00
GeoSot dd07c1ff9e Dropdown: clearMenus is always an event callback 2021-12-01 17:10:39 +02:00
GeoSot f71640f048 Dropdown: Clean more 2021-12-01 17:10:39 +02:00
GeoSot 21e5618ba7 Dropdown: rename vars 2021-12-01 17:10:39 +02:00
GeoSot 0686fa00f0 Dropdown: Remove redundant Space check 2021-12-01 17:10:39 +02:00
GeoSot a14a552d83 Dropdown: Deduplicate complex check 2021-12-01 17:10:39 +02:00
GeoSot bff95d55af Dropdown: Remove redundant check since the show method already does it 2021-12-01 17:10:39 +02:00
GeoSot 2d32802f53 Dropdown: Change constant to the way we use it 2021-12-01 17:10:39 +02:00
GeoSot fb5921dec4 Dropdown: Merge display='static' & isNavbar functionality activating static popper with no styles attached 2021-12-01 17:10:39 +02:00
GeoSot 137b324930 Dropdown: Remove static method used once 2021-12-01 17:10:39 +02:00
GeoSot 8b308b76f0 Dropdown tests: Use a function to improve readability (#35448) 2021-12-01 16:49:02 +02:00
XhmikosR 01b08d958c Remove the now outdated build/svgo.yml (#35447) 2021-12-01 15:16:24 +02:00
GeoSot cab62af2e6 Fix popover arrow & tooltip template after the setContent addition (#35441) 2021-12-01 15:10:10 +02:00
GeoSot 44a6cd724c Tooltip: remove leftover method (#35440)
Remove a leftover after #32692

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-12-01 14:53:56 +02:00
Ethan Clevenger 2fe90d8b16 Update edge-to-edge design instructions (#35446)
Add note about margins to prevent overflow.
2021-12-01 14:34:03 +02:00
XhmikosR e5d8256e42 tests/unit/util/scrollbar.spec.js: rename function
`parseInt` is a global one.
2021-12-01 14:30:49 +02:00
XhmikosR 11ce6c2dcd tests: fix a few typos 2021-12-01 14:30:49 +02:00
XhmikosR f8f9dc3b5c tests: remove extra spaces, unneeded arrays and add missing newlines 2021-12-01 14:30:49 +02:00
XhmikosR eb54e1a1ce tests: tweak Jasmine's matchers usage
Use:

* toBeNull
* toEqual
* toBeTrue
* toBeFalse
* toHaveSize
* toHaveClass
2021-12-01 14:30:49 +02:00
XhmikosR 5739bf7637 tests/browsers.js: remove unneeded export 2021-12-01 14:30:49 +02:00
Christopher Boik 2a51370e92 Fix variable name in customizing comment (#35414)
Correct table lightening variable to match the variable name defined and described in the loop variable section above
2021-11-30 16:41:08 +02:00
XhmikosR d13b5ca2a5 Update devDependencies (#35421)
* @popperjs/core  ^2.10.2  →  ^2.11.0
* cspell          ^5.13.0  →  ^5.13.1
* postcss         ^8.3.11  →   ^8.4.4
* rollup          ^2.60.1  →  ^2.60.2
* sass            ^1.43.4  →  ^1.44.0
2021-11-30 08:36:39 +02:00
GeoSot 1692fc6b4b Alert: add a couple more tests (#35419)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-29 14:32:11 +02:00
GeoSot 3129ff075b BaseComponent: add a couple more tests (#35410)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-29 14:27:03 +02:00
François Karman 5290080d4d Update typography.md
remove a unnecessary <abbr> tag
2021-11-25 14:08:52 -10:00
Travis Risner 45eb70e03c Correctly implement RFS in :root CSS variable for $body-font-size (#35326)
* rfs fix

* Update scss/_root.scss

* Update _reboot.scss

Co-authored-by: Mark Otto <otto@github.com>
Co-authored-by: Mark Otto <markd.otto@gmail.com>
2021-11-25 14:01:19 -10:00
Si Nguyen 3c8fbb6581 Update _close.scss 2021-11-25 13:56:32 -10:00
GeoSot 6f077ff7bc Clean tooltip component unneeded functionality (#32692) 2021-11-25 20:08:11 +02:00
GeoSot 374eeecfbc tooltip.js: use array.includes instead of for iteration (#35127) 2021-11-25 19:39:13 +02:00
GeoSot 91ad255e07 Change adjustDialog's if conditions to improve readability 2021-11-25 19:23:49 +02:00
GeoSot 569bca54d2 Add test for modal-content 2021-11-25 19:23:49 +02:00
GeoSot 0f9fd75d6c Respect modal's initial overflowY 2021-11-25 19:23:49 +02:00
GeoSot cc3e5789ec Remove some uncovered code that seems to be unused 2021-11-25 19:23:49 +02:00
GeoSot 92e664c921 Change check for dynamic modal 2021-11-25 19:23:49 +02:00
GeoSot fc33ce4b46 Tweak methods
Name them to be more descriptive and have agnostic functionality
2021-11-25 19:23:49 +02:00
GeoSot 79e01c3bad Some refactoring on modal, to improve readability and generic functionality 2021-11-25 19:23:49 +02:00
GeoSot 94a596fbcb Add a template factory helper to handle all template cases (#34519)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-25 19:14:02 +02:00
XhmikosR fa33e83f25 build/change-version.js: fix wrong map usage (#35395) 2021-11-24 10:49:35 +02:00
GeoSot 58ffe2334a build: read & dynamically resolve imports on plugins build (#34509)
Our individual js/dist files are now deduplicated properly thus resulting in a size reduction, which varies from ~25% to ~60% depending on the components used. The average savings are 20% uncompressed and ~15% with gzip.

This will mostly benefit cases that more than one component is imported from js/dist. In all other cases it doesn't have any effect.

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-23 17:37:14 +02:00
Neeraj Kumar Das 1ee058adf5 Make footer examples responsive to mobile screens (#35365) 2021-11-23 16:15:46 +02:00
Julien Déramond 7edad9453c Add missing scss/maps imports (#35373)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-23 10:06:33 +02:00
XhmikosR 8ddc42f584 Update devDependencies (#35368)
* @babel/preset-env      ^7.16.0  →  ^7.16.4
* cspell                 ^5.12.6  →  ^5.13.0
* eslint                  ^8.2.0  →   ^8.3.0
* eslint-plugin-unicorn  ^38.0.1  →  ^39.0.0
* hugo-bin               ^0.77.2  →  ^0.77.4
* karma                   ^6.3.8  →   ^6.3.9
* rollup                 ^2.60.0  →  ^2.60.1
2021-11-23 09:58:35 +02:00
XhmikosR 9c0163329f Update devDependencies (#35356)
* eslint-plugin-import  ^2.25.2  →  ^2.25.3
* linkinator            ^2.14.5  →  ^2.16.2
* nodemon               ^2.0.14  →  ^2.0.15
* rollup                ^2.59.0  →  ^2.60.0
* terser                 ^5.9.0  →  ^5.10.0
2021-11-16 08:10:32 +02:00
Mark Otto 9f099d3e4f Move reassigned Sass maps for colors to another stylesheet (#34942) 2021-11-15 13:03:48 +02:00
Tobias Nießen 96dcc150d5 Fix typo in vertical alignment of table cells (#35348) 2021-11-15 12:52:06 +02:00
XhmikosR 367caea501 Update devDependencies (#35322)
* cspell                 ^5.12.4  →  ^5.12.6
* eslint                  ^8.1.0  →   ^8.2.0
* eslint-plugin-unicorn  ^37.0.1  →  ^38.0.1
* hugo-bin               ^0.76.1  →  ^0.77.2
* karma                   ^6.3.7  →   ^6.3.8
* linkinator             ^2.14.4  →  ^2.14.5
* postcss-cli             ^9.0.1  →   ^9.0.2
* rtlcss                  ^3.4.0  →   ^3.5.0
2021-11-09 08:02:36 +02:00
Scott O'Hara e958cd2637 floating-labels: remove aria-label in the select example (#35327)
While it is understood that this is just an example, the visible text (label) of "Works with selects" and the `aria-label="Floating label select example"` created a [WCAG 2.5.3 Label in name](https://www.w3.org/WAI/WCAG21/quickref/#label-in-name) failure.  

As the `aria-label` isn't necessary here since this `select` is already provided an accessible name by its `label` element, removing the unnecessary `aria-label` seems the best course of action as:
* removing it solves the WCAG issue 
* it removes the potential implication to developers that they'd even _need_ an `aria-label` here, let alone indirectly suggesting that it's ok for the visible text and accessible name to be out of alignment
2021-11-09 07:54:42 +02:00
Julien Déramond 31998dfd1e Fix spacing utility classes mentioned in navbar supported content documentation (#35328)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-05 20:36:56 +02:00
Christian Oliff 98889f2144 README.md remove broken "David DM" dependency badges (#35313) 2021-11-03 14:36:39 +02:00
Mark Otto 6c706e947d Update import stacks required for modifying utilities (#35320) 2021-11-03 14:31:21 +02:00
kyletsang 889286cef1 Add top placement info to offcanvas docs 2021-11-02 13:56:47 -07:00
XhmikosR 05f1b15dda Update devDependencies (#35270)
* @babel/cli                ^7.15.7  →  ^7.16.0
* @babel/core               ^7.15.8  →  ^7.16.0
* @babel/preset-env         ^7.15.8  →  ^7.16.0
* autoprefixer              ^10.3.7  →  ^10.4.0
* cspell                    ^5.12.3  →  ^5.12.4
* eslint                     ^8.0.1  →   ^8.1.0
* karma                      ^6.3.5  →   ^6.3.7
* karma-firefox-launcher     ^2.1.1  →   ^2.1.2
* rollup                    ^2.58.0  →  ^2.59.0
* sass                      ^1.43.2  →  ^1.43.4
2021-11-02 08:09:33 +02:00
Michael Sørensen 44ea0d6925 make-col-ready(): remove the unused $gutter variable (#34334)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
Co-authored-by: Mark Otto <markd.otto@gmail.com>
2021-11-01 08:59:21 +02:00
Mark Otto 0c449b8b82 Always set the CSS variables for gutters in containers (#34644)
We already do this in rows, so to best support our containers, we need
to do it at the container level as well.

Fixes #32658, fixes #34614, closes #32658.

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-01 08:39:48 +02:00
Julien Déramond 328a29162d Add missing role="search" and type="search" in navbar doc and examples (#35223)
* Add missing `role="search"` and `type="search"` in navbar doc and examples

* Update site/content/docs/5.1/components/navbar.md

* Remove warning callout about ensuring correct search role in navbar doc

Co-authored-by: Gaël Poupard <ffoodd@users.noreply.github.com>
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-11-01 08:33:39 +02:00
Christian Oliff 8ed1218294 update grid example docs (#35308) 2021-11-01 08:29:31 +02:00
Gaël Poupard 989de20bae Don't override CSS direction in code elements (#35230)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-29 09:00:36 +03:00
Gaël Poupard 600a9ee521 Ensure sufficient contrast in accordion-item (#35231)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-29 08:53:42 +03:00
Gaël Poupard 7a9a3ab50f Drop prefixed version of ::file-selector-button (#35232)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-29 08:47:24 +03:00
Barabas 2e87f9aef0 images.md: remove zero-width space (#35234)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-28 14:22:32 +03:00
Oxydent dc06b9966e fix(offcanvas): Adding titles class where missing (#35264)
Co-authored-by: louismaximepiton <louismaxime.piton@orange.com>
2021-10-26 14:37:45 +03:00
XhmikosR 5771fcc4a9 Update devDependencies (#35233)
* @rollup/plugin-commonjs      ^21.0.0  →  ^21.0.1
* @rollup/plugin-node-resolve  ^13.0.5  →  ^13.0.6
* clean-css-cli                 ^5.4.1  →   ^5.4.2
* eslint                        ^8.0.0  →   ^8.0.1
* karma                         ^6.3.4  →   ^6.3.5
* nodemon                      ^2.0.13  →  ^2.0.14
* postcss                       ^8.3.9  →  ^8.3.11
* rtlcss                        ^3.3.0  →   ^3.4.0
* sass                         ^1.42.1  →  ^1.43.2
2021-10-21 16:40:29 +03:00
Gaël Poupard a0fd92ed2b Fix typo in RTL Hello World! sentence (#35236)
* docs(RTL): fix typo in Hello World! sentence

* chore(docs): Cspell arabic words update
2021-10-21 16:33:29 +03:00
GeoSot b991a6b851 tests: try to fix a few random failures (#35184)
* Change `Swipe` dispose spy on EventHandler
* Modal hide spy on backdrop hide
2021-10-13 17:45:39 +03:00
GeoSot 9640e2d5dd Change the way collapse handles its children on opening 2021-10-13 16:38:27 +03:00
GeoSot 1eea132866 collapse: extract duplicate code to a function 2021-10-13 16:38:27 +03:00
XhmikosR 99f6cf509d Update devDependencies (#35174)
* eslint                 ^7.32.0  →    ^8.0.0
* eslint-plugin-import   ^2.24.2  →   ^2.25.2
* eslint-plugin-unicorn  ^36.0.0  →   ^37.0.1
* vnu-jar                21.10.8  →  21.10.12
2021-10-13 15:56:37 +03:00
XhmikosR 45bba10714 Switch to Node.js 16 and npm 8. (#35178) 2021-10-13 15:26:48 +03:00
XhmikosR e8f702666f JS: minor refactoring (#35183)
* add missing comments
* shorten block comments
* reorder constants
* reorder public/private methods
* sort exports alphabetically in util/index.js
* fix a couple of typos
2021-10-13 15:19:28 +03:00
GeoSot db44392bda Swipe: add test to ensure that it ignores pinch events (#35161) 2021-10-12 15:48:19 +03:00
Jesse Mandel d7dec124eb Update nuget docs to specify only .NET Framework projects are supported. (#35124)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-11 19:31:53 +03:00
XhmikosR 1fe4acc270 Update devDependencies (#35155)
* @babel/core                  ^7.15.5  →  ^7.15.8
* @babel/preset-env            ^7.15.6  →  ^7.15.8
* cspell                       ^5.12.0  →  ^5.12.3
* linkinator                   ^2.14.3  →  ^2.14.4
* vnu-jar                       21.9.2  →  21.10.8
2021-10-11 19:07:43 +03:00
Geremia Taglialatela 5b124f647f Add color and border-color css variables to tables (#35055) 2021-10-11 17:41:43 +03:00
GeoSot 8ec6c94522 Extract Carousel's swipe functionality to a separate Class (#32999) 2021-10-11 17:04:43 +03:00
Mark Otto b21c7ccbb7 Reset z-index on .navbar-expand .offcanvas, plus prevent box-shadow issues (#35153) 2021-10-10 14:56:35 +03:00
XhmikosR 24e3ca2474 tooltip.js: ignore a LGTM error (#35147)
The code on this line is either sanitized or the user chose to not sanitize it.
2021-10-10 14:49:41 +03:00
XhmikosR c44d64ed71 Merge remote-tracking branch 'remotes/origin/v513' 2021-10-09 18:28:28 +03:00
XhmikosR ec62be2724 ESLint: disable no-negated-condition rule (#35137) 2021-10-08 12:47:55 +03:00
XhmikosR a260967a55 tests: minor cleanup (#35138)
* tests: minor cleanup

* tests: use the util noop function
2021-10-08 12:32:11 +03:00
XhmikosR eb0f705621 scrollspy.js: chain functions (#35139) 2021-10-08 12:28:05 +03:00
XhmikosR 64e13162fa Sanitizer: fix logic and add a test. (#35133)
This was broken in 2596c97 inadvertently.
Added a test so that we don't hit this in the future.
2021-10-07 17:48:36 +03:00
Chris Midgley 9ff87f5f0e docs: remove 'and' in middle of list (#35113) 2021-10-07 16:42:22 +03:00
devhoussam c331a150cd Add Sass variables for hr background-color and border
Co-Authored-By: Houssam Hammouda <11141564+devhoussam@users.noreply.github.com>
2021-10-05 15:49:35 -07:00
XhmikosR 2596c97034 util/sanitizer.js: use Array.every() (#35120) 2021-10-06 00:23:14 +03:00
GeoSot 598b4c59a6 Use for...of in visual tests too 2021-10-05 19:52:11 +03:00
XhmikosR 3afe4b8c7d Enable unicorn/numeric-separators-style rule.
This is taken care of by babel via @babel/plugin-proposal-numeric-separator
2021-10-05 19:52:11 +03:00
XhmikosR 567a41347e Fix a unicorn/no-array-callback-reference issue 2021-10-05 19:52:11 +03:00
XhmikosR 666fe596bf Enable unicorn/no-array-for-each rule 2021-10-05 19:52:11 +03:00
XhmikosR 2b4d0d166b Enable unicorn/no-for-loop rule 2021-10-05 19:52:11 +03:00
XhmikosR 9f1579aa04 Enable unicorn/prefer-prototype-methods rule 2021-10-05 19:52:11 +03:00
Mark Otto 57d80fcd32 Separate container classes from $enable-grid-classes option (#35005)
* Separate container classes from enable-grid-classes optoin

* Document the new option

* Mention in migration guide

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-05 19:46:33 +03:00
dependabot[bot] cf2f7cfbe5 Bump postcss from 8.3.8 to 8.3.9 (#35110)
Bumps [postcss](https://github.com/postcss/postcss) from 8.3.8 to 8.3.9.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.3.8...8.3.9)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-05 19:37:30 +03:00
dependabot[bot] 71e334e41a Bump autoprefixer from 10.3.6 to 10.3.7 (#35109)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.3.6 to 10.3.7.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.3.6...10.3.7)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2021-10-05 19:31:48 +03:00
dependabot[bot] 7bd4c9b79e Bump linkinator from 2.14.0 to 2.14.3 (#35119)
Bumps [linkinator](https://github.com/JustinBeckwith/linkinator) from 2.14.0 to 2.14.3.
- [Release notes](https://github.com/JustinBeckwith/linkinator/releases)
- [Commits](https://github.com/JustinBeckwith/linkinator/compare/v2.14.0...v2.14.3)

---
updated-dependencies:
- dependency-name: linkinator
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-05 19:26:27 +03:00
246 changed files with 24140 additions and 14261 deletions
+7 -7
View File
@@ -2,7 +2,7 @@
"files": [
{
"path": "./dist/css/bootstrap-grid.css",
"maxSize": "7.25 kB"
"maxSize": "7.5 kB"
},
{
"path": "./dist/css/bootstrap-grid.min.css",
@@ -14,7 +14,7 @@
},
{
"path": "./dist/css/bootstrap-reboot.min.css",
"maxSize": "2.35 kB"
"maxSize": "2.5 kB"
},
{
"path": "./dist/css/bootstrap-utilities.css",
@@ -22,15 +22,15 @@
},
{
"path": "./dist/css/bootstrap-utilities.min.css",
"maxSize": "6.85 kB"
"maxSize": "7 kB"
},
{
"path": "./dist/css/bootstrap.css",
"maxSize": "25.5 kB"
"maxSize": "26 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
"maxSize": "23.25 kB"
"maxSize": "24 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
@@ -46,7 +46,7 @@
},
{
"path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "18.25 kB"
"maxSize": "18.5 kB"
},
{
"path": "./dist/js/bootstrap.js",
@@ -54,7 +54,7 @@
},
{
"path": "./dist/js/bootstrap.min.js",
"maxSize": "16 kB"
"maxSize": "16.25 kB"
}
],
"ci": {
+15 -14
View File
@@ -11,6 +11,7 @@
"Blockquotes",
"Bootstrappers",
"borderless",
"Brotli",
"browserslist",
"browserslistrc",
"btncheck",
@@ -19,11 +20,11 @@
"callouts",
"clearfix",
"Codesniffer",
"colorspace",
"combinator",
"Contentful",
"Cpath",
"Crossfade",
"crossfading",
"cssgrid",
"Csvg",
"Datalists",
@@ -42,9 +43,7 @@
"fieldsets",
"flexbox",
"fullscreen",
"fusv",
"getbootstrap",
"globby",
"Grayscale",
"Hoverable",
"hreflang",
@@ -53,6 +52,7 @@
"Jumpstart",
"keyframes",
"libera",
"libman",
"Libsass",
"lightboxes",
"Lowercased",
@@ -68,6 +68,7 @@
"Noto",
"offcanvas",
"offcanvases",
"Packagist",
"popperjs",
"prebuild",
"precompiled",
@@ -84,12 +85,10 @@
"scrollbars",
"scrollspy",
"Segoe",
"sirv",
"srcset",
"stickied",
"Stylelint",
"subnav",
"svgo",
"tabbable",
"textareas",
"toggleable",
@@ -97,7 +96,6 @@
"touchend",
"twbs",
"unitless",
"Unported",
"unstylable",
"unstyled",
"Uppercased",
@@ -107,15 +105,18 @@
"vstack",
"walkthroughs",
"WCAG",
"webfont",
"Woohoo",
"zindex",
"بالعالم",
"مرحبا"
"zindex"
],
"language": "en-US",
"files": [
"**/*.md"
],
"language": "en,en-US",
"ignorePaths": [
".cspell.json",
".min."
]
"dist/",
"*.min.*",
"**/*rtl*",
"**/tests/**"
],
"useGitignore": true
}
+1 -4
View File
@@ -37,6 +37,7 @@
}
],
"no-console": "error",
"no-negated-condition": "off",
"object-curly-spacing": [
"error",
"always"
@@ -51,16 +52,12 @@
],
"unicorn/explicit-length-check": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-method-this-argument": "off",
"unicorn/no-for-loop": "off",
"unicorn/no-null": "off",
"unicorn/no-unused-properties": "error",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-array-flat": "off",
"unicorn/prefer-dom-node-dataset": "off",
"unicorn/prefer-module": "off",
"unicorn/prefer-prototype-methods": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-spread": "off",
"unicorn/prevent-abbreviations": "off"
+2 -1
View File
@@ -2,10 +2,11 @@ name: BrowserStack
on:
push:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
browserstack:
+2 -1
View File
@@ -5,10 +5,11 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
bundlewatch:
+1
View File
@@ -14,6 +14,7 @@ on:
- "!dependabot/**"
schedule:
- cron: "0 2 * * 5"
workflow_dispatch:
jobs:
analyze:
+28
View File
@@ -0,0 +1,28 @@
name: cspell
on:
push:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 16
jobs:
cspell:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Run cspell
uses: streetsidesoftware/cspell-action@v1
with:
config: ".cspell.json"
files: "**/*.md"
inline: error
incremental_files_only: false
+2 -1
View File
@@ -5,10 +5,11 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
css:
+15 -3
View File
@@ -5,10 +5,11 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
docs:
@@ -29,5 +30,16 @@ jobs:
- name: Install npm dependencies
run: npm ci
- name: Test docs
run: npm run docs
- name: Build docs
run: npm run docs-build
- name: Validate HTML
run: npm run docs-vnu
- name: Run linkinator
uses: JustinBeckwith/linkinator-action@v1
with:
paths: _site
recurse: true
verbosity: error
skip: "^(?!http://localhost)"
+19
View File
@@ -0,0 +1,19 @@
name: Close Issue Awaiting Reply
on:
schedule:
- cron: "0 0 * * *"
jobs:
issue-close-require:
runs-on: ubuntu-latest
if: github.repository == 'twbs/bootstrap'
steps:
- name: awaiting reply
uses: actions-cool/issues-helper@v3
with:
actions: "close-issues"
labels: "awaiting-reply"
inactive-day: 14
body: |
As the issue was labeled with `awaiting-reply`, but there has been no response in 14 days, this issue will be closed. If you have any questions, you can comment/reply.
+19
View File
@@ -0,0 +1,19 @@
name: Issue Labeled
on:
issues:
types: [labeled]
jobs:
issue-labeled:
if: github.repository == 'twbs/bootstrap'
runs-on: ubuntu-latest
steps:
- name: awaiting reply
if: github.event.label.name == 'awaiting-reply'
uses: actions-cool/issues-helper@v3
with:
actions: "create-comment"
token: ${{ secrets.GITHUB_TOKEN }}
body: |
Hello @${{ github.event.issue.user.login }}. Bug reports must include a **live demo** of the issue. Per our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md), please create a reduced test case on [CodePen](https://codepen.io/) or [JS Bin](https://jsbin.com/) and report back with your link, Bootstrap version, and specific browser and Operating System details.
+4 -8
View File
@@ -5,20 +5,17 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 16
jobs:
run:
name: Node ${{ matrix.node }}
name: JS Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [12, 14, 16]
steps:
- name: Clone repository
uses: actions/checkout@v2
@@ -26,7 +23,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
node-version: ${{ env.NODE }}
cache: npm
- name: Install npm dependencies
@@ -40,7 +37,6 @@ jobs:
- name: Run Coveralls
uses: coverallsapp/github-action@1.1.3
if: matrix.node == 14
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
path-to-lcov: "./js/coverage/lcov.info"
+2 -1
View File
@@ -5,10 +5,11 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
lint:
+2 -1
View File
@@ -5,10 +5,11 @@ on:
branches-ignore:
- "dependabot/**"
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 14
NODE: 16
jobs:
css:
+1
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
workflow_dispatch:
jobs:
update_release_draft:
+2 -1
View File
@@ -1,7 +1,8 @@
# Ignore docs files
/_site/
# Hugo resources folder
# Hugo files
/resources/
/.hugo_build.lock
# Numerous always-ignore extensions
*.diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": [
"stylelint-config-twbs-bootstrap/scss"
"stylelint-config-twbs-bootstrap"
],
"rules": {
"declaration-property-value-disallowed-list": {
+2 -2
View File
@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Copyright (c) 2011-2022 Twitter, Inc.
Copyright (c) 2011-2022 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+64 -56
View File
@@ -53,7 +53,7 @@ Several quick start options are available:
- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.1.3`
- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass`
Read the [Getting started page](https://getbootstrap.com/docs/5.1/getting-started/introduction/) for information on the framework contents, templates and examples, and more.
Read the [Getting started page](https://getbootstrap.com/docs/5.1/getting-started/introduction/) for information on the framework contents, templates, examples, and more.
## Status
@@ -65,8 +65,6 @@ Read the [Getting started page](https://getbootstrap.com/docs/5.1/getting-starte
[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap)
[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap)
[![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest)
[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer)
[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev)
[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main)](https://coveralls.io/github/twbs/bootstrap?branch=main)
[![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
[![CSS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=brotli&label=CSS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
@@ -79,57 +77,61 @@ Read the [Getting started page](https://getbootstrap.com/docs/5.1/getting-starte
## What's included
Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this:
Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations.
```text
bootstrap/
├── css/
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
│ ├── bootstrap.rtl.min.css
── bootstrap.rtl.min.css.map
└── js/
├── bootstrap.bundle.js
── bootstrap.bundle.js.map
├── bootstrap.bundle.min.js
├── bootstrap.bundle.min.js.map
├── bootstrap.esm.js
├── bootstrap.esm.js.map
├── bootstrap.esm.min.js
├── bootstrap.esm.min.js.map
├── bootstrap.js
├── bootstrap.js.map
├── bootstrap.min.js
── bootstrap.min.js.map
```
<details>
<summary>Download contents</summary>
```text
bootstrap/
├── css/
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
├── bootstrap.rtl.min.css
── bootstrap.rtl.min.css.map
└── js/
├── bootstrap.bundle.js
├── bootstrap.bundle.js.map
├── bootstrap.bundle.min.js
├── bootstrap.bundle.min.js.map
├── bootstrap.esm.js
├── bootstrap.esm.js.map
├── bootstrap.esm.min.js
├── bootstrap.esm.min.js.map
── bootstrap.js
├── bootstrap.js.map
├── bootstrap.min.js
└── bootstrap.min.js.map
```
</details>
We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [Source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/).
@@ -186,7 +188,7 @@ Get updates on Bootstrap's development and chat with the project maintainers and
For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible.
See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release.
See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. [Release planning](https://github.com/twbs/release#upcoming-release-schedule) takes place in a separate project.
## Creators
@@ -205,11 +207,17 @@ See [the Releases section of our GitHub project](https://github.com/twbs/bootstr
## Thanks
<a href="https://www.browserstack.com/">
<img src="https://live.browserstack.com/images/opensource/browserstack-logo.svg" alt="BrowserStack Logo" width="192" height="42">
<img src="https://live.browserstack.com/images/opensource/browserstack-logo.svg" alt="BrowserStack" width="192" height="42">
</a>
Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers!
<a href="https://www.netlify.com/">
<img src="https://www.netlify.com/v3/img/components/full-logo-light.svg" alt="Netlify" width="147" height="40">
</a>
Thanks to [Netlify](https://www.netlify.com/) for providing us with Deploy Previews!
## Sponsors
@@ -236,4 +244,4 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com
## Copyright and license
Code and documentation copyright 20112021 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/).
Code and documentation copyright 20112022 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/).
+68 -147
View File
@@ -2,8 +2,8 @@
/*!
* Script to build our plugins to use them separately.
* Copyright 2020-2021 The Bootstrap Authors
* Copyright 2020-2021 Twitter, Inc.
* Copyright 2020-2022 The Bootstrap Authors
* Copyright 2020-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
@@ -11,173 +11,94 @@
const path = require('path')
const rollup = require('rollup')
const globby = require('globby')
const { babel } = require('@rollup/plugin-babel')
const banner = require('./banner.js')
const rootPath = path.resolve(__dirname, '../js/dist/')
const plugins = [
babel({
// Only transpile our source code
exclude: 'node_modules/**',
// Include the helpers in each file, at most one copy of each
babelHelpers: 'bundled'
const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/')
const jsFiles = globby.sync(sourcePath + '/**/*.js')
// Array which holds the resolved plugins
const resolvedPlugins = []
// Trims the "js" extension and uppercases => first letter, hyphens, backslashes & slashes
const filenameToEntity = filename => filename.replace('.js', '')
.replace(/(?:^|-|\/|\\)[a-z]/g, str => str.slice(-1).toUpperCase())
for (const file of jsFiles) {
resolvedPlugins.push({
src: file.replace('.js', ''),
dist: file.replace('src', 'dist'),
fileName: path.basename(file),
className: filenameToEntity(path.basename(file))
// safeClassName: filenameToEntity(path.relative(sourcePath, file))
})
]
const bsPlugins = {
Data: path.resolve(__dirname, '../js/src/dom/data.js'),
EventHandler: path.resolve(__dirname, '../js/src/dom/event-handler.js'),
Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'),
SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'),
Alert: path.resolve(__dirname, '../js/src/alert.js'),
Base: path.resolve(__dirname, '../js/src/base-component.js'),
Button: path.resolve(__dirname, '../js/src/button.js'),
Carousel: path.resolve(__dirname, '../js/src/carousel.js'),
Collapse: path.resolve(__dirname, '../js/src/collapse.js'),
Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'),
Modal: path.resolve(__dirname, '../js/src/modal.js'),
Offcanvas: path.resolve(__dirname, '../js/src/offcanvas.js'),
Popover: path.resolve(__dirname, '../js/src/popover.js'),
ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
Tab: path.resolve(__dirname, '../js/src/tab.js'),
Toast: path.resolve(__dirname, '../js/src/toast.js'),
Tooltip: path.resolve(__dirname, '../js/src/tooltip.js')
}
const defaultPluginConfig = {
external: [
bsPlugins.Data,
bsPlugins.Base,
bsPlugins.EventHandler,
bsPlugins.SelectorEngine
],
globals: {
[bsPlugins.Data]: 'Data',
[bsPlugins.Base]: 'Base',
[bsPlugins.EventHandler]: 'EventHandler',
[bsPlugins.SelectorEngine]: 'SelectorEngine'
}
}
const getConfigByPluginKey = pluginKey => {
switch (pluginKey) {
case 'Alert':
case 'Offcanvas':
case 'Tab':
return defaultPluginConfig
case 'Base':
case 'Button':
case 'Carousel':
case 'Collapse':
case 'Modal':
case 'ScrollSpy': {
const config = Object.assign(defaultPluginConfig)
config.external.push(bsPlugins.Manipulator)
config.globals[bsPlugins.Manipulator] = 'Manipulator'
return config
}
case 'Dropdown':
case 'Tooltip': {
const config = Object.assign(defaultPluginConfig)
config.external.push(bsPlugins.Manipulator, '@popperjs/core')
config.globals[bsPlugins.Manipulator] = 'Manipulator'
config.globals['@popperjs/core'] = 'Popper'
return config
}
case 'Popover':
return {
external: [
bsPlugins.Data,
bsPlugins.SelectorEngine,
bsPlugins.Tooltip
],
globals: {
[bsPlugins.Data]: 'Data',
[bsPlugins.SelectorEngine]: 'SelectorEngine',
[bsPlugins.Tooltip]: 'Tooltip'
}
}
case 'Toast':
return {
external: [
bsPlugins.Data,
bsPlugins.Base,
bsPlugins.EventHandler,
bsPlugins.Manipulator
],
globals: {
[bsPlugins.Data]: 'Data',
[bsPlugins.Base]: 'Base',
[bsPlugins.EventHandler]: 'EventHandler',
[bsPlugins.Manipulator]: 'Manipulator'
}
}
default:
return {
external: []
}
}
}
const utilObjects = new Set([
'Util',
'Sanitizer',
'Backdrop'
])
const domObjects = new Set([
'Data',
'EventHandler',
'Manipulator',
'SelectorEngine'
])
const build = async plugin => {
console.log(`Building ${plugin} plugin...`)
const { external, globals } = getConfigByPluginKey(plugin)
const pluginFilename = path.basename(bsPlugins[plugin])
let pluginPath = rootPath
if (utilObjects.has(plugin)) {
pluginPath = `${rootPath}/util/`
}
if (domObjects.has(plugin)) {
pluginPath = `${rootPath}/dom/`
}
const globals = {}
const bundle = await rollup.rollup({
input: bsPlugins[plugin],
plugins,
external
input: plugin.src,
plugins: [
babel({
// Only transpile our source code
exclude: 'node_modules/**',
// Include the helpers in each file, at most one copy of each
babelHelpers: 'bundled'
})
],
external(source) {
// Pattern to identify local files
const pattern = /^(\.{1,2})\//
// It's not a local file, e.g a Node.js package
if (!pattern.test(source)) {
globals[source] = source
return true
}
const usedPlugin = resolvedPlugins.find(plugin => {
return plugin.src.includes(source.replace(pattern, ''))
})
if (!usedPlugin) {
throw new Error(`Source ${source} is not mapped!`)
}
// We can change `Index` with `UtilIndex` etc if we use
// `safeClassName` instead of `className` everywhere
globals[path.normalize(usedPlugin.src)] = usedPlugin.className
return true
}
})
await bundle.write({
banner: banner(pluginFilename),
banner: banner(plugin.fileName),
format: 'umd',
name: plugin,
name: plugin.className,
sourcemap: true,
globals,
generatedCode: 'es2015',
file: path.resolve(__dirname, `${pluginPath}/${pluginFilename}`)
file: plugin.dist
})
console.log(`Building ${plugin} plugin... Done!`)
console.log(`Built ${plugin.className}`)
}
const main = async () => {
(async () => {
try {
await Promise.all(Object.keys(bsPlugins).map(plugin => build(plugin)))
const basename = path.basename(__filename)
const timeLabel = `[${basename}] finished`
console.log('Building individual plugins...')
console.time(timeLabel)
await Promise.all(Object.values(resolvedPlugins).map(plugin => build(plugin)))
console.timeEnd(timeLabel)
} catch (error) {
console.error(error)
process.exit(1)
}
}
main()
})()
+4 -4
View File
@@ -2,8 +2,8 @@
/*!
* Script to update version number references in the project.
* Copyright 2017-2021 The Bootstrap Authors
* Copyright 2017-2021 Twitter, Inc.
* Copyright 2017-2022 The Bootstrap Authors
* Copyright 2017-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
@@ -57,7 +57,7 @@ async function replaceRecursively(file, oldVersion, newVersion) {
}
async function main(args) {
const [oldVersion, newVersion] = args
let [oldVersion, newVersion] = args
if (!oldVersion || !newVersion) {
console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]')
@@ -66,7 +66,7 @@ async function main(args) {
}
// Strip any leading `v` from arguments because otherwise we will end up with duplicate `v`s
[oldVersion, newVersion].map(arg => arg.startsWith('v') ? arg.slice(1) : arg)
[oldVersion, newVersion] = [oldVersion, newVersion].map(arg => arg.startsWith('v') ? arg.slice(1) : arg)
try {
const files = await globby(GLOB, GLOBBY_OPTIONS)
+7 -7
View File
@@ -5,8 +5,8 @@
* Remember to use the same vendor files as the CDN ones,
* otherwise the hashes won't match!
*
* Copyright 2017-2021 The Bootstrap Authors
* Copyright 2017-2021 Twitter, Inc.
* Copyright 2017-2022 The Bootstrap Authors
* Copyright 2017-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
@@ -47,10 +47,10 @@ const files = [
}
]
files.forEach(file => {
fs.readFile(file.file, 'utf8', (err, data) => {
if (err) {
throw err
for (const file of files) {
fs.readFile(file.file, 'utf8', (error, data) => {
if (error) {
throw error
}
const algo = 'sha384'
@@ -61,4 +61,4 @@ files.forEach(file => {
sh.sed('-i', new RegExp(`^(\\s+${file.configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile)
})
})
}
+9 -9
View File
@@ -1,19 +1,19 @@
'use strict'
module.exports = ctx => {
const mapConfig = {
inline: false,
annotation: true,
sourcesContent: true
}
module.exports = context => {
return {
map: ctx.file.dirname.includes('examples') ?
false :
{
inline: false,
annotation: true,
sourcesContent: true
},
map: context.file.dirname.includes('examples') ? false : mapConfig,
plugins: {
autoprefixer: {
cascade: false
},
rtlcss: ctx.env === 'RTL' ? {} : false
rtlcss: context.env === 'RTL'
}
}
}
+3 -3
View File
@@ -9,7 +9,7 @@ const banner = require('./banner.js')
const BUNDLE = process.env.BUNDLE === 'true'
const ESM = process.env.ESM === 'true'
let fileDest = `bootstrap${ESM ? '.esm' : ''}`
let fileDestination = `bootstrap${ESM ? '.esm' : ''}`
const external = ['@popperjs/core']
const plugins = [
babel({
@@ -24,7 +24,7 @@ const globals = {
}
if (BUNDLE) {
fileDest += '.bundle'
fileDestination += '.bundle'
// Remove last entry in external array to bundle Popper
external.pop()
delete globals['@popperjs/core']
@@ -41,7 +41,7 @@ const rollupConfig = {
input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`),
output: {
banner,
file: path.resolve(__dirname, `../dist/js/${fileDest}.js`),
file: path.resolve(__dirname, `../dist/js/${fileDestination}.js`),
format: ESM ? 'esm' : 'umd',
globals,
generatedCode: 'es2015'
-59
View File
@@ -1,59 +0,0 @@
# Usage:
# install svgo globally: `npm i -g svgo`
# svgo --config=build/svgo.yml --input=foo.svg
# https://github.com/svg/svgo/blob/master/docs/how-it-works/en.md
# replace default config
multipass: true
#full: true
# https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options
js2svg:
pretty: true
indent: 2
plugins:
# - addAttributesToSVGElement:
# attributes:
# - focusable: false
- cleanupAttrs: true
- cleanupEnableBackground: true
- cleanupIDs: true
- cleanupListOfValues: true
- cleanupNumericValues: true
- collapseGroups: true
- convertColors: true
- convertPathData: true
- convertShapeToPath: true
- convertStyleToAttrs: true
- convertTransform: true
- inlineStyles: true
- mergePaths: true
- minifyStyles: true
- moveElemsAttrsToGroup: true
- moveGroupAttrsToElems: true
- removeAttrs:
attrs:
- "data-name"
- removeComments: true
- removeDesc: true
- removeDoctype: true
- removeEditorsNSData: true
- removeEmptyAttrs: true
- removeEmptyContainers: true
- removeEmptyText: true
- removeHiddenElems: true
- removeMetadata: true
- removeNonInheritableGroupAttrs: true
- removeTitle: false
- removeUnknownsAndDefaults:
keepRoleAttr: true
- removeUnusedNS: true
- removeUselessDefs: true
- removeUselessStrokeAndFill: true
- removeViewBox: false
- removeXMLNS: false
- removeXMLProcInst: true
- sortAttrs: true
+2 -2
View File
@@ -2,8 +2,8 @@
/*!
* Script to run vnu-jar if Java is available.
* Copyright 2017-2021 The Bootstrap Authors
* Copyright 2017-2021 Twitter, Inc.
* Copyright 2017-2022 The Bootstrap Authors
* Copyright 2017-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
+9 -9
View File
@@ -3,7 +3,7 @@
/*!
* Script to create the built examples zip archive;
* requires the `zip` command to be present!
* Copyright 2020-2021 The Bootstrap Authors
* Copyright 2020-2022 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
@@ -57,22 +57,22 @@ sh.mkdir('-p', [
sh.cp('-Rf', `${docsDir}/examples/*`, distFolder)
cssFiles.forEach(file => {
for (const file of cssFiles) {
sh.cp('-f', `${docsDir}/dist/css/${file}`, `${distFolder}/assets/dist/css/`)
})
}
jsFiles.forEach(file => {
for (const file of jsFiles) {
sh.cp('-f', `${docsDir}/dist/js/${file}`, `${distFolder}/assets/dist/js/`)
})
}
imgFiles.forEach(file => {
for (const file of imgFiles) {
sh.cp('-f', `${docsDir}/assets/brand/${file}`, `${distFolder}/assets/brand/`)
})
}
sh.rm(`${distFolder}/index.html`)
// get all examples' HTML files
sh.find(`${distFolder}/**/*.html`).forEach(file => {
for (const file of sh.find(`${distFolder}/**/*.html`)) {
const fileContents = sh.cat(file)
.toString()
.replace(new RegExp(`"/docs/${versionShort}/`, 'g'), '"../')
@@ -81,7 +81,7 @@ sh.find(`${distFolder}/**/*.html`).forEach(file => {
.replace(/(<script src="\.\.\/.*) integrity=".*>/g, '$1></script>')
.replace(/( +)<!-- favicons(.|\n)+<style>/i, ' <style>')
new sh.ShellString(fileContents).to(file)
})
}
// create the zip file
sh.exec(`zip -r9 "${distFolder}.zip" "${distFolder}"`)
+13 -3
View File
@@ -1,7 +1,13 @@
languageCode: "en"
title: "Bootstrap"
baseURL: "https://getbootstrap.com"
enableInlineShortcodes: true
security:
enableInlineShortcodes: true
funcs:
getenv:
- ^HUGO_
- NETLIFY
markup:
goldmark:
@@ -75,5 +81,9 @@ params:
js_hash: "sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
js_bundle: "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
js_bundle_hash: "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
popper_hash: "sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.2/dist/umd/popper.min.js"
popper_hash: "sha384-q9CRHqZndzlxGLOj+xrdLDJa9ittGte1NksRmgJKeCV9DrM7Kz868XYqsKWPpAmn"
anchors:
min: 2
max: 5
+12 -27
View File
@@ -5,30 +5,15 @@
* --------------------------------------------------------------------------
*/
import Alert from './src/alert'
import Button from './src/button'
import Carousel from './src/carousel'
import Collapse from './src/collapse'
import Dropdown from './src/dropdown'
import Modal from './src/modal'
import Offcanvas from './src/offcanvas'
import Popover from './src/popover'
import ScrollSpy from './src/scrollspy'
import Tab from './src/tab'
import Toast from './src/toast'
import Tooltip from './src/tooltip'
export {
Alert,
Button,
Carousel,
Collapse,
Dropdown,
Modal,
Offcanvas,
Popover,
ScrollSpy,
Tab,
Toast,
Tooltip
}
export { default as Alert } from './src/alert'
export { default as Button } from './src/button'
export { default as Carousel } from './src/carousel'
export { default as Collapse } from './src/collapse'
export { default as Dropdown } from './src/dropdown'
export { default as Modal } from './src/modal'
export { default as Offcanvas } from './src/offcanvas'
export { default as Popover } from './src/popover'
export { default as ScrollSpy } from './src/scrollspy'
export { default as Tab } from './src/tab'
export { default as Toast } from './src/toast'
export { default as Tooltip } from './src/tooltip'
+2 -14
View File
@@ -11,9 +11,7 @@ import BaseComponent from './base-component'
import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'alert'
@@ -26,20 +24,16 @@ const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Alert extends BaseComponent {
// Getters
static get NAME() {
return NAME
}
// Public
close() {
const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)
@@ -61,7 +55,6 @@ class Alert extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Alert.getOrCreateInstance(this)
@@ -80,18 +73,13 @@ class Alert extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
enableDismissTrigger(Alert, 'close')
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Alert to jQuery only if jQuery is present
*/
defineJQueryPlugin(Alert)
+26 -16
View File
@@ -6,47 +6,57 @@
*/
import Data from './dom/data'
import {
executeAfterTransition,
getElement
} from './util/index'
import { executeAfterTransition, getElement } from './util/index'
import EventHandler from './dom/event-handler'
import Config from './util/config'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const VERSION = '5.1.3'
class BaseComponent {
constructor(element) {
element = getElement(element)
/**
* Class definition
*/
class BaseComponent extends Config {
constructor(element, config) {
super()
element = getElement(element)
if (!element) {
return
}
this._element = element
this._config = this._getConfig(config)
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)
Object.getOwnPropertyNames(this).forEach(propertyName => {
for (const propertyName of Object.getOwnPropertyNames(this)) {
this[propertyName] = null
})
}
}
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated)
}
/** Static */
_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)
}
@@ -59,10 +69,6 @@ class BaseComponent {
return VERSION
}
static get NAME() {
throw new Error('You have to implement the static method "NAME", for each component!')
}
static get DATA_KEY() {
return `bs.${this.NAME}`
}
@@ -70,6 +76,10 @@ class BaseComponent {
static get EVENT_KEY() {
return `.${this.DATA_KEY}`
}
static eventName(name) {
return `${name}${this.EVENT_KEY}`
}
}
export default BaseComponent
+2 -16
View File
@@ -10,9 +10,7 @@ import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'button'
@@ -21,33 +19,26 @@ const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const CLASS_NAME_ACTIVE = 'active'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]'
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Button extends BaseComponent {
// Getters
static get NAME() {
return NAME
}
// 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))
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Button.getOrCreateInstance(this)
@@ -60,9 +51,7 @@ class Button extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
@@ -75,10 +64,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Button to jQuery only if jQuery is present
*/
defineJQueryPlugin(Button)
+138 -215
View File
@@ -8,22 +8,20 @@
import {
defineJQueryPlugin,
getElementFromSelector,
getNextActiveElement,
isRTL,
isVisible,
getNextActiveElement,
reflow,
triggerTransitionEnd,
typeCheckConfig
triggerTransitionEnd
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import Swipe from './util/swipe'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'carousel'
@@ -34,7 +32,42 @@ const DATA_API_KEY = '.data-api'
const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
const SWIPE_THRESHOLD = 40
const ORDER_NEXT = 'next'
const ORDER_PREV = 'prev'
const DIRECTION_LEFT = 'left'
const DIRECTION_RIGHT = 'right'
const EVENT_SLIDE = `slide${EVENT_KEY}`
const EVENT_SLID = `slid${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_CAROUSEL = 'carousel'
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_SLIDE = 'slide'
const CLASS_NAME_END = 'carousel-item-end'
const CLASS_NAME_START = 'carousel-item-start'
const CLASS_NAME_NEXT = 'carousel-item-next'
const CLASS_NAME_PREV = 'carousel-item-prev'
const SELECTOR_ACTIVE = '.active'
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
const SELECTOR_ITEM = '.carousel-item'
const SELECTOR_ITEM_IMG = '.carousel-item img'
const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'
const SELECTOR_INDICATORS = '.carousel-indicators'
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
const KEY_TO_DIRECTION = {
[ARROW_LEFT_KEY]: DIRECTION_RIGHT,
[ARROW_RIGHT_KEY]: DIRECTION_LEFT
}
const Default = {
interval: 5000,
@@ -54,60 +87,13 @@ const DefaultType = {
touch: 'boolean'
}
const ORDER_NEXT = 'next'
const ORDER_PREV = 'prev'
const DIRECTION_LEFT = 'left'
const DIRECTION_RIGHT = 'right'
const KEY_TO_DIRECTION = {
[ARROW_LEFT_KEY]: DIRECTION_RIGHT,
[ARROW_RIGHT_KEY]: DIRECTION_LEFT
}
const EVENT_SLIDE = `slide${EVENT_KEY}`
const EVENT_SLID = `slid${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_CAROUSEL = 'carousel'
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_SLIDE = 'slide'
const CLASS_NAME_END = 'carousel-item-end'
const CLASS_NAME_START = 'carousel-item-start'
const CLASS_NAME_NEXT = 'carousel-item-next'
const CLASS_NAME_PREV = 'carousel-item-prev'
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
const SELECTOR_ACTIVE = '.active'
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
const SELECTOR_ITEM = '.carousel-item'
const SELECTOR_ITEM_IMG = '.carousel-item img'
const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'
const SELECTOR_INDICATORS = '.carousel-indicators'
const SELECTOR_INDICATOR = '[data-bs-target]'
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
const POINTER_TYPE_TOUCH = 'touch'
const POINTER_TYPE_PEN = 'pen'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Carousel extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._items = null
this._interval = null
@@ -115,34 +101,32 @@ class Carousel extends BaseComponent {
this._isPaused = false
this._isSliding = false
this.touchTimeout = null
this.touchStartX = 0
this.touchDeltaX = 0
this._swipeHelper = null
this._config = this._getConfig(config)
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
this._pointerEvent = Boolean(window.PointerEvent)
this._addEventListeners()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
next() {
this._slide(ORDER_NEXT)
}
nextWhenVisible() {
// FIXME TODO use `document.visibilityState`
// Don't call next when the page isn't visible
// or the carousel or its parent isn't visible
if (!document.hidden && isVisible(this._element)) {
@@ -164,8 +148,7 @@ class Carousel extends BaseComponent {
this.cycle(true)
}
clearInterval(this._interval)
this._interval = null
this._clearInterval()
}
cycle(event) {
@@ -173,23 +156,16 @@ class Carousel extends BaseComponent {
this._isPaused = false
}
if (this._interval) {
clearInterval(this._interval)
this._interval = null
}
if (this._config && this._config.interval && !this._isPaused) {
this._clearInterval()
if (this._config.interval && !this._isPaused) {
this._updateInterval()
this._interval = setInterval(
(document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
this._config.interval
)
this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)
}
}
to(index) {
this._activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
this._activeElement = this._getActive()
const activeIndex = this._getItemIndex(this._activeElement)
if (index > this._items.length - 1 || index < 0) {
@@ -214,34 +190,18 @@ class Carousel extends BaseComponent {
this._slide(order, this._items[index])
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
dispose() {
if (this._swipeHelper) {
this._swipeHelper.dispose()
}
typeCheckConfig(NAME, config, DefaultType)
return config
super.dispose()
}
_handleSwipe() {
const absDeltax = Math.abs(this.touchDeltaX)
if (absDeltax <= SWIPE_THRESHOLD) {
return
}
const direction = absDeltax / this.touchDeltaX
this.touchDeltaX = 0
if (!direction) {
return
}
this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT)
// Private
_configAfterMerge(config) {
config.defaultInterval = config.interval
return config
}
_addEventListeners() {
@@ -254,70 +214,44 @@ class Carousel extends BaseComponent {
EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event))
}
if (this._config.touch && this._touchSupported) {
if (this._config.touch && Swipe.isSupported()) {
this._addTouchEventListeners()
}
}
_addTouchEventListeners() {
const hasPointerPenTouch = event => {
return this._pointerEvent &&
(event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())
}
const start = event => {
if (hasPointerPenTouch(event)) {
this.touchStartX = event.clientX
} else if (!this._pointerEvent) {
this.touchStartX = event.touches[0].clientX
}
}
const move = event => {
// ensure swiping with one touch and not pinching
this.touchDeltaX = event.touches && event.touches.length > 1 ?
0 :
event.touches[0].clientX - this.touchStartX
}
const end = event => {
if (hasPointerPenTouch(event)) {
this.touchDeltaX = event.clientX - this.touchStartX
const endCallBack = () => {
if (this._config.pause !== 'hover') {
return
}
this._handleSwipe()
if (this._config.pause === 'hover') {
// If it's a touch-enabled device, mouseenter/leave are fired as
// part of the mouse compatibility events on first tap - the carousel
// would stop cycling until user tapped out of it;
// here, we listen for touchend, explicitly pause the carousel
// (as if it's the second time we tap on it, mouseenter compat event
// is NOT fired) and after a timeout (to allow for mouse compatibility
// events to fire) we explicitly restart cycling
// If it's a touch-enabled device, mouseenter/leave are fired as
// part of the mouse compatibility events on first tap - the carousel
// would stop cycling until user tapped out of it;
// here, we listen for touchend, explicitly pause the carousel
// (as if it's the second time we tap on it, mouseenter compat event
// is NOT fired) and after a timeout (to allow for mouse compatibility
// events to fire) we explicitly restart cycling
this.pause()
if (this.touchTimeout) {
clearTimeout(this.touchTimeout)
}
this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
this.pause()
if (this.touchTimeout) {
clearTimeout(this.touchTimeout)
}
this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
}
SelectorEngine.find(SELECTOR_ITEM_IMG, this._element).forEach(itemImg => {
EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
})
if (this._pointerEvent) {
EventHandler.on(this._element, EVENT_POINTERDOWN, event => start(event))
EventHandler.on(this._element, EVENT_POINTERUP, event => end(event))
this._element.classList.add(CLASS_NAME_POINTER_EVENT)
} else {
EventHandler.on(this._element, EVENT_TOUCHSTART, event => start(event))
EventHandler.on(this._element, EVENT_TOUCHMOVE, event => move(event))
EventHandler.on(this._element, EVENT_TOUCHEND, event => end(event))
const swipeConfig = {
leftCallback: () => this._slide(DIRECTION_LEFT),
rightCallback: () => this._slide(DIRECTION_RIGHT),
endCallback: endCallBack
}
this._swipeHelper = new Swipe(this._element, swipeConfig)
}
_keydown(event) {
@@ -345,9 +279,8 @@ class Carousel extends BaseComponent {
return getNextActiveElement(this._items, activeElement, isNext, this._config.wrap)
}
_triggerSlideEvent(relatedTarget, eventDirectionName) {
_triggerSlideEvent(relatedTarget, fromIndex, eventDirectionName) {
const targetIndex = this._getItemIndex(relatedTarget)
const fromIndex = this._getItemIndex(SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element))
return EventHandler.trigger(this._element, EVENT_SLIDE, {
relatedTarget,
@@ -357,27 +290,26 @@ class Carousel extends BaseComponent {
})
}
_setActiveIndicatorElement(element) {
if (this._indicatorsElement) {
const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
_setActiveIndicatorElement(index) {
if (!this._indicatorsElement) {
return
}
activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
activeIndicator.removeAttribute('aria-current')
const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
const indicators = SelectorEngine.find(SELECTOR_INDICATOR, this._indicatorsElement)
activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
activeIndicator.removeAttribute('aria-current')
for (let i = 0; i < indicators.length; i++) {
if (Number.parseInt(indicators[i].getAttribute('data-bs-slide-to'), 10) === this._getItemIndex(element)) {
indicators[i].classList.add(CLASS_NAME_ACTIVE)
indicators[i].setAttribute('aria-current', 'true')
break
}
}
const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement)
if (newActiveIndicator) {
newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)
newActiveIndicator.setAttribute('aria-current', 'true')
}
}
_updateInterval() {
const element = this._activeElement || SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
const element = this._activeElement || this._getActive()
if (!element) {
return
@@ -385,17 +317,12 @@ class Carousel extends BaseComponent {
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)
if (elementInterval) {
this._config.defaultInterval = this._config.defaultInterval || this._config.interval
this._config.interval = elementInterval
} else {
this._config.interval = this._config.defaultInterval || this._config.interval
}
this._config.interval = elementInterval || this._config.defaultInterval
}
_slide(directionOrOrder, element) {
const order = this._directionToOrder(directionOrOrder)
const activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
const activeElement = this._getActive()
const activeElementIndex = this._getItemIndex(activeElement)
const nextElement = element || this._getItemByOrder(order, activeElement)
@@ -416,7 +343,7 @@ class Carousel extends BaseComponent {
return
}
const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
const slideEvent = this._triggerSlideEvent(nextElement, activeElementIndex, eventDirectionName)
if (slideEvent.defaultPrevented) {
return
}
@@ -432,10 +359,24 @@ class Carousel extends BaseComponent {
this.pause()
}
this._setActiveIndicatorElement(nextElement)
this._setActiveIndicatorElement(nextElementIndex)
this._activeElement = nextElement
const triggerSlidEvent = () => {
nextElement.classList.add(orderClassName)
reflow(nextElement)
activeElement.classList.add(directionalClassName)
nextElement.classList.add(directionalClassName)
const completeCallBack = () => {
nextElement.classList.remove(directionalClassName, orderClassName)
nextElement.classList.add(CLASS_NAME_ACTIVE)
activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)
this._isSliding = false
EventHandler.trigger(this._element, EVENT_SLID, {
relatedTarget: nextElement,
direction: eventDirectionName,
@@ -444,39 +385,28 @@ class Carousel extends BaseComponent {
})
}
if (this._element.classList.contains(CLASS_NAME_SLIDE)) {
nextElement.classList.add(orderClassName)
reflow(nextElement)
activeElement.classList.add(directionalClassName)
nextElement.classList.add(directionalClassName)
const completeCallBack = () => {
nextElement.classList.remove(directionalClassName, orderClassName)
nextElement.classList.add(CLASS_NAME_ACTIVE)
activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)
this._isSliding = false
setTimeout(triggerSlidEvent, 0)
}
this._queueCallback(completeCallBack, activeElement, true)
} else {
activeElement.classList.remove(CLASS_NAME_ACTIVE)
nextElement.classList.add(CLASS_NAME_ACTIVE)
this._isSliding = false
triggerSlidEvent()
}
this._queueCallback(completeCallBack, activeElement, this._isAnimated())
if (isCycling) {
this.cycle()
}
}
_isAnimated() {
return this._element.classList.contains(CLASS_NAME_SLIDE)
}
_getActive() {
return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
}
_clearInterval() {
if (this._interval) {
clearInterval(this._interval)
this._interval = null
}
}
_directionToOrder(direction) {
if (![DIRECTION_RIGHT, DIRECTION_LEFT].includes(direction)) {
return direction
@@ -502,7 +432,6 @@ class Carousel extends BaseComponent {
}
// Static
static carouselInterface(element, config) {
const data = Carousel.getOrCreateInstance(element, config)
@@ -544,7 +473,6 @@ class Carousel extends BaseComponent {
}
const config = {
...Manipulator.getDataAttributes(target),
...Manipulator.getDataAttributes(this)
}
const slideIndex = this.getAttribute('data-bs-slide-to')
@@ -564,9 +492,7 @@ class Carousel extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler)
@@ -574,16 +500,13 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.da
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (let i = 0, len = carousels.length; i < len; i++) {
Carousel.carouselInterface(carousels[i], Carousel.getInstance(carousels[i]))
for (const carousel of carousels) {
Carousel.getOrCreateInstance(carousel)
}
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Carousel to jQuery only if jQuery is present
*/
defineJQueryPlugin(Carousel)
+60 -93
View File
@@ -8,21 +8,16 @@
import {
defineJQueryPlugin,
getElement,
getSelectorFromElement,
getElementFromSelector,
reflow,
typeCheckConfig
getSelectorFromElement,
reflow
} from './util/index'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'collapse'
@@ -30,16 +25,6 @@ const DATA_KEY = 'bs.collapse'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const Default = {
toggle: true,
parent: null
}
const DefaultType = {
toggle: 'boolean',
parent: '(null|element)'
}
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
@@ -59,30 +44,35 @@ const HEIGHT = 'height'
const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'
const Default = {
toggle: true,
parent: null
}
const DefaultType = {
toggle: 'boolean',
parent: '(null|element)'
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Collapse extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._isTransitioning = false
this._config = this._getConfig(config)
this._triggerArray = []
const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = toggleList.length; i < len; i++) {
const elem = toggleList[i]
for (const elem of toggleList) {
const selector = getSelectorFromElement(elem)
const filterElement = SelectorEngine.find(selector)
.filter(foundElem => foundElem === this._element)
.filter(foundElement => foundElement === this._element)
if (selector !== null && filterElement.length) {
this._selector = selector
this._triggerArray.push(elem)
}
}
@@ -99,17 +89,19 @@ class Collapse extends BaseComponent {
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
if (this._isShown()) {
this.hide()
@@ -123,22 +115,17 @@ class Collapse extends BaseComponent {
return
}
let actives = []
let activesData
let activeChildren = []
// find active children
if (this._config.parent) {
const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)
actives = SelectorEngine.find(SELECTOR_ACTIVES, this._config.parent).filter(elem => !children.includes(elem)) // remove children if greater depth
activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)
.filter(element => element !== this._element)
.map(element => Collapse.getOrCreateInstance(element, { toggle: false }))
}
const container = SelectorEngine.findOne(this._selector)
if (actives.length) {
const tempActiveData = actives.find(elem => container !== elem)
activesData = tempActiveData ? Collapse.getInstance(tempActiveData) : null
if (activesData && activesData._isTransitioning) {
return
}
if (activeChildren.length && activeChildren[0]._isTransitioning) {
return
}
const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)
@@ -146,15 +133,9 @@ class Collapse extends BaseComponent {
return
}
actives.forEach(elemActive => {
if (container !== elemActive) {
Collapse.getOrCreateInstance(elemActive, { toggle: false }).hide()
}
if (!activesData) {
Data.set(elemActive, DATA_KEY, null)
}
})
for (const activeInstance of activeChildren) {
activeInstance.hide()
}
const dimension = this._getDimension()
@@ -203,12 +184,10 @@ class Collapse extends BaseComponent {
this._element.classList.add(CLASS_NAME_COLLAPSING)
this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
const triggerArrayLength = this._triggerArray.length
for (let i = 0; i < triggerArrayLength; i++) {
const trigger = this._triggerArray[i]
const elem = getElementFromSelector(trigger)
for (const trigger of this._triggerArray) {
const element = getElementFromSelector(trigger)
if (elem && !this._isShown(elem)) {
if (element && !this._isShown(element)) {
this._addAriaAndCollapsedClass([trigger], false)
}
}
@@ -232,16 +211,9 @@ class Collapse extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...config
}
_configAfterMerge(config) {
config.toggle = Boolean(config.toggle) // Coerce string values
config.parent = getElement(config.parent)
typeCheckConfig(NAME, config, DefaultType)
return config
}
@@ -254,15 +226,21 @@ class Collapse extends BaseComponent {
return
}
const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)
SelectorEngine.find(SELECTOR_DATA_TOGGLE, this._config.parent).filter(elem => !children.includes(elem))
.forEach(element => {
const selected = getElementFromSelector(element)
const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)
if (selected) {
this._addAriaAndCollapsedClass([element], this._isShown(selected))
}
})
for (const element of children) {
const selected = 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) {
@@ -270,26 +248,20 @@ class Collapse extends BaseComponent {
return
}
triggerArray.forEach(elem => {
if (isOpen) {
elem.classList.remove(CLASS_NAME_COLLAPSED)
} else {
elem.classList.add(CLASS_NAME_COLLAPSED)
}
elem.setAttribute('aria-expanded', isOpen)
})
for (const element of triggerArray) {
element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)
element.setAttribute('aria-expanded', isOpen)
}
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const _config = {}
if (typeof config === 'string' && /show|hide/.test(config)) {
_config.toggle = false
}
const _config = {}
if (typeof config === 'string' && /show|hide/.test(config)) {
_config.toggle = false
}
return this.each(function () {
const data = Collapse.getOrCreateInstance(this, _config)
if (typeof config === 'string') {
@@ -304,9 +276,7 @@ class Collapse extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
@@ -318,16 +288,13 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
const selector = getSelectorFromElement(this)
const selectorElements = SelectorEngine.find(selector)
selectorElements.forEach(element => {
for (const element of selectorElements) {
Collapse.getOrCreateInstance(element, { toggle: false }).toggle()
})
}
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Collapse to jQuery only if jQuery is present
*/
defineJQueryPlugin(Collapse)
-2
View File
@@ -6,9 +6,7 @@
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const elementMap = new Map()
+45 -73
View File
@@ -8,9 +8,7 @@
import { getjQuery } from '../util/index'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
@@ -73,9 +71,7 @@ const nativeEvents = new Set([
])
/**
* ------------------------------------------------------------------------
* Private methods
* ------------------------------------------------------------------------
*/
function getUidEvent(element, uid) {
@@ -108,66 +104,54 @@ function bootstrapDelegationHandler(element, selector, fn) {
const domElements = element.querySelectorAll(selector)
for (let { target } = event; target && target !== this; target = target.parentNode) {
for (let i = domElements.length; i--;) {
if (domElements[i] === target) {
event.delegateTarget = target
if (handler.oneOff) {
EventHandler.off(element, event.type, selector, fn)
}
return fn.apply(target, [event])
for (const domElement of domElements) {
if (domElement !== target) {
continue
}
event.delegateTarget = target
if (handler.oneOff) {
EventHandler.off(element, event.type, selector, fn)
}
return fn.apply(target, [event])
}
}
// To please ESLint
return null
}
}
function findHandler(events, handler, delegationSelector = null) {
const uidEventList = Object.keys(events)
for (let i = 0, len = uidEventList.length; i < len; i++) {
const event = events[uidEventList[i]]
if (event.originalHandler === handler && event.delegationSelector === delegationSelector) {
return event
}
}
return null
return Object.values(events)
.find(event => event.originalHandler === handler && event.delegationSelector === delegationSelector)
}
function normalizeParams(originalTypeEvent, handler, delegationFn) {
function normalizeParameters(originalTypeEvent, handler, delegationFunction) {
const delegation = typeof handler === 'string'
const originalHandler = delegation ? delegationFn : handler
const originalHandler = delegation ? delegationFunction : handler
let typeEvent = getTypeEvent(originalTypeEvent)
const isNative = nativeEvents.has(typeEvent)
if (!isNative) {
if (!nativeEvents.has(typeEvent)) {
typeEvent = originalTypeEvent
}
return [delegation, originalHandler, typeEvent]
}
function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {
if (typeof originalTypeEvent !== 'string' || !element) {
return
}
if (!handler) {
handler = delegationFn
delegationFn = null
handler = delegationFunction
delegationFunction = null
}
// 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 (customEventsRegex.test(originalTypeEvent)) {
const wrapFn = fn => {
const wrapFunction = fn => {
return function (event) {
if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {
return fn.call(this, event)
@@ -175,27 +159,27 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
}
}
if (delegationFn) {
delegationFn = wrapFn(delegationFn)
if (delegationFunction) {
delegationFunction = wrapFunction(delegationFunction)
} else {
handler = wrapFn(handler)
handler = wrapFunction(handler)
}
}
const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const events = getEvent(element)
const handlers = events[typeEvent] || (events[typeEvent] = {})
const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null)
const previousFunction = findHandler(handlers, originalHandler, delegation ? handler : null)
if (previousFn) {
previousFn.oneOff = previousFn.oneOff && oneOff
if (previousFunction) {
previousFunction.oneOff = previousFunction.oneOff && oneOff
return
}
const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
const fn = delegation ?
bootstrapDelegationHandler(element, handler, delegationFn) :
bootstrapDelegationHandler(element, handler, delegationFunction) :
bootstrapHandler(element, handler)
fn.delegationSelector = delegation ? handler : null
@@ -221,13 +205,12 @@ function removeHandler(element, events, typeEvent, handler, delegationSelector)
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
const storeElementEvent = events[typeEvent] || {}
Object.keys(storeElementEvent).forEach(handlerKey => {
for (const handlerKey of Object.keys(storeElementEvent)) {
if (handlerKey.includes(namespace)) {
const event = storeElementEvent[handlerKey]
removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
}
})
}
}
function getTypeEvent(event) {
@@ -237,20 +220,20 @@ function getTypeEvent(event) {
}
const EventHandler = {
on(element, event, handler, delegationFn) {
addHandler(element, event, handler, delegationFn, false)
on(element, event, handler, delegationFunction) {
addHandler(element, event, handler, delegationFunction, false)
},
one(element, event, handler, delegationFn) {
addHandler(element, event, handler, delegationFn, true)
one(element, event, handler, delegationFunction) {
addHandler(element, event, handler, delegationFunction, true)
},
off(element, originalTypeEvent, handler, delegationFn) {
off(element, originalTypeEvent, handler, delegationFunction) {
if (typeof originalTypeEvent !== 'string' || !element) {
return
}
const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const inNamespace = typeEvent !== originalTypeEvent
const events = getEvent(element)
const isNamespace = originalTypeEvent.startsWith('.')
@@ -266,21 +249,20 @@ const EventHandler = {
}
if (isNamespace) {
Object.keys(events).forEach(elementEvent => {
for (const elementEvent of Object.keys(events)) {
removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
})
}
}
const storeElementEvent = events[typeEvent] || {}
Object.keys(storeElementEvent).forEach(keyHandlers => {
for (const keyHandlers of Object.keys(storeElementEvent)) {
const handlerKey = keyHandlers.replace(stripUidRegex, '')
if (!inNamespace || originalTypeEvent.includes(handlerKey)) {
const event = storeElementEvent[keyHandlers]
removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
}
})
}
},
trigger(element, event, args) {
@@ -291,13 +273,11 @@ const EventHandler = {
const $ = getjQuery()
const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent
const isNative = nativeEvents.has(typeEvent)
let jQueryEvent
let jQueryEvent = null
let bubbles = true
let nativeDispatch = true
let defaultPrevented = false
let evt = null
if (inNamespace && $) {
jQueryEvent = $.Event(event, args)
@@ -308,25 +288,17 @@ const EventHandler = {
defaultPrevented = jQueryEvent.isDefaultPrevented()
}
if (isNative) {
evt = document.createEvent('HTMLEvents')
evt.initEvent(typeEvent, bubbles, true)
} else {
evt = new CustomEvent(event, {
bubbles,
cancelable: true
})
}
const evt = new Event(event, { bubbles, cancelable: true })
// merge custom information in our event
if (typeof args !== 'undefined') {
Object.keys(args).forEach(key => {
for (const key of Object.keys(args)) {
Object.defineProperty(evt, key, {
get() {
return args[key]
}
})
})
}
}
if (defaultPrevented) {
@@ -337,7 +309,7 @@ const EventHandler = {
element.dispatchEvent(evt)
}
if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') {
if (evt.defaultPrevented && jQueryEvent) {
jQueryEvent.preventDefault()
}
+13 -14
View File
@@ -5,24 +5,24 @@
* --------------------------------------------------------------------------
*/
function normalizeData(val) {
if (val === 'true') {
function normalizeData(value) {
if (value === 'true') {
return true
}
if (val === 'false') {
if (value === 'false') {
return false
}
if (val === Number(val).toString()) {
return Number(val)
if (value === Number(value).toString()) {
return Number(value)
}
if (val === '' || val === 'null') {
if (value === '' || value === 'null') {
return null
}
return val
return value
}
function normalizeDataKey(key) {
@@ -44,14 +44,13 @@ const Manipulator = {
}
const attributes = {}
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs'))
Object.keys(element.dataset)
.filter(key => key.startsWith('bs'))
.forEach(key => {
let pureKey = key.replace(/^bs/, '')
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)
attributes[pureKey] = normalizeData(element.dataset[key])
})
for (const key of bsKeys) {
let pureKey = key.replace(/^bs/, '')
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)
attributes[pureKey] = normalizeData(element.dataset[key])
}
return attributes
},
+10 -19
View File
@@ -5,15 +5,11 @@
* --------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
import { isDisabled, isVisible } from '../util/index'
const NODE_TEXT = 3
/**
* Constants
*/
const SelectorEngine = {
find(selector, element = document.documentElement) {
@@ -25,21 +21,16 @@ const SelectorEngine = {
},
children(element, selector) {
return [].concat(...element.children)
.filter(child => child.matches(selector))
return [].concat(...element.children).filter(child => child.matches(selector))
},
parents(element, selector) {
const parents = []
let ancestor = element.parentNode.closest(selector)
let ancestor = element.parentNode
while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== NODE_TEXT) {
if (ancestor.matches(selector)) {
parents.push(ancestor)
}
ancestor = ancestor.parentNode
while (ancestor) {
parents.push(ancestor)
ancestor = ancestor.parentNode.closest(selector)
}
return parents
@@ -58,7 +49,7 @@ const SelectorEngine = {
return []
},
// TODO: this is now unused; remove later along with prev()
next(element, selector) {
let next = element.nextElementSibling
@@ -83,7 +74,7 @@ const SelectorEngine = {
'details',
'[tabindex]',
'[contenteditable="true"]'
].map(selector => `${selector}:not([tabindex^="-"])`).join(', ')
].map(selector => `${selector}:not([tabindex^="-"])`).join(',')
return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))
}
+72 -126
View File
@@ -6,18 +6,15 @@
*/
import * as Popper from '@popperjs/core'
import {
defineJQueryPlugin,
getElement,
getElementFromSelector,
getNextActiveElement,
isDisabled,
isElement,
isRTL,
isVisible,
noop,
typeCheckConfig
noop
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@@ -25,9 +22,7 @@ import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'dropdown'
@@ -36,14 +31,11 @@ const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ESCAPE_KEY = 'Escape'
const SPACE_KEY = 'Space'
const TAB_KEY = 'Tab'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_DOWN_KEY = 'ArrowDown'
const RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button
const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEY}|${ARROW_DOWN_KEY}|${ESCAPE_KEY}`)
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
@@ -56,10 +48,11 @@ const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_DROPUP = 'dropup'
const CLASS_NAME_DROPEND = 'dropend'
const CLASS_NAME_DROPSTART = 'dropstart'
const CLASS_NAME_NAVBAR = 'navbar'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'
const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`
const SELECTOR_MENU = '.dropdown-menu'
const SELECTOR_NAVBAR = '.navbar'
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
@@ -89,23 +82,20 @@ const DefaultType = {
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Dropdown extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._popper = null
this._config = this._getConfig(config)
this._menu = this._getMenuElement()
this._parent = this._element.parentNode // dropdown wrapper
this._menu = SelectorEngine.findOne(SELECTOR_MENU, this._parent)
this._inNavbar = this._detectNavbar()
}
// Getters
static get Default() {
return Default
}
@@ -119,13 +109,12 @@ class Dropdown extends BaseComponent {
}
// Public
toggle() {
return this._isShown() ? this.hide() : this.show()
}
show() {
if (isDisabled(this._element) || this._isShown(this._menu)) {
if (isDisabled(this._element) || this._isShown()) {
return
}
@@ -139,22 +128,16 @@ class Dropdown extends BaseComponent {
return
}
const parent = Dropdown.getParentFromElement(this._element)
// Totally disable Popper for Dropdowns in Navbar
if (this._inNavbar) {
Manipulator.setDataAttribute(this._menu, 'popper', 'none')
} else {
this._createPopper(parent)
}
this._createPopper()
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement &&
!parent.closest(SELECTOR_NAVBAR_NAV)) {
[].concat(...document.body.children)
.forEach(elem => EventHandler.on(elem, 'mouseover', noop))
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
this._element.focus()
@@ -166,7 +149,7 @@ class Dropdown extends BaseComponent {
}
hide() {
if (isDisabled(this._element) || !this._isShown(this._menu)) {
if (isDisabled(this._element) || !this._isShown()) {
return
}
@@ -193,7 +176,6 @@ class Dropdown extends BaseComponent {
}
// Private
_completeHide(relatedTarget) {
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
if (hideEvent.defaultPrevented) {
@@ -203,8 +185,9 @@ class Dropdown extends BaseComponent {
// If this is a touch-enabled device we remove the extra
// empty mouseover listeners we added for iOS support
if ('ontouchstart' in document.documentElement) {
[].concat(...document.body.children)
.forEach(elem => EventHandler.off(elem, 'mouseover', noop))
for (const element of [].concat(...document.body.children)) {
EventHandler.off(element, 'mouseover', noop)
}
}
if (this._popper) {
@@ -219,13 +202,7 @@ class Dropdown extends BaseComponent {
}
_getConfig(config) {
config = {
...this.constructor.Default,
...Manipulator.getDataAttributes(this._element),
...config
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
config = super._getConfig(config)
if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'
@@ -237,7 +214,7 @@ class Dropdown extends BaseComponent {
return config
}
_createPopper(parent) {
_createPopper() {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
}
@@ -245,7 +222,7 @@ class Dropdown extends BaseComponent {
let referenceElement = this._element
if (this._config.reference === 'parent') {
referenceElement = parent
referenceElement = this._parent
} else if (isElement(this._config.reference)) {
referenceElement = getElement(this._config.reference)
} else if (typeof this._config.reference === 'object') {
@@ -253,25 +230,15 @@ class Dropdown extends BaseComponent {
}
const popperConfig = this._getPopperConfig()
const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
if (isDisplayStatic) {
Manipulator.setDataAttribute(this._menu, 'popper', 'static')
}
}
_isShown(element = this._element) {
return element.classList.contains(CLASS_NAME_SHOW)
}
_getMenuElement() {
return SelectorEngine.next(this._element, SELECTOR_MENU)[0]
_isShown() {
return this._menu.classList.contains(CLASS_NAME_SHOW)
}
_getPlacement() {
const parentDropdown = this._element.parentNode
const parentDropdown = this._parent
if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
return PLACEMENT_RIGHT
@@ -292,14 +259,14 @@ class Dropdown extends BaseComponent {
}
_detectNavbar() {
return this._element.closest(`.${CLASS_NAME_NAVBAR}`) !== null
return this._element.closest(SELECTOR_NAVBAR) !== null
}
_getOffset() {
const { offset } = this._config
if (typeof offset === 'string') {
return offset.split(',').map(val => Number.parseInt(val, 10))
return offset.split(',').map(value => Number.parseInt(value, 10))
}
if (typeof offset === 'function') {
@@ -326,8 +293,9 @@ class Dropdown extends BaseComponent {
}]
}
// Disable Popper if we have a static display
if (this._config.display === 'static') {
// Disable Popper if we have a static display or Dropdown is in Navbar
if (this._inNavbar || this._config.display === 'static') {
Manipulator.setDataAttribute(this._menu, 'popper', 'static') // todo:v6 remove
defaultBsPopperConfig.modifiers = [{
name: 'applyStyles',
enabled: false
@@ -341,7 +309,7 @@ class Dropdown extends BaseComponent {
}
_selectMenuItem({ key, target }) {
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
if (!items.length) {
return
@@ -353,7 +321,6 @@ class Dropdown extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Dropdown.getOrCreateInstance(this, config)
@@ -371,111 +338,93 @@ class Dropdown extends BaseComponent {
}
static clearMenus(event) {
if (event && (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY))) {
if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
return
}
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)
for (let i = 0, len = toggles.length; i < len; i++) {
const context = Dropdown.getInstance(toggles[i])
for (const toggle of openToggles) {
const context = Dropdown.getInstance(toggle)
if (!context || context._config.autoClose === false) {
continue
}
if (!context._isShown()) {
const composedPath = event.composedPath()
const isMenuTarget = composedPath.includes(context._menu)
if (
composedPath.includes(context._element) ||
(context._config.autoClose === 'inside' && !isMenuTarget) ||
(context._config.autoClose === 'outside' && isMenuTarget)
) {
continue
}
const relatedTarget = {
relatedTarget: context._element
// Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu
if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
continue
}
if (event) {
const composedPath = event.composedPath()
const isMenuTarget = composedPath.includes(context._menu)
if (
composedPath.includes(context._element) ||
(context._config.autoClose === 'inside' && !isMenuTarget) ||
(context._config.autoClose === 'outside' && isMenuTarget)
) {
continue
}
const relatedTarget = { relatedTarget: context._element }
// Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu
if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
continue
}
if (event.type === 'click') {
relatedTarget.clickEvent = event
}
if (event.type === 'click') {
relatedTarget.clickEvent = event
}
context._completeHide(relatedTarget)
}
}
static getParentFromElement(element) {
return getElementFromSelector(element) || element.parentNode
}
static dataApiKeydownHandler(event) {
// If not input/textarea:
// - And not a key in REGEXP_KEYDOWN => not a dropdown command
// If input/textarea:
// - If space key => not a dropdown command
// - If key is other than escape
// - If key is not up or down => not a dropdown command
// - And not a key in UP | DOWN | ESCAPE => not a dropdown command
// If input/textarea && If key is other than ESCAPE
// - If key is not UP or DOWN => not a dropdown command
// - If trigger inside the menu => not a dropdown command
if (/input|textarea/i.test(event.target.tagName) ?
event.key === SPACE_KEY || (event.key !== ESCAPE_KEY &&
((event.key !== ARROW_DOWN_KEY && event.key !== ARROW_UP_KEY) ||
event.target.closest(SELECTOR_MENU))) :
!REGEXP_KEYDOWN.test(event.key)) {
const { target, key, delegateTarget } = event
const isInput = /input|textarea/i.test(target.tagName)
const isEscapeEvent = key === ESCAPE_KEY
const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(key)
if (!isInput && !(isUpOrDownEvent || isEscapeEvent)) {
return
}
const isActive = this.classList.contains(CLASS_NAME_SHOW)
if (isInput && !isEscapeEvent) {
// eslint-disable-next-line unicorn/no-lonely-if
if (!isUpOrDownEvent || target.closest(SELECTOR_MENU)) {
return
}
}
if (!isActive && event.key === ESCAPE_KEY) {
const isActive = delegateTarget.classList.contains(CLASS_NAME_SHOW)
if (!isActive && isEscapeEvent) {
return
}
event.preventDefault()
event.stopPropagation()
if (isDisabled(this)) {
return
}
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
const getToggleButton = SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, delegateTarget.parentNode)
const instance = Dropdown.getOrCreateInstance(getToggleButton)
if (event.key === ESCAPE_KEY) {
if (isEscapeEvent) {
instance.hide()
getToggleButton.focus()
return
}
if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
if (!isActive) {
instance.show()
}
if (isUpOrDownEvent) {
instance.show()
instance._selectMenuItem(event)
return
}
if (!isActive || event.key === SPACE_KEY) {
Dropdown.clearMenus()
}
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
@@ -488,10 +437,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Dropdown to jQuery only if jQuery is present
*/
defineJQueryPlugin(Dropdown)
+103 -157
View File
@@ -5,16 +5,8 @@
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
getElementFromSelector,
isRTL,
isVisible,
reflow,
typeCheckConfig
} from './util/index'
import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import ScrollBarHelper from './util/scrollbar'
import BaseComponent from './base-component'
@@ -23,9 +15,7 @@ import FocusTrap from './util/focustrap'
import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'modal'
@@ -34,6 +24,25 @@ const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ESCAPE_KEY = 'Escape'
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_RESIZE = `resize${EVENT_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_OPEN = 'modal-open'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_STATIC = 'modal-static'
const OPEN_SELECTOR = '.modal.show'
const SELECTOR_DIALOG = '.modal-dialog'
const SELECTOR_MODAL_BODY = '.modal-body'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
const Default = {
backdrop: true,
keyboard: true,
@@ -46,60 +55,36 @@ const DefaultType = {
focus: 'boolean'
}
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_RESIZE = `resize${EVENT_KEY}`
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_OPEN = 'modal-open'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_STATIC = 'modal-static'
const OPEN_SELECTOR = '.modal.show'
const SELECTOR_DIALOG = '.modal-dialog'
const SELECTOR_MODAL_BODY = '.modal-body'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Modal extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
this._isShown = false
this._ignoreBackdropClick = false
this._isTransitioning = false
this._scrollBar = new ScrollBarHelper()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle(relatedTarget) {
return this._isShown ? this.hide() : this.show(relatedTarget)
}
@@ -118,10 +103,7 @@ class Modal extends BaseComponent {
}
this._isShown = true
if (this._isAnimated()) {
this._isTransitioning = true
}
this._isTransitioning = true
this._scrollBar.hide()
@@ -129,18 +111,10 @@ class Modal extends BaseComponent {
this._adjustDialog()
this._setEscapeEvent()
this._setResizeEvent()
this._toggleEscapeEventListener(true)
this._toggleResizeEventListener(true)
EventHandler.on(this._dialog, EVENT_MOUSEDOWN_DISMISS, () => {
EventHandler.one(this._element, EVENT_MOUSEUP_DISMISS, event => {
if (event.target === this._element) {
this._ignoreBackdropClick = true
}
})
})
this._showBackdrop(() => this._showElement(relatedTarget))
this._backdrop.show(() => this._showElement(relatedTarget))
}
hide() {
@@ -155,28 +129,22 @@ class Modal extends BaseComponent {
}
this._isShown = false
const isAnimated = this._isAnimated()
this._isTransitioning = true
if (isAnimated) {
this._isTransitioning = true
}
this._setEscapeEvent()
this._setResizeEvent()
this._toggleEscapeEventListener(false)
this._toggleResizeEventListener(false)
this._focustrap.deactivate()
this._element.classList.remove(CLASS_NAME_SHOW)
EventHandler.off(this._element, EVENT_CLICK_DISMISS)
EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS)
this._queueCallback(() => this._hideModal(), this._element, isAnimated)
this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())
}
dispose() {
[window, this._dialog]
.forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY))
for (const htmlElement of [window, this._dialog]) {
EventHandler.off(htmlElement, EVENT_KEY)
}
this._backdrop.dispose()
this._focustrap.deactivate()
@@ -188,11 +156,23 @@ class Modal extends BaseComponent {
}
// Private
_initializeBackDrop() {
const clickCallback = () => {
if (this._config.backdrop === 'static') {
this._triggerBackdropTransition()
return
}
this.hide()
}
// 'static' option will be translated to true, and booleans will keep their value
const isVisible = Boolean(this._config.backdrop)
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value
isAnimated: this._isAnimated()
isVisible,
isAnimated: this._isAnimated(),
clickCallback: isVisible ? clickCallback : null
})
}
@@ -202,22 +182,9 @@ class Modal extends BaseComponent {
})
}
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_showElement(relatedTarget) {
const isAnimated = this._isAnimated()
const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
// Don't move modal's DOM position
// try to append dynamic modal
if (!document.body.contains(this._element)) {
document.body.append(this._element)
}
@@ -227,13 +194,12 @@ class Modal extends BaseComponent {
this._element.setAttribute('role', 'dialog')
this._element.scrollTop = 0
const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
if (modalBody) {
modalBody.scrollTop = 0
}
if (isAnimated) {
reflow(this._element)
}
reflow(this._element)
this._element.classList.add(CLASS_NAME_SHOW)
@@ -248,30 +214,37 @@ class Modal extends BaseComponent {
})
}
this._queueCallback(transitionComplete, this._dialog, isAnimated)
this._queueCallback(transitionComplete, this._dialog, this._isAnimated())
}
_setEscapeEvent() {
if (this._isShown) {
EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
if (this._config.keyboard && event.key === ESCAPE_KEY) {
event.preventDefault()
this.hide()
} else if (!this._config.keyboard && event.key === ESCAPE_KEY) {
this._triggerBackdropTransition()
}
})
} else {
_toggleEscapeEventListener(enable) {
if (!enable) {
EventHandler.off(this._element, EVENT_KEYDOWN_DISMISS)
return
}
EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
if (event.key !== ESCAPE_KEY) {
return
}
if (this._config.keyboard) {
event.preventDefault()
this.hide()
return
}
this._triggerBackdropTransition()
})
}
_setResizeEvent() {
if (this._isShown) {
_toggleResizeEventListener(enable) {
if (enable) {
EventHandler.on(window, EVENT_RESIZE, () => this._adjustDialog())
} else {
EventHandler.off(window, EVENT_RESIZE)
return
}
EventHandler.off(window, EVENT_RESIZE)
}
_hideModal() {
@@ -280,6 +253,7 @@ class Modal extends BaseComponent {
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
this._isTransitioning = false
this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments()
@@ -288,27 +262,6 @@ class Modal extends BaseComponent {
})
}
_showBackdrop(callback) {
EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
if (this._ignoreBackdropClick) {
this._ignoreBackdropClick = false
return
}
if (event.target !== event.currentTarget) {
return
}
if (this._config.backdrop === true) {
this.hide()
} else if (this._config.backdrop === 'static') {
this._triggerBackdropTransition()
}
})
this._backdrop.show(callback)
}
_isAnimated() {
return this._element.classList.contains(CLASS_NAME_FADE)
}
@@ -319,46 +272,45 @@ class Modal extends BaseComponent {
return
}
const { classList, scrollHeight, style } = this._element
const isModalOverflowing = scrollHeight > document.documentElement.clientHeight
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const initialOverflowY = this._element.style.overflowY
// return if the following background transition hasn't yet completed
if ((!isModalOverflowing && style.overflowY === 'hidden') || classList.contains(CLASS_NAME_STATIC)) {
if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
return
}
if (!isModalOverflowing) {
style.overflowY = 'hidden'
this._element.style.overflowY = 'hidden'
}
classList.add(CLASS_NAME_STATIC)
this._element.classList.add(CLASS_NAME_STATIC)
this._queueCallback(() => {
classList.remove(CLASS_NAME_STATIC)
if (!isModalOverflowing) {
this._queueCallback(() => {
style.overflowY = ''
}, this._dialog)
}
this._element.classList.remove(CLASS_NAME_STATIC)
this._queueCallback(() => {
this._element.style.overflowY = initialOverflowY
}, this._dialog)
}, this._dialog)
this._element.focus()
}
// ----------------------------------------------------------------------
// the following methods are used to handle overflowing modals
// ----------------------------------------------------------------------
/**
* The following methods are used to handle overflowing modals
*/
_adjustDialog() {
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const scrollbarWidth = this._scrollBar.getWidth()
const isBodyOverflowing = scrollbarWidth > 0
if ((!isBodyOverflowing && isModalOverflowing && !isRTL()) || (isBodyOverflowing && !isModalOverflowing && isRTL())) {
this._element.style.paddingLeft = `${scrollbarWidth}px`
if (isBodyOverflowing && !isModalOverflowing) {
const property = isRTL() ? 'paddingLeft' : 'paddingRight'
this._element.style[property] = `${scrollbarWidth}px`
}
if ((isBodyOverflowing && !isModalOverflowing && !isRTL()) || (!isBodyOverflowing && isModalOverflowing && isRTL())) {
this._element.style.paddingRight = `${scrollbarWidth}px`
if (!isBodyOverflowing && isModalOverflowing) {
const property = isRTL() ? 'paddingRight' : 'paddingLeft'
this._element.style[property] = `${scrollbarWidth}px`
}
}
@@ -368,7 +320,6 @@ class Modal extends BaseComponent {
}
// Static
static jQueryInterface(config, relatedTarget) {
return this.each(function () {
const data = Modal.getOrCreateInstance(this, config)
@@ -387,9 +338,7 @@ class Modal extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
@@ -412,10 +361,10 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})
})
// avoid conflict when clicking moddal toggler while another one is open
const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
if (allReadyOpen) {
Modal.getInstance(allReadyOpen).hide()
// avoid conflict when clicking modal toggler while another one is open
const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
if (alreadyOpen) {
Modal.getInstance(alreadyOpen).hide()
}
const data = Modal.getOrCreateInstance(target)
@@ -426,10 +375,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
enableDismissTrigger(Modal)
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Modal to jQuery only if jQuery is present
*/
defineJQueryPlugin(Modal)
+40 -58
View File
@@ -9,22 +9,18 @@ import {
defineJQueryPlugin,
getElementFromSelector,
isDisabled,
isVisible,
typeCheckConfig
isVisible
} from './util/index'
import ScrollBarHelper from './util/scrollbar'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
import SelectorEngine from './dom/selector-engine'
import Manipulator from './dom/manipulator'
import Backdrop from './util/backdrop'
import FocusTrap from './util/focustrap'
import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'offcanvas'
@@ -34,6 +30,21 @@ const DATA_API_KEY = '.data-api'
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const ESCAPE_KEY = 'Escape'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_SHOWING = 'showing'
const CLASS_NAME_HIDING = 'hiding'
const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'
const OPEN_SELECTOR = '.offcanvas.show'
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'
const Default = {
backdrop: true,
keyboard: true,
@@ -46,30 +57,14 @@ const DefaultType = {
scroll: 'boolean'
}
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'
const OPEN_SELECTOR = '.offcanvas.show'
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Offcanvas extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._isShown = false
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
@@ -77,17 +72,19 @@ class Offcanvas extends BaseComponent {
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
static get Default() {
return Default
}
// Public
toggle(relatedTarget) {
return this._isShown ? this.hide() : this.show(relatedTarget)
}
@@ -104,24 +101,23 @@ class Offcanvas extends BaseComponent {
}
this._isShown = true
this._element.style.visibility = 'visible'
this._backdrop.show()
if (!this._config.scroll) {
new ScrollBarHelper().hide()
}
this._element.removeAttribute('aria-hidden')
this._element.setAttribute('aria-modal', true)
this._element.setAttribute('role', 'dialog')
this._element.classList.add(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_SHOWING)
const completeCallBack = () => {
if (!this._config.scroll) {
this._focustrap.activate()
}
this._element.classList.add(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOWING)
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })
}
@@ -142,14 +138,13 @@ class Offcanvas extends BaseComponent {
this._focustrap.deactivate()
this._element.blur()
this._isShown = false
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_HIDING)
this._backdrop.hide()
const completeCallback = () => {
this._element.setAttribute('aria-hidden', true)
this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
this._element.style.visibility = 'hidden'
if (!this._config.scroll) {
new ScrollBarHelper().reset()
@@ -168,17 +163,6 @@ class Offcanvas extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_initializeBackDrop() {
return new Backdrop({
className: CLASS_NAME_BACKDROP,
@@ -204,7 +188,6 @@ class Offcanvas extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Offcanvas.getOrCreateInstance(this, config)
@@ -223,9 +206,7 @@ class Offcanvas extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
@@ -247,24 +228,25 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})
// avoid conflict when clicking a toggler of an offcanvas, while another is open
const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
if (allReadyOpen && allReadyOpen !== target) {
Offcanvas.getInstance(allReadyOpen).hide()
const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
if (alreadyOpen && alreadyOpen !== target) {
Offcanvas.getInstance(alreadyOpen).hide()
}
const data = Offcanvas.getOrCreateInstance(target)
data.toggle(this)
})
EventHandler.on(window, EVENT_LOAD_DATA_API, () =>
SelectorEngine.find(OPEN_SELECTOR).forEach(el => Offcanvas.getOrCreateInstance(el).show())
)
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {
Offcanvas.getOrCreateInstance(selector).show()
}
})
enableDismissTrigger(Offcanvas)
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/
defineJQueryPlugin(Offcanvas)
+24 -55
View File
@@ -9,15 +9,13 @@ import { defineJQueryPlugin } from './util/index'
import Tooltip from './tooltip'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'popover'
const DATA_KEY = 'bs.popover'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-popover'
const SELECTOR_TITLE = '.popover-header'
const SELECTOR_CONTENT = '.popover-body'
const Default = {
...Tooltip.Default,
@@ -34,93 +32,64 @@ const Default = {
const DefaultType = {
...Tooltip.DefaultType,
content: '(string|element|function)'
content: '(null|string|element|function)'
}
const Event = {
HIDE: `hide${EVENT_KEY}`,
HIDDEN: `hidden${EVENT_KEY}`,
SHOW: `show${EVENT_KEY}`,
SHOWN: `shown${EVENT_KEY}`,
INSERTED: `inserted${EVENT_KEY}`,
CLICK: `click${EVENT_KEY}`,
FOCUSIN: `focusin${EVENT_KEY}`,
FOCUSOUT: `focusout${EVENT_KEY}`,
MOUSEENTER: `mouseenter${EVENT_KEY}`,
MOUSELEAVE: `mouseleave${EVENT_KEY}`
}
const SELECTOR_TITLE = '.popover-header'
const SELECTOR_CONTENT = '.popover-body'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Popover extends Tooltip {
// Getters
static get Default() {
return Default
}
static get NAME() {
return NAME
}
static get Event() {
return Event
}
static get DefaultType() {
return DefaultType
}
// Overrides
isWithContent() {
return this.getTitle() || this._getContent()
static get NAME() {
return NAME
}
setContent(tip) {
this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TITLE)
this._sanitizeAndSetContent(tip, this._getContent(), SELECTOR_CONTENT)
// Overrides
_isWithContent() {
return this._getTitle() || this._getContent()
}
// Private
_getContentForTemplate() {
return {
[SELECTOR_TITLE]: this._getTitle(),
[SELECTOR_CONTENT]: this._getContent()
}
}
_getContent() {
return this._resolvePossibleFunction(this._config.content)
}
_getBasicClassPrefix() {
return CLASS_PREFIX
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Popover.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Popover to jQuery only if jQuery is present
*/
defineJQueryPlugin(Popover)
+63 -92
View File
@@ -5,21 +5,14 @@
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
getElement,
getSelectorFromElement,
typeCheckConfig
} from './util/index'
import { defineJQueryPlugin, getElement, getSelectorFromElement } from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'scrollspy'
@@ -27,18 +20,6 @@ const DATA_KEY = 'bs.scrollspy'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const Default = {
offset: 10,
method: 'auto',
target: ''
}
const DefaultType = {
offset: 'number',
method: 'string',
target: '(string|element)'
}
const EVENT_ACTIVATE = `activate${EVENT_KEY}`
const EVENT_SCROLL = `scroll${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
@@ -58,17 +39,26 @@ const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
const METHOD_OFFSET = 'offset'
const METHOD_POSITION = 'position'
const Default = {
offset: 10,
method: 'auto',
target: ''
}
const DefaultType = {
offset: 'number',
method: 'string',
target: '(string|element)'
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._scrollElement = this._element.tagName === 'BODY' ? window : this._element
this._config = this._getConfig(config)
this._offsets = []
this._targets = []
this._activeTarget = null
@@ -81,58 +71,49 @@ class ScrollSpy extends BaseComponent {
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
refresh() {
const autoMethod = this._scrollElement === this._scrollElement.window ?
METHOD_OFFSET :
METHOD_POSITION
const offsetMethod = this._config.method === 'auto' ?
autoMethod :
this._config.method
const offsetBase = offsetMethod === METHOD_POSITION ?
this._getScrollTop() :
0
this._offsets = []
this._targets = []
this._scrollHeight = this._getScrollHeight()
const autoMethod = this._scrollElement === this._scrollElement.window ? METHOD_OFFSET : METHOD_POSITION
const offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method
const offsetBase = offsetMethod === METHOD_POSITION ? this._getScrollTop() : 0
const targets = SelectorEngine.find(SELECTOR_LINK_ITEMS, this._config.target)
.map(element => {
const targetSelector = getSelectorFromElement(element)
const target = targetSelector ? SelectorEngine.findOne(targetSelector) : null
targets.map(element => {
const targetSelector = getSelectorFromElement(element)
const target = targetSelector ? SelectorEngine.findOne(targetSelector) : null
if (target) {
const targetBCR = target.getBoundingClientRect()
if (targetBCR.width || targetBCR.height) {
return [
Manipulator[offsetMethod](target).top + offsetBase,
targetSelector
]
if (!target) {
return null
}
}
return null
})
.filter(item => item)
.sort((a, b) => a[0] - b[0])
.forEach(item => {
this._offsets.push(item[0])
this._targets.push(item[1])
const targetBCR = target.getBoundingClientRect()
return targetBCR.width || targetBCR.height ?
[Manipulator[offsetMethod](target).top + offsetBase, targetSelector] :
null
})
.filter(Boolean)
.sort((a, b) => a[0] - b[0])
for (const target of targets) {
this._offsets.push(target[0])
this._targets.push(target[1])
}
}
dispose() {
@@ -142,17 +123,9 @@ class ScrollSpy extends BaseComponent {
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
_configAfterMerge(config) {
config.target = getElement(config.target) || document.documentElement
typeCheckConfig(NAME, config, DefaultType)
return config
}
@@ -200,7 +173,7 @@ class ScrollSpy extends BaseComponent {
return
}
for (let i = this._offsets.length; i--;) {
for (const i of this._offsets.keys()) {
const isActiveTarget = this._activeTarget !== this._targets[i] &&
scrollTop >= this._offsets[i] &&
(typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1])
@@ -226,20 +199,20 @@ class ScrollSpy extends BaseComponent {
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN))
.classList.add(CLASS_NAME_ACTIVE)
} else {
SelectorEngine.parents(link, SELECTOR_NAV_LIST_GROUP)
.forEach(listGroup => {
// Set triggered links parents as active
// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
SelectorEngine.prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)
.forEach(item => item.classList.add(CLASS_NAME_ACTIVE))
for (const listGroup of SelectorEngine.parents(link, SELECTOR_NAV_LIST_GROUP)) {
// Set triggered links parents as active
// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
for (const item of SelectorEngine.prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)) {
item.classList.add(CLASS_NAME_ACTIVE)
}
// Handle special case when .nav-link is inside .nav-item
SelectorEngine.prev(listGroup, SELECTOR_NAV_ITEMS)
.forEach(navItem => {
SelectorEngine.children(navItem, SELECTOR_NAV_LINKS)
.forEach(item => item.classList.add(CLASS_NAME_ACTIVE))
})
})
// Handle special case when .nav-link is inside .nav-item
for (const navItem of SelectorEngine.prev(listGroup, SELECTOR_NAV_ITEMS)) {
for (const item of SelectorEngine.children(navItem, SELECTOR_NAV_LINKS)) {
item.classList.add(CLASS_NAME_ACTIVE)
}
}
}
}
EventHandler.trigger(this._scrollElement, EVENT_ACTIVATE, {
@@ -248,13 +221,15 @@ class ScrollSpy extends BaseComponent {
}
_clear() {
SelectorEngine.find(SELECTOR_LINK_ITEMS, this._config.target)
const activeNodes = SelectorEngine.find(SELECTOR_LINK_ITEMS, this._config.target)
.filter(node => node.classList.contains(CLASS_NAME_ACTIVE))
.forEach(node => node.classList.remove(CLASS_NAME_ACTIVE))
for (const node of activeNodes) {
node.classList.remove(CLASS_NAME_ACTIVE)
}
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = ScrollSpy.getOrCreateInstance(this, config)
@@ -273,21 +248,17 @@ class ScrollSpy extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
SelectorEngine.find(SELECTOR_DATA_SPY)
.forEach(spy => new ScrollSpy(spy))
for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
new ScrollSpy(spy) // eslint-disable-line no-new
}
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .ScrollSpy to jQuery only if jQuery is present
*/
defineJQueryPlugin(ScrollSpy)
+13 -38
View File
@@ -5,20 +5,13 @@
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
getElementFromSelector,
isDisabled,
reflow
} from './util/index'
import { defineJQueryPlugin, getElementFromSelector, isDisabled, reflow } from './util/index'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'tab'
@@ -46,30 +39,26 @@ const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
const SELECTOR_DROPDOWN_ACTIVE_CHILD = ':scope > .dropdown-menu .active'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Tab extends BaseComponent {
// Getters
static get NAME() {
return NAME
}
// Public
show() {
if ((this._element.parentNode &&
if (this._element.parentNode &&
this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
this._element.classList.contains(CLASS_NAME_ACTIVE))) {
this._element.classList.contains(CLASS_NAME_ACTIVE)) {
return
}
let previous
const target = getElementFromSelector(this._element)
const listElement = this._element.closest(SELECTOR_NAV_LIST_GROUP)
let previous
if (listElement) {
const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? SELECTOR_ACTIVE_UL : SELECTOR_ACTIVE
@@ -78,14 +67,10 @@ class Tab extends BaseComponent {
}
const hideEvent = previous ?
EventHandler.trigger(previous, EVENT_HIDE, {
relatedTarget: this._element
}) :
EventHandler.trigger(previous, EVENT_HIDE, { relatedTarget: this._element }) :
null
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
relatedTarget: previous
})
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget: previous })
if (showEvent.defaultPrevented || (hideEvent !== null && hideEvent.defaultPrevented)) {
return
@@ -94,12 +79,8 @@ class Tab extends BaseComponent {
this._activate(this._element, listElement)
const complete = () => {
EventHandler.trigger(previous, EVENT_HIDDEN, {
relatedTarget: this._element
})
EventHandler.trigger(this._element, EVENT_SHOWN, {
relatedTarget: previous
})
EventHandler.trigger(previous, EVENT_HIDDEN, { relatedTarget: this._element })
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget: previous })
}
if (target) {
@@ -110,7 +91,6 @@ class Tab extends BaseComponent {
}
// Private
_activate(element, container, callback) {
const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ?
SelectorEngine.find(SELECTOR_ACTIVE_UL, container) :
@@ -164,8 +144,9 @@ class Tab extends BaseComponent {
const dropdownElement = element.closest(SELECTOR_DROPDOWN)
if (dropdownElement) {
SelectorEngine.find(SELECTOR_DROPDOWN_TOGGLE, dropdownElement)
.forEach(dropdown => dropdown.classList.add(CLASS_NAME_ACTIVE))
for (const dropdown of SelectorEngine.find(SELECTOR_DROPDOWN_TOGGLE, dropdownElement)) {
dropdown.classList.add(CLASS_NAME_ACTIVE)
}
}
element.setAttribute('aria-expanded', true)
@@ -177,7 +158,6 @@ class Tab extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Tab.getOrCreateInstance(this)
@@ -194,9 +174,7 @@ class Tab extends BaseComponent {
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
@@ -213,10 +191,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Tab to jQuery only if jQuery is present
*/
defineJQueryPlugin(Tab)
+12 -38
View File
@@ -5,20 +5,13 @@
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
reflow,
typeCheckConfig
} from './util/index'
import { defineJQueryPlugin, reflow } from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import BaseComponent from './base-component'
import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'toast'
@@ -52,16 +45,13 @@ const Default = {
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Toast extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._timeout = null
this._hasMouseInteraction = false
this._hasKeyboardInteraction = false
@@ -69,21 +59,19 @@ class Toast extends BaseComponent {
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get Default() {
return Default
}
static get NAME() {
return NAME
}
// Public
show() {
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW)
@@ -106,8 +94,7 @@ class Toast extends BaseComponent {
this._element.classList.remove(CLASS_NAME_HIDE) // @deprecated
reflow(this._element)
this._element.classList.add(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_SHOWING)
this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING)
this._queueCallback(complete, this._element, this._config.animation)
}
@@ -125,8 +112,7 @@ class Toast extends BaseComponent {
const complete = () => {
this._element.classList.add(CLASS_NAME_HIDE) // @deprecated
this._element.classList.remove(CLASS_NAME_SHOWING)
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW)
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
@@ -146,18 +132,6 @@ class Toast extends BaseComponent {
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
return config
}
_maybeScheduleHide() {
if (!this._config.autohide) {
return
@@ -212,7 +186,6 @@ class Toast extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Toast.getOrCreateInstance(this, config)
@@ -228,13 +201,14 @@ class Toast extends BaseComponent {
}
}
/**
* Data API implementation
*/
enableDismissTrigger(Toast)
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Toast to jQuery only if jQuery is present
*/
defineJQueryPlugin(Toast)
+262 -370
View File
@@ -6,55 +6,44 @@
*/
import * as Popper from '@popperjs/core'
import {
defineJQueryPlugin,
findShadowRoot,
getElement,
getUID,
isElement,
isRTL,
noop,
typeCheckConfig
} from './util/index'
import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer'
import Data from './dom/data'
import { defineJQueryPlugin, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index'
import { DefaultAllowlist } from './util/sanitizer'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
import TemplateFactory from './util/template-factory'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'tooltip'
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-tooltip'
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
const DefaultType = {
animation: 'boolean',
template: 'string',
title: '(string|element|function)',
trigger: 'string',
delay: '(number|object)',
html: 'boolean',
selector: '(string|boolean)',
placement: '(string|function)',
offset: '(array|string|function)',
container: '(string|element|boolean)',
fallbackPlacements: 'array',
boundary: '(string|element)',
customClass: '(string|function)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
allowList: 'object',
popperConfig: '(null|object|function)'
}
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_MODAL = 'modal'
const CLASS_NAME_SHOW = 'show'
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
const EVENT_MODAL_HIDE = 'hide.bs.modal'
const TRIGGER_HOVER = 'hover'
const TRIGGER_FOCUS = 'focus'
const TRIGGER_CLICK = 'click'
const TRIGGER_MANUAL = 'manual'
const EVENT_HIDE = 'hide'
const EVENT_HIDDEN = 'hidden'
const EVENT_SHOW = 'show'
const EVENT_SHOWN = 'shown'
const EVENT_INSERTED = 'inserted'
const EVENT_CLICK = 'click'
const EVENT_FOCUSIN = 'focusin'
const EVENT_FOCUSOUT = 'focusout'
const EVENT_MOUSEENTER = 'mouseenter'
const EVENT_MOUSELEAVE = 'mouseleave'
const AttachmentMap = {
AUTO: 'auto',
@@ -87,40 +76,28 @@ const Default = {
popperConfig: null
}
const Event = {
HIDE: `hide${EVENT_KEY}`,
HIDDEN: `hidden${EVENT_KEY}`,
SHOW: `show${EVENT_KEY}`,
SHOWN: `shown${EVENT_KEY}`,
INSERTED: `inserted${EVENT_KEY}`,
CLICK: `click${EVENT_KEY}`,
FOCUSIN: `focusin${EVENT_KEY}`,
FOCUSOUT: `focusout${EVENT_KEY}`,
MOUSEENTER: `mouseenter${EVENT_KEY}`,
MOUSELEAVE: `mouseleave${EVENT_KEY}`
const DefaultType = {
animation: 'boolean',
template: 'string',
title: '(string|element|function)',
trigger: 'string',
delay: '(number|object)',
html: 'boolean',
selector: '(string|boolean)',
placement: '(string|function)',
offset: '(array|string|function)',
container: '(string|element|boolean)',
fallbackPlacements: 'array',
boundary: '(string|element)',
customClass: '(string|function)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
allowList: 'object',
popperConfig: '(null|object|function)'
}
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_MODAL = 'modal'
const CLASS_NAME_SHOW = 'show'
const HOVER_STATE_SHOW = 'show'
const HOVER_STATE_OUT = 'out'
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
const EVENT_MODAL_HIDE = 'hide.bs.modal'
const TRIGGER_HOVER = 'hover'
const TRIGGER_FOCUS = 'focus'
const TRIGGER_CLICK = 'click'
const TRIGGER_MANUAL = 'manual'
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
* Class definition
*/
class Tooltip extends BaseComponent {
@@ -129,42 +106,36 @@ class Tooltip extends BaseComponent {
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
}
super(element)
super(element, config)
// private
// Private
this._isEnabled = true
this._timeout = 0
this._hoverState = ''
this._isHovered = false
this._activeTrigger = {}
this._popper = null
this._templateFactory = null
// Protected
this._config = this._getConfig(config)
this.tip = null
this._setListeners()
}
// Getters
static get Default() {
return Default
}
static get NAME() {
return NAME
}
static get Event() {
return Event
}
static get DefaultType() {
return DefaultType
}
// Public
static get NAME() {
return NAME
}
// Public
enable() {
this._isEnabled = true
}
@@ -188,18 +159,20 @@ class Tooltip extends BaseComponent {
context._activeTrigger.click = !context._activeTrigger.click
if (context._isWithActiveTrigger()) {
context._enter(null, context)
context._enter()
} else {
context._leave(null, context)
}
} else {
if (this.getTipElement().classList.contains(CLASS_NAME_SHOW)) {
this._leave(null, this)
return
context._leave()
}
this._enter(null, this)
return
}
if (this._isShown()) {
this._leave()
return
}
this._enter()
}
dispose() {
@@ -220,11 +193,11 @@ class Tooltip extends BaseComponent {
throw new Error('Please use show on visible elements')
}
if (!(this.isWithContent() && this._isEnabled)) {
if (!(this._isWithContent() && this._isEnabled)) {
return
}
const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW)
const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))
const shadowRoot = findShadowRoot(this._element)
const isInTheDom = shadowRoot === null ?
this._element.ownerDocument.documentElement.contains(this._element) :
@@ -234,227 +207,203 @@ class Tooltip extends BaseComponent {
return
}
// A trick to recreate a tooltip in case a new title is given by using the NOT documented `data-bs-original-title`
// This will be removed later in favor of a `setContent` method
if (this.constructor.NAME === 'tooltip' && this.tip && this.getTitle() !== this.tip.querySelector(SELECTOR_TOOLTIP_INNER).innerHTML) {
this._disposePopper()
this.tip.remove()
this.tip = null
}
const tip = this._getTipElement()
const tip = this.getTipElement()
const tipId = getUID(this.constructor.NAME)
tip.setAttribute('id', tipId)
this._element.setAttribute('aria-describedby', tipId)
if (this._config.animation) {
tip.classList.add(CLASS_NAME_FADE)
}
const placement = typeof this._config.placement === 'function' ?
this._config.placement.call(this, tip, this._element) :
this._config.placement
const attachment = this._getAttachment(placement)
this._addAttachmentClass(attachment)
this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
const { container } = this._config
Data.set(tip, this.constructor.DATA_KEY, this)
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.append(tip)
EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
}
if (this._popper) {
this._popper.update()
} else {
this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
this._createPopper(tip)
}
tip.classList.add(CLASS_NAME_SHOW)
const customClass = this._resolvePossibleFunction(this._config.customClass)
if (customClass) {
tip.classList.add(...customClass.split(' '))
}
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement) {
[].concat(...document.body.children).forEach(element => {
for (const element of [].concat(...document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
})
}
const complete = () => {
const prevHoverState = this._hoverState
this._hoverState = null
EventHandler.trigger(this._element, this.constructor.Event.SHOWN)
if (prevHoverState === HOVER_STATE_OUT) {
this._leave(null, this)
}
}
const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
this._queueCallback(complete, this.tip, isAnimated)
const complete = () => {
const previousHoverState = this._isHovered
this._isHovered = false
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))
if (previousHoverState) {
this._leave()
}
}
this._queueCallback(complete, this.tip, this._isAnimated())
}
hide() {
if (!this._popper) {
if (!this._isShown()) {
return
}
const tip = this.getTipElement()
const complete = () => {
if (this._isWithActiveTrigger()) {
return
}
if (this._hoverState !== HOVER_STATE_SHOW) {
tip.remove()
}
this._cleanTipClass()
this._element.removeAttribute('aria-describedby')
EventHandler.trigger(this._element, this.constructor.Event.HIDDEN)
this._disposePopper()
}
const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE)
const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))
if (hideEvent.defaultPrevented) {
return
}
const tip = this._getTipElement()
tip.classList.remove(CLASS_NAME_SHOW)
// If this is a touch-enabled device we remove the extra
// empty mouseover listeners we added for iOS support
if ('ontouchstart' in document.documentElement) {
[].concat(...document.body.children)
.forEach(element => EventHandler.off(element, 'mouseover', noop))
for (const element of [].concat(...document.body.children)) {
EventHandler.off(element, 'mouseover', noop)
}
}
this._activeTrigger[TRIGGER_CLICK] = false
this._activeTrigger[TRIGGER_FOCUS] = false
this._activeTrigger[TRIGGER_HOVER] = false
this._isHovered = false
const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
this._queueCallback(complete, this.tip, isAnimated)
this._hoverState = ''
const complete = () => {
if (this._isWithActiveTrigger()) {
return
}
if (!this._isHovered) {
tip.remove()
}
this._element.removeAttribute('aria-describedby')
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))
this._disposePopper()
}
this._queueCallback(complete, this.tip, this._isAnimated())
}
update() {
if (this._popper !== null) {
if (this._popper) {
this._popper.update()
}
}
// Protected
isWithContent() {
return Boolean(this.getTitle())
_isWithContent() {
return Boolean(this._getTitle())
}
getTipElement() {
if (this.tip) {
return this.tip
_getTipElement() {
if (!this.tip) {
this.tip = this._createTipElement(this._getContentForTemplate())
}
const element = document.createElement('div')
element.innerHTML = this._config.template
const tip = element.children[0]
this.setContent(tip)
tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
this.tip = tip
return this.tip
}
setContent(tip) {
this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER)
_createTipElement(content) {
const tip = this._getTemplateFactory(content).toHtml()
// todo: remove this check on v6
if (!tip) {
return null
}
tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
// todo: on v6 the following can be achieved with CSS only
tip.classList.add(`bs-${this.constructor.NAME}-auto`)
const tipId = getUID(this.constructor.NAME).toString()
tip.setAttribute('id', tipId)
if (this._isAnimated()) {
tip.classList.add(CLASS_NAME_FADE)
}
return tip
}
_sanitizeAndSetContent(template, content, selector) {
const templateElement = SelectorEngine.findOne(selector, template)
if (!content && templateElement) {
templateElement.remove()
return
setContent(content) {
let isShown = false
if (this.tip) {
isShown = this._isShown()
this.tip.remove()
this.tip = null
}
// we use append for html objects to maintain js events
this.setElementContent(templateElement, content)
this._disposePopper()
this.tip = this._createTipElement(content)
if (isShown) {
this.show()
}
}
setElementContent(element, content) {
if (element === null) {
return
}
if (isElement(content)) {
content = getElement(content)
// content is a DOM node or a jQuery
if (this._config.html) {
if (content.parentNode !== element) {
element.innerHTML = ''
element.append(content)
}
} else {
element.textContent = content.textContent
}
return
}
if (this._config.html) {
if (this._config.sanitize) {
content = sanitizeHtml(content, this._config.allowList, this._config.sanitizeFn)
}
element.innerHTML = content
_getTemplateFactory(content) {
if (this._templateFactory) {
this._templateFactory.changeContent(content)
} else {
element.textContent = content
this._templateFactory = new TemplateFactory({
...this._config,
// the `content` var has to be after `this._config`
// to override config.content in case of popover
content,
extraClass: this._resolvePossibleFunction(this._config.customClass)
})
}
return this._templateFactory
}
_getContentForTemplate() {
return {
[SELECTOR_TOOLTIP_INNER]: this._getTitle()
}
}
getTitle() {
const title = this._element.getAttribute('data-bs-original-title') || this._config.title
return this._resolvePossibleFunction(title)
}
updateAttachment(attachment) {
if (attachment === 'right') {
return 'end'
}
if (attachment === 'left') {
return 'start'
}
return attachment
_getTitle() {
return this._config.title
}
// Private
_initializeOnDelegatedTarget(event) {
return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
}
_initializeOnDelegatedTarget(event, context) {
return context || this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
_isAnimated() {
return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))
}
_isShown() {
return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)
}
_createPopper(tip) {
const placement = typeof this._config.placement === 'function' ?
this._config.placement.call(this, tip, this._element) :
this._config.placement
const attachment = AttachmentMap[placement.toUpperCase()]
this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
}
_getOffset() {
const { offset } = this._config
if (typeof offset === 'string') {
return offset.split(',').map(val => Number.parseInt(val, 10))
return offset.split(',').map(value => Number.parseInt(value, 10))
}
if (typeof offset === 'function') {
@@ -464,8 +413,8 @@ class Tooltip extends BaseComponent {
return offset
}
_resolvePossibleFunction(content) {
return typeof content === 'function' ? content.call(this._element) : content
_resolvePossibleFunction(arg) {
return typeof arg === 'function' ? arg.call(this._element) : arg
}
_getPopperConfig(attachment) {
@@ -497,17 +446,16 @@ class Tooltip extends BaseComponent {
}
},
{
name: 'onChange',
name: 'preSetPlacement',
enabled: true,
phase: 'afterWrite',
fn: data => this._handlePopperPlacementChange(data)
phase: 'beforeMain',
fn: data => {
// Pre-set Popper's placement attribute in order to read the arrow sizes properly.
// Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
this._getTipElement().setAttribute('data-popper-placement', data.state.placement)
}
}
],
onFirstUpdate: data => {
if (data.options.placement !== data.placement) {
this._handlePopperPlacementChange(data)
}
}
]
}
return {
@@ -516,32 +464,34 @@ class Tooltip extends BaseComponent {
}
}
_addAttachmentClass(attachment) {
this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(attachment)}`)
}
_getAttachment(placement) {
return AttachmentMap[placement.toUpperCase()]
}
_setListeners() {
const triggers = this._config.trigger.split(' ')
triggers.forEach(trigger => {
for (const trigger of triggers) {
if (trigger === 'click') {
EventHandler.on(this._element, this.constructor.Event.CLICK, this._config.selector, event => this.toggle(event))
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => this.toggle(event))
} else if (trigger !== TRIGGER_MANUAL) {
const eventIn = trigger === TRIGGER_HOVER ?
this.constructor.Event.MOUSEENTER :
this.constructor.Event.FOCUSIN
this.constructor.eventName(EVENT_MOUSEENTER) :
this.constructor.eventName(EVENT_FOCUSIN)
const eventOut = trigger === TRIGGER_HOVER ?
this.constructor.Event.MOUSELEAVE :
this.constructor.Event.FOCUSOUT
this.constructor.eventName(EVENT_MOUSELEAVE) :
this.constructor.eventName(EVENT_FOCUSOUT)
EventHandler.on(this._element, eventIn, this._config.selector, event => this._enter(event))
EventHandler.on(this._element, eventOut, this._config.selector, event => this._leave(event))
EventHandler.on(this._element, eventIn, this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event)
context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true
context._enter()
})
EventHandler.on(this._element, eventOut, this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event)
context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =
context._element.contains(event.relatedTarget)
context._leave()
})
}
})
}
this._hideModalHandler = () => {
if (this._element) {
@@ -563,103 +513,77 @@ class Tooltip extends BaseComponent {
}
_fixTitle() {
const title = this._element.getAttribute('title')
const originalTitleType = typeof this._element.getAttribute('data-bs-original-title')
const title = this._config.originalTitle
if (title || originalTitleType !== 'string') {
this._element.setAttribute('data-bs-original-title', title || '')
if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) {
this._element.setAttribute('aria-label', title)
}
this._element.setAttribute('title', '')
if (!title) {
return
}
if (!this._element.getAttribute('aria-label') && !this._element.textContent) {
this._element.setAttribute('aria-label', title)
}
this._element.removeAttribute('title')
}
_enter(event, context) {
context = this._initializeOnDelegatedTarget(event, context)
if (event) {
context._activeTrigger[
event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER
] = true
}
if (context.getTipElement().classList.contains(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {
context._hoverState = HOVER_STATE_SHOW
_enter() {
if (this._isShown() || this._isHovered) {
this._isHovered = true
return
}
clearTimeout(context._timeout)
this._isHovered = true
context._hoverState = HOVER_STATE_SHOW
if (!context._config.delay || !context._config.delay.show) {
context.show()
return
}
context._timeout = setTimeout(() => {
if (context._hoverState === HOVER_STATE_SHOW) {
context.show()
this._setTimeout(() => {
if (this._isHovered) {
this.show()
}
}, context._config.delay.show)
}, this._config.delay.show)
}
_leave(event, context) {
context = this._initializeOnDelegatedTarget(event, context)
if (event) {
context._activeTrigger[
event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
] = context._element.contains(event.relatedTarget)
}
if (context._isWithActiveTrigger()) {
_leave() {
if (this._isWithActiveTrigger()) {
return
}
clearTimeout(context._timeout)
this._isHovered = false
context._hoverState = HOVER_STATE_OUT
if (!context._config.delay || !context._config.delay.hide) {
context.hide()
return
}
context._timeout = setTimeout(() => {
if (context._hoverState === HOVER_STATE_OUT) {
context.hide()
this._setTimeout(() => {
if (!this._isHovered) {
this.hide()
}
}, context._config.delay.hide)
}, this._config.delay.hide)
}
_setTimeout(handler, timeout) {
clearTimeout(this._timeout)
this._timeout = setTimeout(handler, timeout)
}
_isWithActiveTrigger() {
for (const trigger in this._activeTrigger) {
if (this._activeTrigger[trigger]) {
return true
}
}
return false
return Object.values(this._activeTrigger).includes(true)
}
_getConfig(config) {
const dataAttributes = Manipulator.getDataAttributes(this._element)
Object.keys(dataAttributes).forEach(dataAttr => {
if (DISALLOWED_ATTRIBUTES.has(dataAttr)) {
delete dataAttributes[dataAttr]
for (const dataAttribute of Object.keys(dataAttributes)) {
if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
delete dataAttributes[dataAttribute]
}
})
}
config = {
...this.constructor.Default,
...dataAttributes,
...(typeof config === 'object' && config ? config : {})
}
config = this._mergeConfigObj(config)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
_configAfterMerge(config) {
config.container = config.container === false ? document.body : getElement(config.container)
if (typeof config.delay === 'number') {
@@ -669,6 +593,8 @@ class Tooltip extends BaseComponent {
}
}
config.originalTitle = this._element.getAttribute('title') || ''
config.title = this._resolvePossibleFunction(config.title) || config.originalTitle
if (typeof config.title === 'number') {
config.title = config.title.toString()
}
@@ -677,12 +603,6 @@ class Tooltip extends BaseComponent {
config.content = config.content.toString()
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
if (config.sanitize) {
config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn)
}
return config
}
@@ -701,32 +621,6 @@ class Tooltip extends BaseComponent {
return config
}
_cleanTipClass() {
const tip = this.getTipElement()
const basicClassPrefixRegex = new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`, 'g')
const tabClass = tip.getAttribute('class').match(basicClassPrefixRegex)
if (tabClass !== null && tabClass.length > 0) {
tabClass.map(token => token.trim())
.forEach(tClass => tip.classList.remove(tClass))
}
}
_getBasicClassPrefix() {
return CLASS_PREFIX
}
_handlePopperPlacementChange(popperData) {
const { state } = popperData
if (!state) {
return
}
this.tip = state.elements.popper
this._cleanTipClass()
this._addAttachmentClass(this._getAttachment(state.placement))
}
_disposePopper() {
if (this._popper) {
this._popper.destroy()
@@ -735,27 +629,25 @@ class Tooltip extends BaseComponent {
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Tooltip.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Tooltip to jQuery only if jQuery is present
*/
defineJQueryPlugin(Tooltip)
+48 -29
View File
@@ -6,7 +6,17 @@
*/
import EventHandler from '../dom/event-handler'
import { execute, executeAfterTransition, getElement, reflow, typeCheckConfig } from './index'
import { execute, executeAfterTransition, getElement, reflow } from './index'
import Config from './config'
/**
* Constants
*/
const NAME = 'backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
const Default = {
className: 'modal-backdrop',
@@ -23,19 +33,33 @@ const DefaultType = {
rootElement: '(element|string)',
clickCallback: '(function|null)'
}
const NAME = 'backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
/**
* Class definition
*/
class Backdrop {
class Backdrop extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
this._isAppended = false
this._element = null
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
show(callback) {
if (!this._config.isVisible) {
execute(callback)
@@ -44,11 +68,12 @@ class Backdrop {
this._append()
const element = this._getElement()
if (this._config.isAnimated) {
reflow(this._getElement())
reflow(element)
}
this._getElement().classList.add(CLASS_NAME_SHOW)
element.classList.add(CLASS_NAME_SHOW)
this._emulateAnimation(() => {
execute(callback)
@@ -69,8 +94,18 @@ class Backdrop {
})
}
// Private
dispose() {
if (!this._isAppended) {
return
}
EventHandler.off(this._element, EVENT_MOUSEDOWN)
this._element.remove()
this._isAppended = false
}
// Private
_getElement() {
if (!this._element) {
const backdrop = document.createElement('div')
@@ -85,15 +120,9 @@ class Backdrop {
return this._element
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
_configAfterMerge(config) {
// use getElement() with the default "body" to get a fresh Element on each instantiation
config.rootElement = getElement(config.rootElement)
typeCheckConfig(NAME, config, DefaultType)
return config
}
@@ -102,26 +131,16 @@ class Backdrop {
return
}
this._config.rootElement.append(this._getElement())
const element = this._getElement()
this._config.rootElement.append(element)
EventHandler.on(this._getElement(), EVENT_MOUSEDOWN, () => {
EventHandler.on(element, EVENT_MOUSEDOWN, () => {
execute(this._config.clickCallback)
})
this._isAppended = true
}
dispose() {
if (!this._isAppended) {
return
}
EventHandler.off(this._element, EVENT_MOUSEDOWN)
this._element.remove()
this._isAppended = false
}
_emulateAnimation(callback) {
executeAfterTransition(callback, this._getElement(), this._config.isAnimated)
}
+63
View File
@@ -0,0 +1,63 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.1.3): util/config.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { isElement, toType } from './index'
import Manipulator from '../dom/manipulator'
/**
* 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) {
return {
...this.constructor.Default,
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
...(typeof config === 'object' ? config : {})
}
}
_typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
for (const property of Object.keys(configTypes)) {
const expectedTypes = configTypes[property]
const value = config[property]
const valueType = isElement(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}".`
)
}
}
}
}
export default Config
+37 -27
View File
@@ -7,17 +7,11 @@
import EventHandler from '../dom/event-handler'
import SelectorEngine from '../dom/selector-engine'
import { typeCheckConfig } from './index'
import Config from './config'
const Default = {
trapElement: null, // The element to trap focus inside of
autofocus: true
}
const DefaultType = {
trapElement: 'element',
autofocus: 'boolean'
}
/**
* Constants
*/
const NAME = 'focustrap'
const DATA_KEY = 'bs.focustrap'
@@ -29,22 +23,49 @@ const TAB_KEY = 'Tab'
const TAB_NAV_FORWARD = 'forward'
const TAB_NAV_BACKWARD = 'backward'
class FocusTrap {
const Default = {
trapElement: null, // The element to trap focus inside of
autofocus: true
}
const DefaultType = {
trapElement: 'element',
autofocus: 'boolean'
}
/**
* Class definition
*/
class FocusTrap extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
this._isActive = false
this._lastTabNavDirection = null
}
activate() {
const { trapElement, autofocus } = this._config
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
activate() {
if (this._isActive) {
return
}
if (autofocus) {
trapElement.focus()
if (this._config.autofocus) {
this._config.trapElement.focus()
}
EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop
@@ -64,12 +85,10 @@ class FocusTrap {
}
// Private
_handleFocusin(event) {
const { target } = event
const { trapElement } = this._config
if (target === document || target === trapElement || trapElement.contains(target)) {
if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {
return
}
@@ -91,15 +110,6 @@ class FocusTrap {
this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
}
export default FocusTrap
+70 -67
View File
@@ -5,23 +5,21 @@
* --------------------------------------------------------------------------
*/
const MAX_UID = 1000000
const MAX_UID = 1_000_000
const MILLISECONDS_MULTIPLIER = 1000
const TRANSITION_END = 'transitionend'
// Shoutout AngusCroll (https://goo.gl/pxwQGp)
const toType = obj => {
if (obj === null || obj === undefined) {
return `${obj}`
const toType = object => {
if (object === null || object === undefined) {
return `${object}`
}
return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase()
return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase()
}
/**
* --------------------------------------------------------------------------
* Public Util Api
* --------------------------------------------------------------------------
* Public Util API
*/
const getUID = prefix => {
@@ -36,22 +34,22 @@ const getSelector = element => {
let selector = element.getAttribute('data-bs-target')
if (!selector || selector === '#') {
let hrefAttr = element.getAttribute('href')
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 (!hrefAttr || (!hrefAttr.includes('#') && !hrefAttr.startsWith('.'))) {
if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
return null
}
// Just in case some CMS puts out a full URL with the anchor appended
if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
hrefAttr = `#${hrefAttr.split('#')[1]}`
if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
hrefAttribute = `#${hrefAttribute.split('#')[1]}`
}
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null
selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
}
return selector
@@ -100,50 +98,56 @@ const triggerTransitionEnd = element => {
element.dispatchEvent(new Event(TRANSITION_END))
}
const isElement = obj => {
if (!obj || typeof obj !== 'object') {
const isElement = object => {
if (!object || typeof object !== 'object') {
return false
}
if (typeof obj.jquery !== 'undefined') {
obj = obj[0]
if (typeof object.jquery !== 'undefined') {
object = object[0]
}
return typeof obj.nodeType !== 'undefined'
return typeof object.nodeType !== 'undefined'
}
const getElement = obj => {
if (isElement(obj)) { // it's a jQuery object or a node element
return obj.jquery ? obj[0] : obj
const getElement = object => {
// it's a jQuery object or a node element
if (isElement(object)) {
return object.jquery ? object[0] : object
}
if (typeof obj === 'string' && obj.length > 0) {
return document.querySelector(obj)
if (typeof object === 'string' && object.length > 0) {
return document.querySelector(object)
}
return null
}
const typeCheckConfig = (componentName, config, configTypes) => {
Object.keys(configTypes).forEach(property => {
const expectedTypes = configTypes[property]
const value = config[property]
const valueType = value && isElement(value) ? 'element' : toType(value)
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(
`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
)
}
})
}
const isVisible = element => {
if (!isElement(element) || element.getClientRects().length === 0) {
return false
}
return getComputedStyle(element).getPropertyValue('visibility') === 'visible'
const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'
// Handle `details` element as its content may falsie 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 => {
@@ -196,15 +200,12 @@ const noop = () => {}
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = element => {
// eslint-disable-next-line no-unused-expressions
element.offsetHeight
element.offsetHeight // eslint-disable-line no-unused-expressions
}
const getjQuery = () => {
const { jQuery } = window
if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return jQuery
if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return window.jQuery
}
return null
@@ -217,7 +218,9 @@ const onDOMContentLoaded = callback => {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
DOMContentLoadedCallbacks.forEach(callback => callback())
for (const callback of DOMContentLoadedCallbacks) {
callback()
}
})
}
@@ -291,15 +294,15 @@ const executeAfterTransition = (callback, transitionElement, waitForTransition =
* @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 the element does not exist in the list return an element
// depending on the direction and if cycle is allowed
if (index === -1) {
return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]
}
const listLength = list.length
index += shouldGetNext ? 1 : -1
if (isCycleAllowed) {
@@ -310,24 +313,24 @@ const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed
}
export {
getElement,
getUID,
getSelectorFromElement,
getElementFromSelector,
getTransitionDurationFromElement,
triggerTransitionEnd,
isElement,
typeCheckConfig,
isVisible,
isDisabled,
findShadowRoot,
noop,
getNextActiveElement,
reflow,
getjQuery,
onDOMContentLoaded,
isRTL,
defineJQueryPlugin,
execute,
executeAfterTransition
executeAfterTransition,
findShadowRoot,
getElement,
getElementFromSelector,
getjQuery,
getNextActiveElement,
getSelectorFromElement,
getTransitionDurationFromElement,
getUID,
isDisabled,
isElement,
isRTL,
isVisible,
noop,
onDOMContentLoaded,
reflow,
triggerTransitionEnd,
toType
}
+8 -16
View File
@@ -43,16 +43,9 @@ const allowedAttribute = (attribute, allowedAttributeList) => {
return true
}
const regExp = allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
// Check if a regular expression validates the attribute.
for (let i = 0, len = regExp.length; i < len; i++) {
if (regExp[i].test(attributeName)) {
return true
}
}
return false
return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
.some(regex => regex.test(attributeName))
}
export const DefaultAllowlist = {
@@ -89,21 +82,20 @@ export const DefaultAllowlist = {
ul: []
}
export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) {
export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
if (!unsafeHtml.length) {
return unsafeHtml
}
if (sanitizeFn && typeof sanitizeFn === 'function') {
return sanitizeFn(unsafeHtml)
if (sanitizeFunction && typeof sanitizeFunction === 'function') {
return sanitizeFunction(unsafeHtml)
}
const domParser = new window.DOMParser()
const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
const elements = [].concat(...createdDocument.body.querySelectorAll('*'))
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i]
for (const element of elements) {
const elementName = element.nodeName.toLowerCase()
if (!Object.keys(allowList).includes(elementName)) {
@@ -115,11 +107,11 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) {
const attributeList = [].concat(...element.attributes)
const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])
attributeList.forEach(attribute => {
for (const attribute of attributeList) {
if (!allowedAttribute(attribute, allowedAttributes)) {
element.removeAttribute(attribute.nodeName)
}
})
}
}
return createdDocument.body.innerHTML
+46 -29
View File
@@ -9,14 +9,25 @@ import SelectorEngine from '../dom/selector-engine'
import Manipulator from '../dom/manipulator'
import { isElement } from './index'
/**
* Constants
*/
const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
const SELECTOR_STICKY_CONTENT = '.sticky-top'
const PROPERTY_PADDING = 'padding-right'
const PROPERTY_MARGIN = 'margin-right'
/**
* Class definition
*/
class ScrollBarHelper {
constructor() {
this._element = document.body
}
// Public
getWidth() {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = document.documentElement.clientWidth
@@ -27,55 +38,62 @@ class ScrollBarHelper {
const width = this.getWidth()
this._disableOverFlow()
// give padding to element to balance the hidden scrollbar width
this._setElementAttributes(this._element, 'paddingRight', calculatedValue => calculatedValue + width)
this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)
// trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth
this._setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width)
this._setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width)
this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)
this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)
}
reset() {
this._resetElementAttributes(this._element, 'overflow')
this._resetElementAttributes(this._element, PROPERTY_PADDING)
this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING)
this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN)
}
isOverflowing() {
return this.getWidth() > 0
}
// Private
_disableOverFlow() {
this._saveInitialAttribute(this._element, 'overflow')
this._element.style.overflow = 'hidden'
}
_setElementAttributes(selector, styleProp, callback) {
_setElementAttributes(selector, styleProperty, callback) {
const scrollbarWidth = this.getWidth()
const manipulationCallBack = element => {
if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {
return
}
this._saveInitialAttribute(element, styleProp)
const calculatedValue = window.getComputedStyle(element)[styleProp]
element.style[styleProp] = `${callback(Number.parseFloat(calculatedValue))}px`
this._saveInitialAttribute(element, styleProperty)
const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)
element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)
}
this._applyManipulationCallback(selector, manipulationCallBack)
}
reset() {
this._resetElementAttributes(this._element, 'overflow')
this._resetElementAttributes(this._element, 'paddingRight')
this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')
this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')
}
_saveInitialAttribute(element, styleProp) {
const actualValue = element.style[styleProp]
_saveInitialAttribute(element, styleProperty) {
const actualValue = element.style.getPropertyValue(styleProperty)
if (actualValue) {
Manipulator.setDataAttribute(element, styleProp, actualValue)
Manipulator.setDataAttribute(element, styleProperty, actualValue)
}
}
_resetElementAttributes(selector, styleProp) {
_resetElementAttributes(selector, styleProperty) {
const manipulationCallBack = element => {
const value = Manipulator.getDataAttribute(element, styleProp)
if (typeof value === 'undefined') {
element.style.removeProperty(styleProp)
} else {
Manipulator.removeDataAttribute(element, styleProp)
element.style[styleProp] = value
const value = Manipulator.getDataAttribute(element, styleProperty)
// We only want to remove the property if the value is `null`; the value can also be zero
if (value === null) {
element.style.removeProperty(styleProperty)
return
}
Manipulator.removeDataAttribute(element, styleProperty)
element.style.setProperty(styleProperty, value)
}
this._applyManipulationCallback(selector, manipulationCallBack)
@@ -84,13 +102,12 @@ class ScrollBarHelper {
_applyManipulationCallback(selector, callBack) {
if (isElement(selector)) {
callBack(selector)
} else {
SelectorEngine.find(selector, this._element).forEach(callBack)
return
}
}
isOverflowing() {
return this.getWidth() > 0
for (const sel of SelectorEngine.find(selector, this._element)) {
callBack(sel)
}
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.1.3): util/swipe.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Config from './config'
import EventHandler from '../dom/event-handler'
import { execute } from './index'
/**
* Constants
*/
const NAME = 'swipe'
const EVENT_KEY = '.bs.swipe'
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
const POINTER_TYPE_TOUCH = 'touch'
const POINTER_TYPE_PEN = 'pen'
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
const SWIPE_THRESHOLD = 40
const Default = {
leftCallback: null,
rightCallback: null,
endCallback: null
}
const DefaultType = {
leftCallback: '(function|null)',
rightCallback: '(function|null)',
endCallback: '(function|null)'
}
/**
* Class definition
*/
class Swipe extends Config {
constructor(element, config) {
super()
this._element = element
if (!element || !Swipe.isSupported()) {
return
}
this._config = this._getConfig(config)
this._deltaX = 0
this._supportPointerEvents = Boolean(window.PointerEvent)
this._initEvents()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
dispose() {
EventHandler.off(this._element, EVENT_KEY)
}
// Private
_start(event) {
if (!this._supportPointerEvents) {
this._deltaX = event.touches[0].clientX
return
}
if (this._eventIsPointerPenTouch(event)) {
this._deltaX = event.clientX
}
}
_end(event) {
if (this._eventIsPointerPenTouch(event)) {
this._deltaX = event.clientX - this._deltaX
}
this._handleSwipe()
execute(this._config.endCallback)
}
_move(event) {
this._deltaX = event.touches && event.touches.length > 1 ?
0 :
event.touches[0].clientX - this._deltaX
}
_handleSwipe() {
const absDeltaX = Math.abs(this._deltaX)
if (absDeltaX <= SWIPE_THRESHOLD) {
return
}
const direction = absDeltaX / this._deltaX
this._deltaX = 0
if (!direction) {
return
}
execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)
}
_initEvents() {
if (this._supportPointerEvents) {
EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))
EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))
this._element.classList.add(CLASS_NAME_POINTER_EVENT)
} else {
EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))
EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))
EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))
}
}
_eventIsPointerPenTouch(event) {
return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
}
// Static
static isSupported() {
return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
}
}
export default Swipe
+160
View File
@@ -0,0 +1,160 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.1.3): util/template-factory.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { DefaultAllowlist, sanitizeHtml } from './sanitizer'
import { getElement, isElement } from '../util/index'
import SelectorEngine from '../dom/selector-engine'
import Config from './config'
/**
* Constants
*/
const NAME = 'TemplateFactory'
const Default = {
extraClass: '',
template: '<div></div>',
content: {}, // { selector : text , selector2 : text2 , }
html: false,
sanitize: true,
sanitizeFn: null,
allowList: DefaultAllowlist
}
const DefaultType = {
extraClass: '(string|function)',
template: 'string',
content: 'object',
html: 'boolean',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
allowList: 'object'
}
const DefaultContentType = {
selector: '(string|element)',
entry: '(string|element|function|null)'
}
/**
* Class definition
*/
class TemplateFactory extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
getContent() {
return Object.values(this._config.content)
.map(config => this._resolvePossibleFunction(config))
.filter(Boolean)
}
hasContent() {
return this.getContent().length > 0
}
changeContent(content) {
this._checkContent(content)
this._config.content = { ...this._config.content, ...content }
return this
}
toHtml() {
const templateWrapper = document.createElement('div')
templateWrapper.innerHTML = this._maybeSanitize(this._config.template)
for (const [selector, text] of Object.entries(this._config.content)) {
this._setContent(templateWrapper, text, selector)
}
const template = templateWrapper.children[0]
const extraClass = this._resolvePossibleFunction(this._config.extraClass)
if (extraClass) {
template.classList.add(...extraClass.split(' '))
}
return template
}
// Private
_typeCheckConfig(config) {
super._typeCheckConfig(config)
this._checkContent(config.content)
}
_checkContent(arg) {
for (const [selector, content] of Object.entries(arg)) {
super._typeCheckConfig({ selector, entry: content }, DefaultContentType)
}
}
_setContent(template, content, selector) {
const templateElement = SelectorEngine.findOne(selector, template)
if (!templateElement) {
return
}
content = this._resolvePossibleFunction(content)
if (!content) {
templateElement.remove()
return
}
if (isElement(content)) {
this._putElementInTemplate(getElement(content), templateElement)
return
}
if (this._config.html) {
templateElement.innerHTML = this._maybeSanitize(content)
return
}
templateElement.textContent = content
}
_maybeSanitize(arg) {
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg
}
_resolvePossibleFunction(arg) {
return typeof arg === 'function' ? arg(this) : arg
}
_putElementInTemplate(element, templateElement) {
if (this._config.html) {
templateElement.innerHTML = ''
templateElement.append(element)
return
}
templateElement.textContent = element.textContent
}
}
export default TemplateFactory
+18 -16
View File
@@ -35,7 +35,7 @@ Currently we're aiming for at least 90% test coverage for our code. To ensure yo
describe('getInstance', () => {
it('should return null if there is no instance', () => {
// Make assertion
expect(Tab.getInstance(fixtureEl)).toEqual(null)
expect(Tab.getInstance(fixtureEl)).toBeNull()
})
it('should return this instance', () => {
@@ -50,22 +50,24 @@ describe('getInstance', () => {
})
// Asynchronous test
it('should show a tooltip without the animation', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
it('should show a tooltip without the animation', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl, {
animation: false
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl, {
animation: false
})
tooltipEl.addEventListener('shown.bs.tooltip', () => {
const tip = document.querySelector('.tooltip')
expect(tip).not.toBeNull()
expect(tip.classList.contains('fade')).toEqual(false)
resolve()
})
tooltip.show()
})
tooltipEl.addEventListener('shown.bs.tooltip', () => {
const tip = document.querySelector('.tooltip')
expect(tip).not.toBeNull()
expect(tip.classList.contains('fade')).toEqual(false)
done()
})
tooltip.show()
})
```
+1 -4
View File
@@ -74,9 +74,6 @@ const browsers = {
}
}
const browsersKeys = Object.keys(browsers)
module.exports = {
browsers,
browsersKeys
browsers
}
+22 -25
View File
@@ -1,50 +1,47 @@
const fixtureId = 'fixture'
export const getFixture = () => {
let fixtureEl = document.getElementById(fixtureId)
let fixtureElement = document.getElementById(fixtureId)
if (!fixtureEl) {
fixtureEl = document.createElement('div')
fixtureEl.setAttribute('id', fixtureId)
fixtureEl.style.position = 'absolute'
fixtureEl.style.top = '-10000px'
fixtureEl.style.left = '-10000px'
fixtureEl.style.width = '10000px'
fixtureEl.style.height = '10000px'
document.body.append(fixtureEl)
if (!fixtureElement) {
fixtureElement = document.createElement('div')
fixtureElement.setAttribute('id', fixtureId)
fixtureElement.style.position = 'absolute'
fixtureElement.style.top = '-10000px'
fixtureElement.style.left = '-10000px'
fixtureElement.style.width = '10000px'
fixtureElement.style.height = '10000px'
document.body.append(fixtureElement)
}
return fixtureEl
return fixtureElement
}
export const clearFixture = () => {
const fixtureEl = getFixture()
const fixtureElement = getFixture()
fixtureEl.innerHTML = ''
fixtureElement.innerHTML = ''
}
export const createEvent = (eventName, params = {}) => {
const event = document.createEvent('Event')
event.initEvent(eventName, Boolean(params.bubbles), Boolean(params.cancelable))
return event
export const createEvent = (eventName, parameters = {}) => {
return new Event(eventName, parameters)
}
export const jQueryMock = {
elements: undefined,
fn: {},
each(fn) {
this.elements.forEach(el => {
fn.call(el)
})
for (const element of this.elements) {
fn.call(element)
}
}
}
export const clearBodyAndDocument = () => {
const attributes = ['data-bs-padding-right', 'style']
attributes.forEach(attr => {
document.documentElement.removeAttribute(attr)
document.body.removeAttribute(attr)
})
for (const attribute of attributes) {
document.documentElement.removeAttribute(attribute)
document.body.removeAttribute(attribute)
}
}
+18 -22
View File
@@ -8,11 +8,7 @@ const { babel } = require('@rollup/plugin-babel')
const istanbul = require('rollup-plugin-istanbul')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const replace = require('@rollup/plugin-replace')
const {
browsers,
browsersKeys
} = require('./browsers')
const { browsers } = require('./browsers')
const ENV = process.env
const BROWSERSTACK = Boolean(ENV.BROWSERSTACK)
@@ -54,7 +50,7 @@ const detectBrowsers = {
}
}
const conf = {
const config = {
basePath: '../..',
port: 9876,
colors: true,
@@ -105,8 +101,8 @@ const conf = {
}
if (BROWSERSTACK) {
conf.hostname = ip.address()
conf.browserStack = {
config.hostname = ip.address()
config.browserStack = {
username: ENV.BROWSER_STACK_USERNAME,
accessKey: ENV.BROWSER_STACK_ACCESS_KEY,
build: `bootstrap-${ENV.GITHUB_SHA ? ENV.GITHUB_SHA.slice(0, 7) + '-' : ''}${new Date().toISOString()}`,
@@ -114,8 +110,8 @@ if (BROWSERSTACK) {
retryLimit: 2
}
plugins.push('karma-browserstack-launcher', 'karma-jasmine-html-reporter')
conf.customLaunchers = browsers
conf.browsers = browsersKeys
config.customLaunchers = browsers
config.browsers = Object.keys(browsers)
reporters.push('BrowserStack', 'kjhtml')
} else if (JQUERY_TEST) {
frameworks.push('detectBrowsers')
@@ -124,8 +120,8 @@ if (BROWSERSTACK) {
'karma-firefox-launcher',
'karma-detect-browsers'
)
conf.detectBrowsers = detectBrowsers
conf.files = [
config.detectBrowsers = detectBrowsers
config.files = [
'node_modules/jquery/dist/jquery.slim.min.js',
{
pattern: 'js/tests/unit/jquery.spec.js',
@@ -141,8 +137,8 @@ if (BROWSERSTACK) {
'karma-coverage-istanbul-reporter'
)
reporters.push('coverage-istanbul')
conf.detectBrowsers = detectBrowsers
conf.coverageIstanbulReporter = {
config.detectBrowsers = detectBrowsers
config.coverageIstanbulReporter = {
dir: path.resolve(__dirname, '../coverage/'),
reports: ['lcov', 'text-summary'],
thresholds: {
@@ -157,19 +153,19 @@ if (BROWSERSTACK) {
}
if (DEBUG) {
conf.hostname = ip.address()
config.hostname = ip.address()
plugins.push('karma-jasmine-html-reporter')
reporters.push('kjhtml')
conf.singleRun = false
conf.autoWatch = true
config.singleRun = false
config.autoWatch = true
}
}
conf.frameworks = frameworks
conf.plugins = plugins
conf.reporters = reporters
config.frameworks = frameworks
config.plugins = plugins
config.reporters = reporters
module.exports = karmaConfig => {
conf.logLevel = karmaConfig.LOG_ERROR
karmaConfig.set(conf)
config.logLevel = karmaConfig.LOG_ERROR
karmaConfig.set(config)
}
+80 -48
View File
@@ -1,7 +1,5 @@
import Alert from '../../src/alert'
import { getTransitionDurationFromElement } from '../../src/util/index'
/** Test helpers */
import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture'
describe('Alert', () => {
@@ -27,7 +25,7 @@ describe('Alert', () => {
})
it('should return version', () => {
expect(typeof Alert.VERSION).toEqual('string')
expect(Alert.VERSION).toEqual(jasmine.any(String))
})
describe('DATA_KEY', () => {
@@ -47,7 +45,7 @@ describe('Alert', () => {
const button = document.querySelector('button')
button.click()
expect(document.querySelectorAll('.alert').length).toEqual(0)
expect(document.querySelectorAll('.alert')).toHaveSize(0)
})
it('should close an alert without instantiating it manually with the parent selector', () => {
@@ -60,65 +58,71 @@ describe('Alert', () => {
const button = document.querySelector('button')
button.click()
expect(document.querySelectorAll('.alert').length).toEqual(0)
expect(document.querySelectorAll('.alert')).toHaveSize(0)
})
})
describe('close', () => {
it('should close an alert', done => {
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
fixtureEl.innerHTML = '<div class="alert"></div>'
it('should close an alert', () => {
return new Promise(resolve => {
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = document.querySelector('.alert')
const alert = new Alert(alertEl)
const alertEl = document.querySelector('.alert')
const alert = new Alert(alertEl)
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert').length).toEqual(0)
expect(spy).not.toHaveBeenCalled()
done()
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert')).toHaveSize(0)
expect(spy).not.toHaveBeenCalled()
resolve()
})
alert.close()
})
alert.close()
})
it('should close alert with fade class', done => {
fixtureEl.innerHTML = '<div class="alert fade"></div>'
it('should close alert with fade class', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="alert fade"></div>'
const alertEl = document.querySelector('.alert')
const alert = new Alert(alertEl)
const alertEl = document.querySelector('.alert')
const alert = new Alert(alertEl)
alertEl.addEventListener('transitionend', () => {
expect().nothing()
alertEl.addEventListener('transitionend', () => {
expect().nothing()
})
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert')).toHaveSize(0)
resolve()
})
alert.close()
})
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert').length).toEqual(0)
done()
})
alert.close()
})
it('should not remove alert if close event is prevented', done => {
fixtureEl.innerHTML = '<div class="alert"></div>'
it('should not remove alert if close event is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const getAlert = () => document.querySelector('.alert')
const alertEl = getAlert()
const alert = new Alert(alertEl)
const getAlert = () => document.querySelector('.alert')
const alertEl = getAlert()
const alert = new Alert(alertEl)
alertEl.addEventListener('close.bs.alert', event => {
event.preventDefault()
setTimeout(() => {
expect(getAlert()).not.toBeNull()
done()
}, 10)
alertEl.addEventListener('close.bs.alert', event => {
event.preventDefault()
setTimeout(() => {
expect(getAlert()).not.toBeNull()
resolve()
}, 10)
})
alertEl.addEventListener('closed.bs.alert', () => {
reject(new Error('should not fire closed event'))
})
alert.close()
})
alertEl.addEventListener('closed.bs.alert', () => {
throw new Error('should not fire closed event')
})
alert.close()
})
})
@@ -181,6 +185,34 @@ describe('Alert', () => {
expect(Alert.getInstance(alertEl)).not.toBeNull()
expect(fixtureEl.querySelector('.alert')).not.toBeNull()
})
it('should throw an error on undefined method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = 'undefinedMethod'
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.alert.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should throw an error on protected method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = '_getConfig'
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.alert.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
})
describe('getInstance', () => {
@@ -199,7 +231,7 @@ describe('Alert', () => {
const div = fixtureEl.querySelector('div')
expect(Alert.getInstance(div)).toEqual(null)
expect(Alert.getInstance(div)).toBeNull()
})
})
@@ -220,7 +252,7 @@ describe('Alert', () => {
const div = fixtureEl.querySelector('div')
expect(Alert.getInstance(div)).toEqual(null)
expect(Alert.getInstance(div)).toBeNull()
expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert)
})
})
+24 -3
View File
@@ -37,7 +37,7 @@ describe('Base Component', () => {
describe('Static Methods', () => {
describe('VERSION', () => {
it('should return version', () => {
expect(typeof DummyClass.VERSION).toEqual('string')
expect(DummyClass.VERSION).toEqual(jasmine.any(String))
})
})
@@ -48,6 +48,13 @@ describe('Base Component', () => {
})
describe('NAME', () => {
it('should throw an Error if it is not initialized', () => {
expect(() => {
// eslint-disable-next-line no-unused-expressions
BaseComponent.NAME
}).toThrowError(Error)
})
it('should return plugin NAME', () => {
expect(DummyClass.NAME).toEqual(name)
})
@@ -59,6 +66,7 @@ describe('Base Component', () => {
})
})
})
describe('Public Methods', () => {
describe('constructor', () => {
it('should accept element, either passed as a CSS selector or DOM element', () => {
@@ -74,7 +82,19 @@ describe('Base Component', () => {
expect(elInstance._element).toEqual(el)
expect(selectorInstance._element).toEqual(fixtureEl.querySelector('#bar'))
})
it('should not initialize and add element record to Data (caching), if argument `element` is not an HTML element', () => {
fixtureEl.innerHTML = ''
const el = fixtureEl.querySelector('#foo')
const elInstance = new DummyClass(el)
const selectorInstance = new DummyClass('#bar')
expect(elInstance._element).not.toBeDefined()
expect(selectorInstance._element).not.toBeDefined()
})
})
describe('dispose', () => {
it('should dispose an component', () => {
createInstance()
@@ -123,9 +143,10 @@ describe('Base Component', () => {
const div = fixtureEl.querySelector('div')
expect(DummyClass.getInstance(div)).toEqual(null)
expect(DummyClass.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return an instance', () => {
createInstance()
@@ -139,7 +160,7 @@ describe('Base Component', () => {
fixtureEl.innerHTML = '<div id="foo"></div>'
element = fixtureEl.querySelector('#foo')
expect(DummyClass.getInstance(element)).toEqual(null)
expect(DummyClass.getInstance(element)).toBeNull()
expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass)
})
})
+11 -17
View File
@@ -1,11 +1,5 @@
import Button from '../../src/button'
/** Test helpers */
import {
getFixture,
clearFixture,
jQueryMock
} from '../helpers/fixture'
import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture'
describe('Button', () => {
let fixtureEl
@@ -51,19 +45,19 @@ describe('Button', () => {
const divTest = fixtureEl.querySelector('.test')
const btnTestParent = fixtureEl.querySelector('.testParent')
expect(btn.classList.contains('active')).toEqual(false)
expect(btn).not.toHaveClass('active')
btn.click()
expect(btn.classList.contains('active')).toEqual(true)
expect(btn).toHaveClass('active')
btn.click()
expect(btn.classList.contains('active')).toEqual(false)
expect(btn).not.toHaveClass('active')
divTest.click()
expect(btnTestParent.classList.contains('active')).toEqual(true)
expect(btnTestParent).toHaveClass('active')
})
})
@@ -75,12 +69,12 @@ describe('Button', () => {
const button = new Button(btnEl)
expect(btnEl.getAttribute('aria-pressed')).toEqual('false')
expect(btnEl.classList.contains('active')).toEqual(false)
expect(btnEl).not.toHaveClass('active')
button.toggle()
expect(btnEl.getAttribute('aria-pressed')).toEqual('true')
expect(btnEl.classList.contains('active')).toEqual(true)
expect(btnEl).toHaveClass('active')
})
})
@@ -127,7 +121,7 @@ describe('Button', () => {
jQueryMock.fn.button.call(jQueryMock, 'toggle')
expect(Button.getInstance(btnEl)).not.toBeNull()
expect(btnEl.classList.contains('active')).toEqual(true)
expect(btnEl).toHaveClass('active')
})
it('should just create a button instance without calling toggle', () => {
@@ -141,7 +135,7 @@ describe('Button', () => {
jQueryMock.fn.button.call(jQueryMock)
expect(Button.getInstance(btnEl)).not.toBeNull()
expect(btnEl.classList.contains('active')).toEqual(false)
expect(btnEl).not.toHaveClass('active')
})
})
@@ -161,7 +155,7 @@ describe('Button', () => {
const div = fixtureEl.querySelector('div')
expect(Button.getInstance(div)).toEqual(null)
expect(Button.getInstance(div)).toBeNull()
})
})
@@ -182,7 +176,7 @@ describe('Button', () => {
const div = fixtureEl.querySelector('div')
expect(Button.getInstance(div)).toEqual(null)
expect(Button.getInstance(div)).toBeNull()
expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button)
})
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,6 +1,4 @@
import Data from '../../../src/dom/data'
/** Test helpers */
import { getFixture, clearFixture } from '../../helpers/fixture'
describe('Data', () => {
@@ -52,7 +50,7 @@ describe('Data', () => {
Data.set(div, TEST_KEY, data)
expect(Data.get(div, TEST_KEY)).toBe(data)
expect(Data.get(div, TEST_KEY)).toEqual(data)
})
it('should overwrite data if something is already stored', () => {
@@ -62,11 +60,12 @@ describe('Data', () => {
Data.set(div, TEST_KEY, data)
Data.set(div, TEST_KEY, copy)
// Using `toBe` since spread creates a shallow copy
expect(Data.get(div, TEST_KEY)).not.toBe(data)
expect(Data.get(div, TEST_KEY)).toBe(copy)
})
it('should do nothing when an element have nothing stored', () => {
it('should do nothing when an element has nothing stored', () => {
Data.remove(div, TEST_KEY)
expect().nothing()
@@ -78,7 +77,7 @@ describe('Data', () => {
Data.set(div, TEST_KEY, data)
Data.remove(div, UNKNOWN_KEY)
expect(Data.get(div, TEST_KEY)).toBe(data)
expect(Data.get(div, TEST_KEY)).toEqual(data)
})
it('should remove data for a given key', () => {
@@ -90,8 +89,8 @@ describe('Data', () => {
expect(Data.get(div, TEST_KEY)).toBeNull()
})
/* eslint-disable no-console */
it('should console.error a message if called with multiple keys', () => {
/* eslint-disable no-console */
console.error = jasmine.createSpy('console.error')
const data = { ...TEST_DATA }
@@ -101,6 +100,7 @@ describe('Data', () => {
Data.set(div, UNKNOWN_KEY, copy)
expect(console.error).toHaveBeenCalled()
expect(Data.get(div, UNKNOWN_KEY)).toBe(null)
expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
})
/* eslint-enable no-console */
})
+282 -255
View File
@@ -1,7 +1,6 @@
import EventHandler from '../../../src/dom/event-handler'
/** Test helpers */
import { getFixture, clearFixture } from '../../helpers/fixture'
import { clearFixture, getFixture } from '../../helpers/fixture'
import { noop } from '../../../src/util'
describe('EventHandler', () => {
let fixtureEl
@@ -20,176 +19,190 @@ describe('EventHandler', () => {
const div = fixtureEl.querySelector('div')
EventHandler.on(div, null, () => {})
EventHandler.on(null, 'click', () => {})
EventHandler.on(div, null, noop)
EventHandler.on(null, 'click', noop)
expect().nothing()
})
it('should add event listener', done => {
fixtureEl.innerHTML = '<div></div>'
it('should add event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'click', () => {
expect().nothing()
done()
EventHandler.on(div, 'click', () => {
expect().nothing()
resolve()
})
div.click()
})
div.click()
})
it('should add namespaced event listener', done => {
fixtureEl.innerHTML = '<div></div>'
it('should add namespaced event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'bs.namespace', () => {
expect().nothing()
done()
EventHandler.on(div, 'bs.namespace', () => {
expect().nothing()
resolve()
})
EventHandler.trigger(div, 'bs.namespace')
})
EventHandler.trigger(div, 'bs.namespace')
})
it('should add native namespaced event listener', done => {
fixtureEl.innerHTML = '<div></div>'
it('should add native namespaced event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'click.namespace', () => {
expect().nothing()
done()
EventHandler.on(div, 'click.namespace', () => {
expect().nothing()
resolve()
})
EventHandler.trigger(div, 'click')
})
EventHandler.trigger(div, 'click')
})
it('should handle event delegation', done => {
EventHandler.on(document, 'click', '.test', () => {
expect().nothing()
done()
it('should handle event delegation', () => {
return new Promise(resolve => {
EventHandler.on(document, 'click', '.test', () => {
expect().nothing()
resolve()
})
fixtureEl.innerHTML = '<div class="test"></div>'
const div = fixtureEl.querySelector('div')
div.click()
})
fixtureEl.innerHTML = '<div class="test"></div>'
const div = fixtureEl.querySelector('div')
div.click()
})
it('should handle mouseenter/mouseleave like the native counterpart', done => {
fixtureEl.innerHTML = [
'<div class="outer">',
'<div class="inner">',
'<div class="nested">',
'<div class="deep"></div>',
'</div>',
'</div>',
'<div class="sibling"></div>',
'</div>'
]
it('should handle mouseenter/mouseleave like the native counterpart', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="outer">',
'<div class="inner">',
'<div class="nested">',
'<div class="deep"></div>',
'</div>',
'</div>',
'<div class="sibling"></div>',
'</div>'
].join('')
const outer = fixtureEl.querySelector('.outer')
const inner = fixtureEl.querySelector('.inner')
const nested = fixtureEl.querySelector('.nested')
const deep = fixtureEl.querySelector('.deep')
const sibling = fixtureEl.querySelector('.sibling')
const outer = fixtureEl.querySelector('.outer')
const inner = fixtureEl.querySelector('.inner')
const nested = fixtureEl.querySelector('.nested')
const deep = fixtureEl.querySelector('.deep')
const sibling = fixtureEl.querySelector('.sibling')
const enterSpy = jasmine.createSpy('mouseenter')
const leaveSpy = jasmine.createSpy('mouseleave')
const delegateEnterSpy = jasmine.createSpy('mouseenter')
const delegateLeaveSpy = jasmine.createSpy('mouseleave')
const enterSpy = jasmine.createSpy('mouseenter')
const leaveSpy = jasmine.createSpy('mouseleave')
const delegateEnterSpy = jasmine.createSpy('mouseenter')
const delegateLeaveSpy = jasmine.createSpy('mouseleave')
EventHandler.on(inner, 'mouseenter', enterSpy)
EventHandler.on(inner, 'mouseleave', leaveSpy)
EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
EventHandler.on(inner, 'mouseenter', enterSpy)
EventHandler.on(inner, 'mouseleave', leaveSpy)
EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
EventHandler.on(sibling, 'mouseenter', () => {
expect(enterSpy.calls.count()).toBe(2)
expect(leaveSpy.calls.count()).toBe(2)
expect(delegateEnterSpy.calls.count()).toBe(2)
expect(delegateLeaveSpy.calls.count()).toBe(2)
done()
})
EventHandler.on(sibling, 'mouseenter', () => {
expect(enterSpy.calls.count()).toEqual(2)
expect(leaveSpy.calls.count()).toEqual(2)
expect(delegateEnterSpy.calls.count()).toEqual(2)
expect(delegateLeaveSpy.calls.count()).toEqual(2)
resolve()
})
const moveMouse = (from, to) => {
from.dispatchEvent(new MouseEvent('mouseout', {
bubbles: true,
relatedTarget: to
}))
const moveMouse = (from, to) => {
from.dispatchEvent(new MouseEvent('mouseout', {
bubbles: true,
relatedTarget: to
}))
to.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
relatedTarget: from
}))
}
to.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
relatedTarget: from
}))
}
// from outer to deep and back to outer (nested)
moveMouse(outer, inner)
moveMouse(inner, nested)
moveMouse(nested, deep)
moveMouse(deep, nested)
moveMouse(nested, inner)
moveMouse(inner, outer)
setTimeout(() => {
expect(enterSpy.calls.count()).toBe(1)
expect(leaveSpy.calls.count()).toBe(1)
expect(delegateEnterSpy.calls.count()).toBe(1)
expect(delegateLeaveSpy.calls.count()).toBe(1)
// from outer to inner to sibling (adjacent)
// from outer to deep and back to outer (nested)
moveMouse(outer, inner)
moveMouse(inner, sibling)
}, 20)
moveMouse(inner, nested)
moveMouse(nested, deep)
moveMouse(deep, nested)
moveMouse(nested, inner)
moveMouse(inner, outer)
setTimeout(() => {
expect(enterSpy.calls.count()).toEqual(1)
expect(leaveSpy.calls.count()).toEqual(1)
expect(delegateEnterSpy.calls.count()).toEqual(1)
expect(delegateLeaveSpy.calls.count()).toEqual(1)
// from outer to inner to sibling (adjacent)
moveMouse(outer, inner)
moveMouse(inner, sibling)
}, 20)
})
})
})
describe('one', () => {
it('should call listener just once', done => {
fixtureEl.innerHTML = '<div></div>'
it('should call listener just once', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
}
}
}
EventHandler.one(div, 'bootstrap', obj.oneListener)
EventHandler.one(div, 'bootstrap', obj.oneListener)
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
setTimeout(() => {
expect(called).toEqual(1)
done()
}, 20)
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
it('should call delegated listener just once', done => {
fixtureEl.innerHTML = '<div></div>'
it('should call delegated listener just once', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
}
}
}
EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
setTimeout(() => {
expect(called).toEqual(1)
done()
}, 20)
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
})
@@ -198,171 +211,185 @@ describe('EventHandler', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.off(div, null, () => {})
EventHandler.off(null, 'click', () => {})
EventHandler.off(div, null, noop)
EventHandler.off(null, 'click', noop)
expect().nothing()
})
it('should remove a listener', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove a listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
const handler = () => {
called++
}
let called = 0
const handler = () => {
called++
}
EventHandler.on(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
EventHandler.on(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
EventHandler.off(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
EventHandler.off(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect(called).toEqual(1)
done()
}, 20)
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
it('should remove all the events', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove all the events', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
let called = 0
EventHandler.on(div, 'foobar', () => {
called++
EventHandler.on(div, 'foobar', () => {
called++
})
EventHandler.on(div, 'foobar', () => {
called++
})
EventHandler.trigger(div, 'foobar')
EventHandler.off(div, 'foobar')
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect(called).toEqual(2)
resolve()
}, 20)
})
EventHandler.on(div, 'foobar', () => {
called++
})
EventHandler.trigger(div, 'foobar')
EventHandler.off(div, 'foobar')
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect(called).toEqual(2)
done()
}, 20)
})
it('should remove all the namespaced listeners if namespace is passed', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove all the namespaced listeners if namespace is passed', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
let called = 0
EventHandler.on(div, 'foobar.namespace', () => {
called++
EventHandler.on(div, 'foobar.namespace', () => {
called++
})
EventHandler.on(div, 'foofoo.namespace', () => {
called++
})
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
EventHandler.off(div, '.namespace')
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
setTimeout(() => {
expect(called).toEqual(2)
resolve()
}, 20)
})
EventHandler.on(div, 'foofoo.namespace', () => {
called++
})
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
EventHandler.off(div, '.namespace')
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
setTimeout(() => {
expect(called).toEqual(2)
done()
}, 20)
})
it('should remove the namespaced listeners', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove the namespaced listeners', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let calledCallback1 = 0
let calledCallback2 = 0
let calledCallback1 = 0
let calledCallback2 = 0
EventHandler.on(div, 'foobar.namespace', () => {
calledCallback1++
EventHandler.on(div, 'foobar.namespace', () => {
calledCallback1++
})
EventHandler.on(div, 'foofoo.namespace', () => {
calledCallback2++
})
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.off(div, 'foobar.namespace')
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
setTimeout(() => {
expect(calledCallback1).toEqual(1)
expect(calledCallback2).toEqual(1)
resolve()
}, 20)
})
EventHandler.on(div, 'foofoo.namespace', () => {
calledCallback2++
})
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.off(div, 'foobar.namespace')
EventHandler.trigger(div, 'foobar.namespace')
EventHandler.trigger(div, 'foofoo.namespace')
setTimeout(() => {
expect(calledCallback1).toEqual(1)
expect(calledCallback2).toEqual(1)
done()
}, 20)
})
it('should remove the all the namespaced listeners for native events', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove the all the namespaced listeners for native events', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
let called = 0
EventHandler.on(div, 'click.namespace', () => {
called++
EventHandler.on(div, 'click.namespace', () => {
called++
})
EventHandler.on(div, 'click.namespace2', () => {
called++
})
EventHandler.trigger(div, 'click')
EventHandler.off(div, 'click')
EventHandler.trigger(div, 'click')
setTimeout(() => {
expect(called).toEqual(2)
resolve()
}, 20)
})
EventHandler.on(div, 'click.namespace2', () => {
called++
})
EventHandler.trigger(div, 'click')
EventHandler.off(div, 'click')
EventHandler.trigger(div, 'click')
setTimeout(() => {
expect(called).toEqual(2)
done()
}, 20)
})
it('should remove the specified namespaced listeners for native events', done => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
it('should remove the specified namespaced listeners for native events', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called1 = 0
let called2 = 0
let called1 = 0
let called2 = 0
EventHandler.on(div, 'click.namespace', () => {
called1++
EventHandler.on(div, 'click.namespace', () => {
called1++
})
EventHandler.on(div, 'click.namespace2', () => {
called2++
})
EventHandler.trigger(div, 'click')
EventHandler.off(div, 'click.namespace')
EventHandler.trigger(div, 'click')
setTimeout(() => {
expect(called1).toEqual(1)
expect(called2).toEqual(2)
resolve()
}, 20)
})
EventHandler.on(div, 'click.namespace2', () => {
called2++
})
EventHandler.trigger(div, 'click')
EventHandler.off(div, 'click.namespace')
EventHandler.trigger(div, 'click')
setTimeout(() => {
expect(called1).toEqual(1)
expect(called2).toEqual(2)
done()
}, 20)
})
it('should remove a listener registered by .one', done => {
fixtureEl.innerHTML = '<div></div>'
it('should remove a listener registered by .one', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const handler = () => {
throw new Error('called')
}
const div = fixtureEl.querySelector('div')
const handler = () => {
reject(new Error('called'))
}
EventHandler.one(div, 'foobar', handler)
EventHandler.off(div, 'foobar', handler)
EventHandler.one(div, 'foobar', handler)
EventHandler.off(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect().nothing()
done()
}, 20)
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect().nothing()
resolve()
}, 20)
})
})
it('should remove the correct delegated event listener', () => {
+33 -33
View File
@@ -1,7 +1,5 @@
import Manipulator from '../../../src/dom/manipulator'
/** Test helpers */
import { getFixture, clearFixture } from '../../helpers/fixture'
import { clearFixture, getFixture } from '../../helpers/fixture'
describe('Manipulator', () => {
let fixtureEl
@@ -98,10 +96,10 @@ describe('Manipulator', () => {
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(false)
expect(Manipulator.getDataAttribute(div, 'test')).toBeFalse()
div.setAttribute('data-bs-test', 'true')
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(true)
expect(Manipulator.getDataAttribute(div, 'test')).toBeTrue()
div.setAttribute('data-bs-test', '1')
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1)
@@ -136,42 +134,44 @@ describe('Manipulator', () => {
})
})
it('should not change offset when viewport is scrolled', done => {
const top = 500
const left = 1000
const scrollY = 200
const scrollX = 400
it('should not change offset when viewport is scrolled', () => {
return new Promise(resolve => {
const top = 500
const left = 1000
const scrollY = 200
const scrollX = 400
fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>`
fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>`
const div = fixtureEl.querySelector('div')
const offset = Manipulator.offset(div)
const div = fixtureEl.querySelector('div')
const offset = Manipulator.offset(div)
// append an element that forces scrollbars on the window so we can scroll
const { defaultView: win, body } = fixtureEl.ownerDocument
const forceScrollBars = document.createElement('div')
forceScrollBars.style.cssText = 'position:absolute;top:5000px;left:5000px;width:1px;height:1px'
body.append(forceScrollBars)
// append an element that forces scrollbars on the window so we can scroll
const { defaultView: win, body } = fixtureEl.ownerDocument
const forceScrollBars = document.createElement('div')
forceScrollBars.style.cssText = 'position:absolute;top:5000px;left:5000px;width:1px;height:1px'
body.append(forceScrollBars)
const scrollHandler = () => {
expect(window.pageYOffset).toBe(scrollY)
expect(window.pageXOffset).toBe(scrollX)
const scrollHandler = () => {
expect(window.pageYOffset).toEqual(scrollY)
expect(window.pageXOffset).toEqual(scrollX)
const newOffset = Manipulator.offset(div)
const newOffset = Manipulator.offset(div)
expect(newOffset).toEqual({
top: offset.top,
left: offset.left
})
expect(newOffset).toEqual({
top: offset.top,
left: offset.left
})
win.removeEventListener('scroll', scrollHandler)
forceScrollBars.remove()
win.scrollTo(0, 0)
done()
}
win.removeEventListener('scroll', scrollHandler)
forceScrollBars.remove()
win.scrollTo(0, 0)
resolve()
}
win.addEventListener('scroll', scrollHandler)
win.scrollTo(scrollX, scrollY)
win.addEventListener('scroll', scrollHandler)
win.scrollTo(scrollX, scrollY)
})
})
})
+21 -27
View File
@@ -1,6 +1,4 @@
import SelectorEngine from '../../../src/dom/selector-engine'
/** Test helpers */
import { getFixture, clearFixture } from '../../helpers/fixture'
describe('SelectorEngine', () => {
@@ -32,13 +30,15 @@ describe('SelectorEngine', () => {
})
it('should handle :scope selectors', () => {
fixtureEl.innerHTML = `<ul>
<li></li>
<li>
<a href="#" class="active">link</a>
</li>
<li></li>
</ul>`
fixtureEl.innerHTML = [
'<ul>',
' <li></li>',
' <li>',
' <a href="#" class="active">link</a>',
' </li>',
' <li></li>',
'</ul>'
].join('')
const listEl = fixtureEl.querySelector('ul')
const aActive = fixtureEl.querySelector('.active')
@@ -59,11 +59,13 @@ describe('SelectorEngine', () => {
describe('children', () => {
it('should find children', () => {
fixtureEl.innerHTML = `<ul>
<li></li>
<li></li>
<li></li>
</ul>`
fixtureEl.innerHTML = [
'<ul>',
' <li></li>',
' <li></li>',
' <li></li>',
'</ul>'
].join('')
const list = fixtureEl.querySelector('ul')
const liList = [].concat(...fixtureEl.querySelectorAll('li'))
@@ -75,7 +77,7 @@ describe('SelectorEngine', () => {
describe('parents', () => {
it('should return parents', () => {
expect(SelectorEngine.parents(fixtureEl, 'body').length).toEqual(1)
expect(SelectorEngine.parents(fixtureEl, 'body')).toHaveSize(1)
})
})
@@ -199,9 +201,7 @@ describe('SelectorEngine', () => {
})
it('should return not return elements with negative tab index', () => {
fixtureEl.innerHTML = [
'<button tabindex="-1">lorem</button>'
].join('')
fixtureEl.innerHTML = '<button tabindex="-1">lorem</button>'
const expectedElements = []
@@ -209,9 +209,7 @@ describe('SelectorEngine', () => {
})
it('should return contenteditable elements', () => {
fixtureEl.innerHTML = [
'<div contenteditable="true">lorem</div>'
].join('')
fixtureEl.innerHTML = '<div contenteditable="true">lorem</div>'
const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')]
@@ -219,9 +217,7 @@ describe('SelectorEngine', () => {
})
it('should not return disabled elements', () => {
fixtureEl.innerHTML = [
'<button disabled="true">lorem</button>'
].join('')
fixtureEl.innerHTML = '<button disabled="true">lorem</button>'
const expectedElements = []
@@ -229,9 +225,7 @@ describe('SelectorEngine', () => {
})
it('should not return invisible elements', () => {
fixtureEl.innerHTML = [
'<button style="display:none;">lorem</button>'
].join('')
fixtureEl.innerHTML = '<button style="display:none;">lorem</button>'
const expectedElements = []
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -1,4 +1,5 @@
/* eslint-env jquery */
import Alert from '../../src/alert'
import Button from '../../src/button'
import Carousel from '../../src/carousel'
@@ -11,9 +12,7 @@ import ScrollSpy from '../../src/scrollspy'
import Tab from '../../src/tab'
import Toast from '../../src/toast'
import Tooltip from '../../src/tooltip'
/** Test helpers */
import { getFixture, clearFixture } from '../helpers/fixture'
import { clearFixture, getFixture } from '../helpers/fixture'
describe('jQuery', () => {
let fixtureEl
@@ -41,19 +40,21 @@ describe('jQuery', () => {
expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip)
})
it('should use jQuery event system', done => {
fixtureEl.innerHTML = [
'<div class="alert">',
' <button type="button" data-bs-dismiss="alert">x</button>',
'</div>'
].join('')
it('should use jQuery event system', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="alert">',
' <button type="button" data-bs-dismiss="alert">x</button>',
'</div>'
].join('')
$(fixtureEl).find('.alert')
.one('closed.bs.alert', () => {
expect($(fixtureEl).find('.alert').length).toEqual(0)
done()
})
$(fixtureEl).find('.alert')
.one('closed.bs.alert', () => {
expect($(fixtureEl).find('.alert')).toHaveSize(0)
resolve()
})
$(fixtureEl).find('button').trigger('click')
$(fixtureEl).find('button').trigger('click')
})
})
})
File diff suppressed because it is too large Load Diff
+341 -269
View File
@@ -1,9 +1,7 @@
import Offcanvas from '../../src/offcanvas'
import EventHandler from '../../src/dom/event-handler'
/** Test helpers */
import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
import { isVisible } from '../../src/util'
import { isVisible } from '../../src/util/index'
import ScrollBarHelper from '../../src/util/scrollbar'
describe('Offcanvas', () => {
@@ -57,7 +55,7 @@ describe('Offcanvas', () => {
closeEl.click()
expect(offCanvas._config.keyboard).toBe(true)
expect(offCanvas._config.keyboard).toBeTrue()
expect(offCanvas.hide).toHaveBeenCalled()
})
@@ -103,47 +101,38 @@ describe('Offcanvas', () => {
document.dispatchEvent(keyDownEsc)
expect(offCanvas._config.keyboard).toBe(false)
expect(offCanvas._config.keyboard).toBeFalse()
expect(offCanvas.hide).not.toHaveBeenCalled()
})
})
describe('config', () => {
it('should have default values', () => {
fixtureEl.innerHTML = [
'<div class="offcanvas">',
'</div>'
].join('')
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
expect(offCanvas._config.backdrop).toEqual(true)
expect(offCanvas._backdrop._config.isVisible).toEqual(true)
expect(offCanvas._config.keyboard).toEqual(true)
expect(offCanvas._config.scroll).toEqual(false)
expect(offCanvas._config.backdrop).toBeTrue()
expect(offCanvas._backdrop._config.isVisible).toBeTrue()
expect(offCanvas._config.keyboard).toBeTrue()
expect(offCanvas._config.scroll).toBeFalse()
})
it('should read data attributes and override default config', () => {
fixtureEl.innerHTML = [
'<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">',
'</div>'
].join('')
fixtureEl.innerHTML = '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
expect(offCanvas._config.backdrop).toEqual(false)
expect(offCanvas._backdrop._config.isVisible).toEqual(false)
expect(offCanvas._config.keyboard).toEqual(false)
expect(offCanvas._config.scroll).toEqual(true)
expect(offCanvas._config.backdrop).toBeFalse()
expect(offCanvas._backdrop._config.isVisible).toBeFalse()
expect(offCanvas._config.keyboard).toBeFalse()
expect(offCanvas._config.scroll).toBeTrue()
})
it('given a config object must override data attributes', () => {
fixtureEl.innerHTML = [
'<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">',
'</div>'
].join('')
fixtureEl.innerHTML = '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, {
@@ -151,90 +140,98 @@ describe('Offcanvas', () => {
keyboard: true,
scroll: false
})
expect(offCanvas._config.backdrop).toEqual(true)
expect(offCanvas._config.keyboard).toEqual(true)
expect(offCanvas._config.scroll).toEqual(false)
expect(offCanvas._config.backdrop).toBeTrue()
expect(offCanvas._config.keyboard).toBeTrue()
expect(offCanvas._config.scroll).toBeFalse()
})
})
describe('options', () => {
it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: true })
spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: true })
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled()
offCanvas.hide()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled()
offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled()
done()
})
offCanvas.show()
})
it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: false })
spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: false })
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled()
offCanvas.hide()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled()
offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled()
done()
})
offCanvas.show()
})
it('should hide a shown element if user click on backdrop', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should hide a shown element if user click on backdrop', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true })
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true })
const clickEvent = document.createEvent('MouseEvents')
clickEvent.initEvent('mousedown', true, true)
spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough()
const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true })
spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(typeof offCanvas._backdrop._config.clickCallback).toBe('function')
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function))
offCanvas._backdrop._getElement().dispatchEvent(clickEvent)
offCanvas._backdrop._getElement().dispatchEvent(clickEvent)
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled()
done()
})
offCanvas.show()
})
it('should not trap focus if scroll is allowed', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should not trap focus if scroll is allowed', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, {
scroll: true
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, {
scroll: true
})
spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._focustrap.activate).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._focustrap.activate).not.toHaveBeenCalled()
done()
})
offCanvas.show()
})
})
@@ -253,22 +250,49 @@ describe('Offcanvas', () => {
})
it('should call hide method if show class is present', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()
expect(offCanvasEl.classList.contains('show')).toBe(true)
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas, 'hide')
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
spyOn(offCanvas, 'hide')
offCanvas.toggle()
offCanvas.toggle()
expect(offCanvas.hide).toHaveBeenCalled()
expect(offCanvas.hide).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
})
describe('show', () => {
it('should add `showing` class during opening and `show` class on end', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvasEl.addEventListener('show.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('show')
})
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('showing')
expect(offCanvasEl).toHaveClass('show')
resolve()
})
offCanvas.show()
expect(offCanvasEl).toHaveClass('showing')
})
})
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
@@ -276,7 +300,7 @@ describe('Offcanvas', () => {
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()
expect(offCanvasEl.classList.contains('show')).toBe(true)
expect(offCanvasEl).toHaveClass('show')
spyOn(offCanvas._backdrop, 'show').and.callThrough()
spyOn(EventHandler, 'trigger').and.callThrough()
@@ -286,83 +310,117 @@ describe('Offcanvas', () => {
expect(offCanvas._backdrop.show).not.toHaveBeenCalled()
})
it('should show a hidden element', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should show a hidden element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'show').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'show').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(true)
expect(offCanvas._backdrop.show).toHaveBeenCalled()
done()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
expect(offCanvas._backdrop.show).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
offCanvas.show()
})
it('should not fire shown when show is prevented', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should not fire shown when show is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'show').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'show').and.callThrough()
const expectEnd = () => {
setTimeout(() => {
expect(offCanvas._backdrop.show).not.toHaveBeenCalled()
done()
}, 10)
}
const expectEnd = () => {
setTimeout(() => {
expect(offCanvas._backdrop.show).not.toHaveBeenCalled()
resolve()
}, 10)
}
offCanvasEl.addEventListener('show.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
offCanvasEl.addEventListener('show.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
reject(new Error('should not fire shown event'))
})
offCanvas.show()
})
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
throw new Error('should not fire shown event')
})
offCanvas.show()
})
it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
it('on window load, should make visible an offcanvas element, if its markup contains class "show"', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
spyOn(Offcanvas.prototype, 'show').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('div')
spyOn(Offcanvas.prototype, 'show').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
done()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
resolve()
})
window.dispatchEvent(createEvent('load'))
const instance = Offcanvas.getInstance(offCanvasEl)
expect(instance).not.toBeNull()
expect(Offcanvas.prototype.show).toHaveBeenCalled()
})
window.dispatchEvent(createEvent('load'))
const instance = Offcanvas.getInstance(offCanvasEl)
expect(instance).not.toBeNull()
expect(Offcanvas.prototype.show).toHaveBeenCalled()
})
it('should trap focus', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should trap focus', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._focustrap, 'activate').and.callThrough()
spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._focustrap.activate).toHaveBeenCalled()
done()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._focustrap.activate).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
offCanvas.show()
})
})
describe('hide', () => {
it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvasEl.addEventListener('hide.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('showing')
expect(offCanvasEl).toHaveClass('show')
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('hiding')
expect(offCanvasEl).not.toHaveClass('show')
resolve()
})
offCanvas.show()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
offCanvas.hide()
expect(offCanvasEl).not.toHaveClass('showing')
expect(offCanvasEl).toHaveClass('hiding')
})
})
})
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
@@ -377,65 +435,71 @@ describe('Offcanvas', () => {
expect(EventHandler.trigger).not.toHaveBeenCalled()
})
it('should hide a shown element', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should hide a shown element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.show()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.show()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(false)
expect(offCanvas._backdrop.hide).toHaveBeenCalled()
done()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('show')
expect(offCanvas._backdrop.hide).toHaveBeenCalled()
resolve()
})
offCanvas.hide()
})
offCanvas.hide()
})
it('should not fire hidden when hide is prevented', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should not fire hidden when hide is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'hide').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.show()
offCanvas.show()
const expectEnd = () => {
setTimeout(() => {
expect(offCanvas._backdrop.hide).not.toHaveBeenCalled()
done()
}, 10)
}
const expectEnd = () => {
setTimeout(() => {
expect(offCanvas._backdrop.hide).not.toHaveBeenCalled()
resolve()
}, 10)
}
offCanvasEl.addEventListener('hide.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
offCanvasEl.addEventListener('hide.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
reject(new Error('should not fire hidden event'))
})
offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
throw new Error('should not fire hidden event')
})
offCanvas.hide()
})
it('should release focus trap', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
it('should release focus trap', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._focustrap, 'deactivate').and.callThrough()
offCanvas.show()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
spyOn(offCanvas._focustrap, 'deactivate').and.callThrough()
offCanvas.show()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvas._focustrap.deactivate).toHaveBeenCalled()
done()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvas._focustrap.deactivate).toHaveBeenCalled()
resolve()
})
offCanvas.hide()
})
offCanvas.hide()
})
})
@@ -460,27 +524,29 @@ describe('Offcanvas', () => {
expect(offCanvas._backdrop).toBeNull()
expect(focustrap.deactivate).toHaveBeenCalled()
expect(offCanvas._focustrap).toBeNull()
expect(Offcanvas.getInstance(offCanvasEl)).toEqual(null)
expect(Offcanvas.getInstance(offCanvasEl)).toBeNull()
})
})
describe('data-api', () => {
it('should not prevent event for input', done => {
fixtureEl.innerHTML = [
'<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />',
'<div id="offcanvasdiv1" class="offcanvas"></div>'
].join('')
it('should not prevent event for input', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />',
'<div id="offcanvasdiv1" class="offcanvas"></div>'
].join('')
const target = fixtureEl.querySelector('input')
const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1')
const target = fixtureEl.querySelector('input')
const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1')
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(true)
expect(target.checked).toEqual(true)
done()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
expect(target.checked).toBeTrue()
resolve()
})
target.click()
})
target.click()
})
it('should not call toggle on disabled elements', () => {
@@ -498,76 +564,82 @@ describe('Offcanvas', () => {
expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled()
})
it('should call hide first, if another offcanvas is open', done => {
fixtureEl.innerHTML = [
'<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2" ></button>',
'<div id="offcanvas1" class="offcanvas"></div>',
'<div id="offcanvas2" class="offcanvas"></div>'
].join('')
it('should call hide first, if another offcanvas is open', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2"></button>',
'<div id="offcanvas1" class="offcanvas"></div>',
'<div id="offcanvas2" class="offcanvas"></div>'
].join('')
const trigger2 = fixtureEl.querySelector('#btn2')
const offcanvasEl1 = document.querySelector('#offcanvas1')
const offcanvasEl2 = document.querySelector('#offcanvas2')
const offcanvas1 = new Offcanvas(offcanvasEl1)
const trigger2 = fixtureEl.querySelector('#btn2')
const offcanvasEl1 = document.querySelector('#offcanvas1')
const offcanvasEl2 = document.querySelector('#offcanvas2')
const offcanvas1 = new Offcanvas(offcanvasEl1)
offcanvasEl1.addEventListener('shown.bs.offcanvas', () => {
trigger2.click()
offcanvasEl1.addEventListener('shown.bs.offcanvas', () => {
trigger2.click()
})
offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => {
expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull()
resolve()
})
offcanvas1.show()
})
offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => {
expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull()
done()
})
offcanvas1.show()
})
it('should focus on trigger element after closing offcanvas', done => {
fixtureEl.innerHTML = [
'<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas" ></button>',
'<div id="offcanvas" class="offcanvas"></div>'
].join('')
it('should focus on trigger element after closing offcanvas', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>',
'<div id="offcanvas" class="offcanvas"></div>'
].join('')
const trigger = fixtureEl.querySelector('#btn')
const offcanvasEl = fixtureEl.querySelector('#offcanvas')
const offcanvas = new Offcanvas(offcanvasEl)
spyOn(trigger, 'focus')
const trigger = fixtureEl.querySelector('#btn')
const offcanvasEl = fixtureEl.querySelector('#offcanvas')
const offcanvas = new Offcanvas(offcanvasEl)
spyOn(trigger, 'focus')
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
offcanvas.hide()
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
offcanvas.hide()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(trigger.focus).toHaveBeenCalled()
resolve()
}, 5)
})
trigger.click()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(trigger.focus).toHaveBeenCalled()
done()
}, 5)
})
trigger.click()
})
it('should not focus on trigger element after closing offcanvas, if it is not visible', done => {
fixtureEl.innerHTML = [
'<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas" ></button>',
'<div id="offcanvas" class="offcanvas"></div>'
].join('')
it('should not focus on trigger element after closing offcanvas, if it is not visible', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>',
'<div id="offcanvas" class="offcanvas"></div>'
].join('')
const trigger = fixtureEl.querySelector('#btn')
const offcanvasEl = fixtureEl.querySelector('#offcanvas')
const offcanvas = new Offcanvas(offcanvasEl)
spyOn(trigger, 'focus')
const trigger = fixtureEl.querySelector('#btn')
const offcanvasEl = fixtureEl.querySelector('#offcanvas')
const offcanvas = new Offcanvas(offcanvasEl)
spyOn(trigger, 'focus')
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
trigger.style.display = 'none'
offcanvas.hide()
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
trigger.style.display = 'none'
offcanvas.hide()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(isVisible(trigger)).toBeFalse()
expect(trigger.focus).not.toHaveBeenCalled()
resolve()
}, 5)
})
trigger.click()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(isVisible(trigger)).toBe(false)
expect(trigger.focus).not.toHaveBeenCalled()
done()
}, 5)
})
trigger.click()
})
})
@@ -667,7 +739,7 @@ describe('Offcanvas', () => {
const offcanvas = Offcanvas.getInstance(div)
expect(offcanvas).not.toBeNull()
expect(offcanvas._config.scroll).toBe(true)
expect(offcanvas._config.scroll).toBeTrue()
})
})
@@ -708,7 +780,7 @@ describe('Offcanvas', () => {
const div = fixtureEl.querySelector('div')
expect(Offcanvas.getInstance(div)).toEqual(null)
expect(Offcanvas.getInstance(div)).toBeNull()
expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas)
})
@@ -717,13 +789,13 @@ describe('Offcanvas', () => {
const div = fixtureEl.querySelector('div')
expect(Offcanvas.getInstance(div)).toEqual(null)
expect(Offcanvas.getInstance(div)).toBeNull()
const offcanvas = Offcanvas.getOrCreateInstance(div, {
scroll: true
})
expect(offcanvas).toBeInstanceOf(Offcanvas)
expect(offcanvas._config.scroll).toEqual(true)
expect(offcanvas._config.scroll).toBeTrue()
})
it('should return the instance when exists without given configuration', () => {
@@ -741,7 +813,7 @@ describe('Offcanvas', () => {
expect(offcanvas).toBeInstanceOf(Offcanvas)
expect(offcanvas2).toEqual(offcanvas)
expect(offcanvas2._config.scroll).toEqual(true)
expect(offcanvas2._config.scroll).toBeTrue()
})
})
})
+190 -152
View File
@@ -1,6 +1,5 @@
import Popover from '../../src/popover'
/** Test helpers */
import EventHandler from '../../src/dom/event-handler'
import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture'
describe('Popover', () => {
@@ -15,9 +14,9 @@ describe('Popover', () => {
const popoverList = document.querySelectorAll('.popover')
popoverList.forEach(popoverEl => {
for (const popoverEl of popoverList) {
popoverEl.remove()
})
}
})
describe('VERSION', () => {
@@ -44,12 +43,6 @@ describe('Popover', () => {
})
})
describe('Event', () => {
it('should return plugin events', () => {
expect(Popover.Event).toEqual(jasmine.any(Object))
})
})
describe('EVENT_KEY', () => {
it('should return plugin event key', () => {
expect(Popover.EVENT_KEY).toEqual('.bs.popover')
@@ -63,165 +56,210 @@ describe('Popover', () => {
})
describe('show', () => {
it('should show a popover', done => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
it('should show a popover', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
popoverEl.addEventListener('shown.bs.popover', () => {
expect(document.querySelector('.popover')).not.toBeNull()
done()
})
popoverEl.addEventListener('shown.bs.popover', () => {
expect(document.querySelector('.popover')).not.toBeNull()
resolve()
})
popover.show()
})
it('should set title and content from functions', done => {
fixtureEl.innerHTML = '<a href="#">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
title: () => 'Bootstrap',
content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap')
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻')
done()
})
popover.show()
})
it('should show a popover with just content', done => {
fixtureEl.innerHTML = '<a href="#">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
content: 'Popover content'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content')
done()
})
popover.show()
})
it('should show a popover with just content without having header', done => {
fixtureEl.innerHTML = '<a href="#">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
content: 'Some beautiful content :)'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-header')).toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)')
done()
})
popover.show()
})
it('should show a popover with just title without having body', done => {
fixtureEl.innerHTML = '<a href="#">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
title: 'Title, which does not require content'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body')).toBeNull()
expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title, which does not require content')
done()
})
popover.show()
})
it('should call setContent once', done => {
fixtureEl.innerHTML = '<a href="#">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
content: 'Popover content'
})
const spy = spyOn(popover, 'setContent').and.callThrough()
let times = 1
popoverEl.addEventListener('hidden.bs.popover', () => {
popover.show()
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content')
expect(spy).toHaveBeenCalledTimes(1)
if (times > 1) {
done()
}
times++
popover.hide()
})
popover.show()
})
it('should show a popover with provided custom class', done => {
it('should set title and content from functions', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
title: () => 'Bootstrap',
content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap')
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻')
resolve()
})
popover.show()
})
})
it('should show a popover with just content without having header', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
content: 'Some beautiful content :)'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-header')).toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)')
resolve()
})
popover.show()
})
})
it('should show a popover with just title without having body', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
title: 'Title which does not require content'
})
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body')).toBeNull()
expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content')
resolve()
})
popover.show()
})
})
it('should show a popover with just title without having body using data-attribute to get config', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="Title which does not require content">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
popoverEl.addEventListener('shown.bs.popover', () => {
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body')).toBeNull()
expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content')
resolve()
})
popover.show()
})
})
it('should NOT show a popover without `title` and `content`', () => {
fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="">Nice link</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, { animation: false })
spyOn(EventHandler, 'trigger').and.callThrough()
popover.show()
expect(EventHandler.trigger).not.toHaveBeenCalledWith(popoverEl, Popover.eventName('show'))
expect(document.querySelector('.popover')).toBeNull()
})
it('"setContent" should keep the initial template', () => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
popoverEl.addEventListener('shown.bs.popover', () => {
const tip = document.querySelector('.popover')
expect(tip).not.toBeNull()
expect(tip.classList.contains('custom-class')).toBeTrue()
done()
})
popover.setContent({ '.tooltip-inner': 'foo' })
const tip = popover._getTipElement()
popover.show()
expect(tip).toHaveClass('popover')
expect(tip).toHaveClass('bs-popover-auto')
expect(tip.querySelector('.popover-arrow')).not.toBeNull()
expect(tip.querySelector('.popover-header')).not.toBeNull()
expect(tip.querySelector('.popover-body')).not.toBeNull()
})
it('should call setContent once', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl, {
content: 'Popover content'
})
expect(popover._templateFactory).toBeNull()
let spy = null
let times = 1
popoverEl.addEventListener('hidden.bs.popover', () => {
popover.show()
})
popoverEl.addEventListener('shown.bs.popover', () => {
spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough()
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content')
expect(spy).toHaveBeenCalledTimes(0)
if (times > 1) {
resolve()
}
times++
popover.hide()
})
popover.show()
})
})
it('should show a popover with provided custom class', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
popoverEl.addEventListener('shown.bs.popover', () => {
const tip = document.querySelector('.popover')
expect(tip).not.toBeNull()
expect(tip).toHaveClass('custom-class')
resolve()
})
popover.show()
})
})
})
describe('hide', () => {
it('should hide a popover', done => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
it('should hide a popover', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
const popoverEl = fixtureEl.querySelector('a')
const popover = new Popover(popoverEl)
popoverEl.addEventListener('shown.bs.popover', () => {
popover.hide()
popoverEl.addEventListener('shown.bs.popover', () => {
popover.hide()
})
popoverEl.addEventListener('hidden.bs.popover', () => {
expect(document.querySelector('.popover')).toBeNull()
resolve()
})
popover.show()
})
popoverEl.addEventListener('hidden.bs.popover', () => {
expect(document.querySelector('.popover')).toBeNull()
done()
})
popover.show()
})
})
@@ -315,7 +353,7 @@ describe('Popover', () => {
const popoverEl = fixtureEl.querySelector('a')
expect(Popover.getInstance(popoverEl)).toEqual(null)
expect(Popover.getInstance(popoverEl)).toBeNull()
})
})
@@ -336,7 +374,7 @@ describe('Popover', () => {
const div = fixtureEl.querySelector('div')
expect(Popover.getInstance(div)).toEqual(null)
expect(Popover.getInstance(div)).toBeNull()
expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover)
})
@@ -345,7 +383,7 @@ describe('Popover', () => {
const div = fixtureEl.querySelector('div')
expect(Popover.getInstance(div)).toEqual(null)
expect(Popover.getInstance(div)).toBeNull()
const popover = Popover.getOrCreateInstance(div, {
placement: 'top'
})
+411 -395
View File
@@ -1,8 +1,6 @@
import ScrollSpy from '../../src/scrollspy'
import Manipulator from '../../src/dom/manipulator'
/** Test helpers */
import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
describe('ScrollSpy', () => {
let fixtureEl
@@ -16,7 +14,7 @@ describe('ScrollSpy', () => {
const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop
function listener() {
expect(element.classList.contains('active')).toEqual(true)
expect(element).toHaveClass('active')
contentEl.removeEventListener('scroll', listener)
expect(scrollSpy._process).toHaveBeenCalled()
spy.calls.reset()
@@ -75,8 +73,8 @@ describe('ScrollSpy', () => {
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="two" style="height: 300px;"></div>',
' <div id="three" style="height: 10px;"></div>',
' <div id="two" style="height: 300px;"></div>',
' <div id="three" style="height: 10px;"></div>',
'</div>'
].join('')
@@ -84,421 +82,439 @@ describe('ScrollSpy', () => {
target: '#navigation'
})
expect(scrollSpy._targets.length).toEqual(2)
expect(scrollSpy._targets).toHaveSize(2)
})
it('should only switch "active" class on current target', done => {
fixtureEl.innerHTML = [
'<div id="root" class="active" style="display: block">',
' <div class="topbar">',
' <div class="topbar-inner">',
' <div class="container" id="ss-target">',
' <ul class="nav">',
' <li class="nav-item"><a href="#masthead">Overview</a></li>',
' <li class="nav-item"><a href="#detail">Detail</a></li>',
' </ul>',
' </div>',
' </div>',
' </div>',
' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
' <div style="height: 200px;">',
' <h4 id="masthead">Overview</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' <div style="height: 200px;">',
' <h4 id="detail">Detail</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' </div>',
'</div>'
].join('')
it('should only switch "active" class on current target', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="root" class="active" style="display: block">',
' <div class="topbar">',
' <div class="topbar-inner">',
' <div class="container" id="ss-target">',
' <ul class="nav">',
' <li class="nav-item"><a href="#masthead">Overview</a></li>',
' <li class="nav-item"><a href="#detail">Detail</a></li>',
' </ul>',
' </div>',
' </div>',
' </div>',
' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
' <div style="height: 200px;">',
' <h4 id="masthead">Overview</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' <div style="height: 200px;">',
' <h4 id="detail">Detail</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' </div>',
'</div>'
].join('')
const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
const rootEl = fixtureEl.querySelector('#root')
const scrollSpy = new ScrollSpy(scrollSpyEl, {
target: 'ss-target'
})
const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
const rootEl = fixtureEl.querySelector('#root')
const scrollSpy = new ScrollSpy(scrollSpyEl, {
target: 'ss-target'
})
spyOn(scrollSpy, '_process').and.callThrough()
spyOn(scrollSpy, '_process').and.callThrough()
scrollSpyEl.addEventListener('scroll', () => {
expect(rootEl.classList.contains('active')).toEqual(true)
expect(scrollSpy._process).toHaveBeenCalled()
done()
})
scrollSpyEl.addEventListener('scroll', () => {
expect(rootEl).toHaveClass('active')
expect(scrollSpy._process).toHaveBeenCalled()
resolve()
})
scrollSpyEl.scrollTop = 350
})
it('should only switch "active" class on current target specified w element', done => {
fixtureEl.innerHTML = [
'<div id="root" class="active" style="display: block">',
' <div class="topbar">',
' <div class="topbar-inner">',
' <div class="container" id="ss-target">',
' <ul class="nav">',
' <li class="nav-item"><a href="#masthead">Overview</a></li>',
' <li class="nav-item"><a href="#detail">Detail</a></li>',
' </ul>',
' </div>',
' </div>',
' </div>',
' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
' <div style="height: 200px;">',
' <h4 id="masthead">Overview</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' <div style="height: 200px;">',
' <h4 id="detail">Detail</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' </div>',
'</div>'
].join('')
const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
const rootEl = fixtureEl.querySelector('#root')
const scrollSpy = new ScrollSpy(scrollSpyEl, {
target: fixtureEl.querySelector('#ss-target')
})
spyOn(scrollSpy, '_process').and.callThrough()
scrollSpyEl.addEventListener('scroll', () => {
expect(rootEl.classList.contains('active')).toEqual(true)
expect(scrollSpy._process).toHaveBeenCalled()
done()
})
scrollSpyEl.scrollTop = 350
})
it('should correctly select middle navigation option when large offset is used', done => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>',
' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="one" style="height: 500px;"></div>',
' <div id="two" style="height: 300px;"></div>',
' <div id="three" style="height: 10px;"></div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: Manipulator.position(contentEl).top
})
spyOn(scrollSpy, '_process').and.callThrough()
contentEl.addEventListener('scroll', () => {
expect(fixtureEl.querySelector('#one-link').classList.contains('active')).toEqual(false)
expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true)
expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false)
expect(scrollSpy._process).toHaveBeenCalled()
done()
})
contentEl.scrollTop = 550
})
it('should add the active class to the correct element', done => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>',
' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>',
' </ul>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: () => done()
})
}
scrollSpyEl.scrollTop = 350
})
})
it('should add the active class to the correct element (nav markup)', done => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <nav class="nav">',
' <a class="nav-link" id="a-1" href="#div-1">div 1</a>',
' <a class="nav-link" id="a-2" href="#div-2">div 2</a>',
' </nav>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
it('should only switch "active" class on current target specified w element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="root" class="active" style="display: block">',
' <div class="topbar">',
' <div class="topbar-inner">',
' <div class="container" id="ss-target">',
' <ul class="nav">',
' <li class="nav-item"><a href="#masthead">Overview</a></li>',
' <li class="nav-item"><a href="#detail">Detail</a></li>',
' </ul>',
' </div>',
' </div>',
' </div>',
' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
' <div style="height: 200px;">',
' <h4 id="masthead">Overview</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' <div style="height: 200px;">',
' <h4 id="detail">Detail</h4>',
' <p style="height: 200px;"></p>',
' </div>',
' </div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
const rootEl = fixtureEl.querySelector('#root')
const scrollSpy = new ScrollSpy(scrollSpyEl, {
target: fixtureEl.querySelector('#ss-target')
})
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: () => done()
})
}
spyOn(scrollSpy, '_process').and.callThrough()
scrollSpyEl.addEventListener('scroll', () => {
expect(rootEl).toHaveClass('active')
expect(scrollSpy._process).toHaveBeenCalled()
resolve()
})
scrollSpyEl.scrollTop = 350
})
})
it('should add the active class to the correct element (list-group markup)', done => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <div class="list-group">',
' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>',
' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>',
' </div>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
it('should correctly select middle navigation option when large offset is used', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>',
' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="one" style="height: 500px;"></div>',
' <div id="two" style="height: 300px;"></div>',
' <div id="three" style="height: 10px;"></div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: Manipulator.position(contentEl).top
})
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: () => done()
})
}
spyOn(scrollSpy, '_process').and.callThrough()
contentEl.addEventListener('scroll', () => {
expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active')
expect(fixtureEl.querySelector('#two-link')).toHaveClass('active')
expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active')
expect(scrollSpy._process).toHaveBeenCalled()
resolve()
})
contentEl.scrollTop = 550
})
})
it('should clear selection if above the first section', done => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="spacer" style="height: 100px;"></div>',
' <div id="one" style="height: 100px;"></div>',
' <div id="two" style="height: 100px;"></div>',
' <div id="three" style="height: 100px;"></div>',
' <div id="spacer" style="height: 100px;"></div>',
'</div>'
].join('')
it('should add the active class to the correct element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>',
' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>',
' </ul>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: Manipulator.position(contentEl).top
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: resolve
})
}
})
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
let firstTime = true
contentEl.addEventListener('scroll', () => {
const active = fixtureEl.querySelector('.active')
expect(spy).toHaveBeenCalled()
spy.calls.reset()
if (firstTime) {
expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
expect(active.getAttribute('id')).toEqual('two-link')
firstTime = false
contentEl.scrollTop = 0
} else {
expect(active).toBeNull()
done()
}
})
contentEl.scrollTop = 201
})
it('should not clear selection if above the first section and first section is at the top', done => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="one" style="height: 100px;"></div>',
' <div id="two" style="height: 100px;"></div>',
' <div id="three" style="height: 100px;"></div>',
' <div id="spacer" style="height: 100px;"></div>',
'</div>'
].join('')
it('should add the active class to the correct element (nav markup)', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <nav class="nav">',
' <a class="nav-link" id="a-1" href="#div-1">div 1</a>',
' <a class="nav-link" id="a-2" href="#div-2">div 2</a>',
' </nav>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
const negativeHeight = -10
const startOfSectionTwo = 101
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: contentEl.offsetTop
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: resolve
})
}
})
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
let firstTime = true
contentEl.addEventListener('scroll', () => {
const active = fixtureEl.querySelector('.active')
expect(spy).toHaveBeenCalled()
spy.calls.reset()
if (firstTime) {
expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
expect(active.getAttribute('id')).toEqual('two-link')
firstTime = false
contentEl.scrollTop = negativeHeight
} else {
expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
expect(active.getAttribute('id')).toEqual('one-link')
done()
}
})
contentEl.scrollTop = startOfSectionTwo
})
it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>',
' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>',
' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>',
' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>',
' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>',
' </ul>',
'</nav>',
'<div class="content" style="position: relative; overflow: auto; height: 100px">',
' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>',
' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>',
' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>',
' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>',
' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>',
'</div>'
].join('')
it('should add the active class to the correct element (list-group markup)', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <div class="list-group">',
' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>',
' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>',
' </div>',
'</nav>',
'<div class="content" style="overflow: auto; height: 50px">',
' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
spy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
spy,
cb: resolve
})
}
})
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
})
testElementIsActiveAfterScroll({
elementSelector: '#li-100-5',
targetSelector: '#div-100-5',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-4',
targetSelector: '#div-100-4',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-3',
targetSelector: '#div-100-3',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-2',
targetSelector: '#div-100-2',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-1',
targetSelector: '#div-100-1',
scrollSpy,
spy,
contentEl,
cb: done
})
}
})
}
})
}
})
}
it('should clear selection if above the first section', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="spacer" style="height: 100px;"></div>',
' <div id="one" style="height: 100px;"></div>',
' <div id="two" style="height: 100px;"></div>',
' <div id="three" style="height: 100px;"></div>',
' <div id="spacer" style="height: 100px;"></div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: Manipulator.position(contentEl).top
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
let firstTime = true
contentEl.addEventListener('scroll', () => {
const active = fixtureEl.querySelector('.active')
expect(spy).toHaveBeenCalled()
spy.calls.reset()
if (firstTime) {
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active.getAttribute('id')).toEqual('two-link')
firstTime = false
contentEl.scrollTop = 0
} else {
expect(active).toBeNull()
resolve()
}
})
contentEl.scrollTop = 201
})
})
it('should not clear selection if above the first section and first section is at the top', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="one" style="height: 100px;"></div>',
' <div id="two" style="height: 100px;"></div>',
' <div id="three" style="height: 100px;"></div>',
' <div id="spacer" style="height: 100px;"></div>',
'</div>'
].join('')
const negativeHeight = -10
const startOfSectionTwo = 101
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: contentEl.offsetTop
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
let firstTime = true
contentEl.addEventListener('scroll', () => {
const active = fixtureEl.querySelector('.active')
expect(spy).toHaveBeenCalled()
spy.calls.reset()
if (firstTime) {
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active.getAttribute('id')).toEqual('two-link')
firstTime = false
contentEl.scrollTop = negativeHeight
} else {
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active.getAttribute('id')).toEqual('one-link')
resolve()
}
})
contentEl.scrollTop = startOfSectionTwo
})
})
it('should correctly select navigation element on backward scrolling when each target section height is 100%', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<nav class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>',
' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>',
' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>',
' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>',
' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>',
' </ul>',
'</nav>',
'<div class="content" style="position: relative; overflow: auto; height: 100px">',
' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>',
' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>',
' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>',
' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>',
' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#li-100-5',
targetSelector: '#div-100-5',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-4',
targetSelector: '#div-100-4',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-3',
targetSelector: '#div-100-3',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-2',
targetSelector: '#div-100-2',
scrollSpy,
spy,
contentEl,
cb() {
contentEl.scrollTop = 0
testElementIsActiveAfterScroll({
elementSelector: '#li-100-1',
targetSelector: '#div-100-1',
scrollSpy,
spy,
contentEl,
cb: resolve
})
}
})
}
})
}
})
}
})
})
})
@@ -604,7 +620,7 @@ describe('ScrollSpy', () => {
const scrollspy = ScrollSpy.getInstance(div)
expect(scrollspy).not.toBeNull()
expect(scrollspy._config.offset).toBe(15)
expect(scrollspy._config.offset).toEqual(15)
})
it('should not re create a scrollspy', () => {
@@ -665,7 +681,7 @@ describe('ScrollSpy', () => {
})
it('should return null if there is no instance', () => {
expect(ScrollSpy.getInstance(fixtureEl)).toEqual(null)
expect(ScrollSpy.getInstance(fixtureEl)).toBeNull()
})
})
@@ -686,7 +702,7 @@ describe('ScrollSpy', () => {
const div = fixtureEl.querySelector('div')
expect(ScrollSpy.getInstance(div)).toEqual(null)
expect(ScrollSpy.getInstance(div)).toBeNull()
expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy)
})
@@ -695,7 +711,7 @@ describe('ScrollSpy', () => {
const div = fixtureEl.querySelector('div')
expect(ScrollSpy.getInstance(div)).toEqual(null)
expect(ScrollSpy.getInstance(div)).toBeNull()
const scrollspy = ScrollSpy.getOrCreateInstance(div, {
offset: 1
})
+497 -451
View File
File diff suppressed because it is too large Load Diff
+370 -344
View File
@@ -1,7 +1,5 @@
import Toast from '../../src/toast'
/** Test helpers */
import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
describe('Toast', () => {
let fixtureEl
@@ -38,52 +36,56 @@ describe('Toast', () => {
expect(toastByElement._element).toEqual(toastEl)
})
it('should allow to config in js', done => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should allow to config in js', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl, {
delay: 1
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl, {
delay: 1
})
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).toHaveClass('show')
resolve()
})
toast.show()
})
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl.classList.contains('show')).toEqual(true)
done()
})
toast.show()
})
it('should close toast when close element with data-bs-dismiss attribute is set', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">',
' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
'</div>'
].join('')
it('should close toast when close element with data-bs-dismiss attribute is set', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">',
' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl.classList.contains('show')).toEqual(true)
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).toHaveClass('show')
const button = toastEl.querySelector('.btn-close')
const button = toastEl.querySelector('.btn-close')
button.click()
button.click()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl.classList.contains('show')).toEqual(false)
done()
})
toast.show()
})
})
@@ -113,304 +115,324 @@ describe('Toast', () => {
})
describe('show', () => {
it('should auto hide', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should auto hide', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl.classList.contains('show')).toEqual(false)
done()
})
toast.show()
})
it('should not add fade class', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl.classList.contains('fade')).toEqual(false)
done()
})
toast.show()
})
it('should not trigger shown if show is prevented', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const assertDone = () => {
setTimeout(() => {
expect(toastEl.classList.contains('show')).toEqual(false)
done()
}, 20)
}
toastEl.addEventListener('show.bs.toast', event => {
event.preventDefault()
assertDone()
})
toastEl.addEventListener('shown.bs.toast', () => {
throw new Error('shown event should not be triggered if show is prevented')
})
toast.show()
})
it('should clear timeout if toast is shown again before it is hidden', done => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
toast._config.autohide = false
toastEl.addEventListener('shown.bs.toast', () => {
expect(toast._clearTimeout).toHaveBeenCalled()
expect(toast._timeout).toBeNull()
done()
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
}, toast._config.delay / 2)
spyOn(toast, '_clearTimeout').and.callThrough()
toast.show()
})
})
it('should clear timeout if toast is interacted with mouse', done => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should not add fade class', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = spyOn(toast, '_clearTimeout').and.callThrough()
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
spy.calls.reset()
toastEl.addEventListener('mouseover', () => {
expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
expect(toast._timeout).toBeNull()
done()
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).not.toHaveClass('fade')
resolve()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
toast.show()
})
})
it('should clear timeout if toast is interacted with keyboard', done => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
it('should not trigger shown if show is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = spyOn(toast, '_clearTimeout').and.callThrough()
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
spy.calls.reset()
const assertDone = () => {
setTimeout(() => {
expect(toastEl).not.toHaveClass('show')
resolve()
}, 20)
}
toastEl.addEventListener('focusin', () => {
expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
expect(toast._timeout).toBeNull()
done()
toastEl.addEventListener('show.bs.toast', event => {
event.preventDefault()
assertDone()
})
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
}, toast._config.delay / 2)
toastEl.addEventListener('shown.bs.toast', () => {
reject(new Error('shown event should not be triggered if show is prevented'))
})
toast.show()
toast.show()
})
})
it('should still auto hide after being interacted with mouse and keyboard', done => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
it('should clear timeout if toast is shown again before it is hidden', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
toast._config.autohide = false
toastEl.addEventListener('shown.bs.toast', () => {
expect(toast._clearTimeout).toHaveBeenCalled()
expect(toast._timeout).toBeNull()
resolve()
})
toast.show()
}, toast._config.delay / 2)
spyOn(toast, '_clearTimeout').and.callThrough()
toast.show()
})
})
it('should clear timeout if toast is interacted with mouse', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = spyOn(toast, '_clearTimeout').and.callThrough()
setTimeout(() => {
spy.calls.reset()
toastEl.addEventListener('mouseover', () => {
expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
expect(toast._timeout).toBeNull()
resolve()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
})
})
it('should clear timeout if toast is interacted with keyboard', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = spyOn(toast, '_clearTimeout').and.callThrough()
setTimeout(() => {
spy.calls.reset()
toastEl.addEventListener('focusin', () => {
expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
expect(toast._timeout).toBeNull()
resolve()
})
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
}, toast._config.delay / 2)
toastEl.addEventListener('focusin', () => {
const mouseOutEvent = createEvent('mouseout')
toastEl.dispatchEvent(mouseOutEvent)
})
toastEl.addEventListener('mouseout', () => {
const outsideFocusable = document.getElementById('outside-focusable')
outsideFocusable.focus()
})
toastEl.addEventListener('focusout', () => {
expect(toast._timeout).not.toBeNull()
done()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
toast.show()
})
})
it('should not auto hide if focus leaves but mouse pointer remains inside', done => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
it('should still auto hide after being interacted with mouse and keyboard', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
toastEl.addEventListener('focusin', () => {
const outsideFocusable = document.getElementById('outside-focusable')
outsideFocusable.focus()
})
toastEl.addEventListener('focusin', () => {
const mouseOutEvent = createEvent('mouseout')
toastEl.dispatchEvent(mouseOutEvent)
})
toastEl.addEventListener('focusout', () => {
expect(toast._timeout).toBeNull()
done()
})
toastEl.addEventListener('mouseout', () => {
const outsideFocusable = document.getElementById('outside-focusable')
outsideFocusable.focus()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toastEl.addEventListener('focusout', () => {
expect(toast._timeout).not.toBeNull()
resolve()
})
toast.show()
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
})
})
it('should not auto hide if mouse pointer leaves but focus remains inside', done => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
it('should not auto hide if focus leaves but mouse pointer remains inside', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
toastEl.addEventListener('focusin', () => {
const mouseOutEvent = createEvent('mouseout')
toastEl.dispatchEvent(mouseOutEvent)
})
toastEl.addEventListener('focusin', () => {
const outsideFocusable = document.getElementById('outside-focusable')
outsideFocusable.focus()
})
toastEl.addEventListener('mouseout', () => {
expect(toast._timeout).toBeNull()
done()
})
toastEl.addEventListener('focusout', () => {
expect(toast._timeout).toBeNull()
resolve()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
toast.show()
})
})
it('should not auto hide if mouse pointer leaves but focus remains inside', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<button id="outside-focusable">outside focusable</button>',
'<div class="toast">',
' <div class="toast-body">',
' a simple toast',
' <button>with a button</button>',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
toastEl.addEventListener('focusin', () => {
const mouseOutEvent = createEvent('mouseout')
toastEl.dispatchEvent(mouseOutEvent)
})
toastEl.addEventListener('mouseout', () => {
expect(toast._timeout).toBeNull()
resolve()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
})
})
})
describe('hide', () => {
it('should allow to hide toast manually', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
' </div>'
].join('')
it('should allow to hide toast manually', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
toastEl.addEventListener('shown.bs.toast', () => {
toast.hide()
toastEl.addEventListener('shown.bs.toast', () => {
toast.hide()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl.classList.contains('show')).toEqual(false)
done()
})
toast.show()
})
it('should do nothing when we call hide on a non shown toast', () => {
@@ -426,39 +448,41 @@ describe('Toast', () => {
expect(toastEl.classList.contains).toHaveBeenCalled()
})
it('should not trigger hidden if hide is prevented', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should not trigger hidden if hide is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const assertDone = () => {
setTimeout(() => {
expect(toastEl.classList.contains('show')).toEqual(true)
done()
}, 20)
}
const assertDone = () => {
setTimeout(() => {
expect(toastEl).toHaveClass('show')
resolve()
}, 20)
}
toastEl.addEventListener('shown.bs.toast', () => {
toast.hide()
toastEl.addEventListener('shown.bs.toast', () => {
toast.hide()
})
toastEl.addEventListener('hide.bs.toast', event => {
event.preventDefault()
assertDone()
})
toastEl.addEventListener('hidden.bs.toast', () => {
reject(new Error('hidden event should not be triggered if hide is prevented'))
})
toast.show()
})
toastEl.addEventListener('hide.bs.toast', event => {
event.preventDefault()
assertDone()
})
toastEl.addEventListener('hidden.bs.toast', () => {
throw new Error('hidden event should not be triggered if hide is prevented')
})
toast.show()
})
})
@@ -477,34 +501,36 @@ describe('Toast', () => {
expect(Toast.getInstance(toastEl)).toBeNull()
})
it('should allow to destroy toast and hide it before that', done => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="0" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should allow to destroy toast and hide it before that', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="0" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
const expected = () => {
expect(toastEl.classList.contains('show')).toEqual(true)
expect(Toast.getInstance(toastEl)).not.toBeNull()
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
const expected = () => {
expect(toastEl).toHaveClass('show')
expect(Toast.getInstance(toastEl)).not.toBeNull()
toast.dispose()
toast.dispose()
expect(Toast.getInstance(toastEl)).toBeNull()
expect(toastEl.classList.contains('show')).toEqual(false)
expect(Toast.getInstance(toastEl)).toBeNull()
expect(toastEl).not.toHaveClass('show')
done()
}
resolve()
}
toastEl.addEventListener('shown.bs.toast', () => {
setTimeout(expected, 1)
toastEl.addEventListener('shown.bs.toast', () => {
setTimeout(expected, 1)
})
toast.show()
})
toast.show()
})
})
@@ -584,7 +610,7 @@ describe('Toast', () => {
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toEqual(null)
expect(Toast.getInstance(div)).toBeNull()
})
})
@@ -605,7 +631,7 @@ describe('Toast', () => {
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toEqual(null)
expect(Toast.getInstance(div)).toBeNull()
expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast)
})
@@ -614,7 +640,7 @@ describe('Toast', () => {
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toEqual(null)
expect(Toast.getInstance(div)).toBeNull()
const toast = Toast.getOrCreateInstance(div, {
delay: 1
})
File diff suppressed because it is too large Load Diff
+246 -217
View File
@@ -17,274 +17,303 @@ describe('Backdrop', () => {
clearFixture()
const list = document.querySelectorAll(CLASS_BACKDROP)
list.forEach(el => {
for (const el of list) {
el.remove()
})
}
})
describe('show', () => {
it('if it is "shown", should append the backdrop html once, on show, and contain "show" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show()
instance.show(() => {
expect(getElements().length).toEqual(1)
getElements().forEach(el => {
expect(el.classList.contains(CLASS_NAME_SHOW)).toEqual(true)
it('should append the backdrop html once on show and include the "show" class if it is "shown"', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements()).toHaveSize(0)
instance.show()
instance.show(() => {
expect(getElements()).toHaveSize(1)
for (const el of getElements()) {
expect(el).toHaveClass(CLASS_NAME_SHOW)
}
resolve()
})
done()
})
})
it('if it is not "shown", should not append the backdrop html', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
it('should not append the backdrop html if it is not "shown"', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(0)
done()
expect(getElements()).toHaveSize(0)
instance.show(() => {
expect(getElements()).toHaveSize(0)
resolve()
})
})
})
it('if it is "shown" and "animated", should append the backdrop html once, and contain "fade" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(1)
getElements().forEach(el => {
expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true)
it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements()).toHaveSize(0)
instance.show(() => {
expect(getElements()).toHaveSize(1)
for (const el of getElements()) {
expect(el).toHaveClass(CLASS_NAME_FADE)
}
resolve()
})
done()
})
})
})
describe('hide', () => {
it('should remove the backdrop html', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
it('should remove the backdrop html', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP)
const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(1)
instance.hide(() => {
expect(getElements().length).toEqual(0)
done()
expect(getElements()).toHaveSize(0)
instance.show(() => {
expect(getElements()).toHaveSize(1)
instance.hide(() => {
expect(getElements()).toHaveSize(0)
resolve()
})
})
})
})
it('should remove "show" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const elem = instance._getElement()
it('should remove the "show" class', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const elem = instance._getElement()
instance.show()
instance.hide(() => {
expect(elem.classList.contains(CLASS_NAME_SHOW)).toEqual(false)
done()
})
})
it('if it is not "shown", should not try to remove Node on remove method', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
const spy = spyOn(instance, 'dispose').and.callThrough()
expect(getElements().length).toEqual(0)
expect(instance._isAppended).toEqual(false)
instance.show(() => {
instance.show()
instance.hide(() => {
expect(getElements().length).toEqual(0)
expect(spy).not.toHaveBeenCalled()
expect(instance._isAppended).toEqual(false)
done()
expect(elem).not.toHaveClass(CLASS_NAME_SHOW)
resolve()
})
})
})
it('should not error if the backdrop no longer has a parent', done => {
fixtureEl.innerHTML = '<div id="wrapper"></div>'
it('should not try to remove Node on remove method if it is not "shown"', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
const spy = spyOn(instance, 'dispose').and.callThrough()
const wrapper = fixtureEl.querySelector('#wrapper')
const instance = new Backdrop({
isVisible: true,
isAnimated: true,
rootElement: wrapper
expect(getElements()).toHaveSize(0)
expect(instance._isAppended).toBeFalse()
instance.show(() => {
instance.hide(() => {
expect(getElements()).toHaveSize(0)
expect(spy).not.toHaveBeenCalled()
expect(instance._isAppended).toBeFalse()
resolve()
})
})
})
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
it('should not error if the backdrop no longer has a parent', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div id="wrapper"></div>'
instance.show(() => {
wrapper.remove()
instance.hide(() => {
expect(getElements().length).toEqual(0)
done()
const wrapper = fixtureEl.querySelector('#wrapper')
const instance = new Backdrop({
isVisible: true,
isAnimated: true,
rootElement: wrapper
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
instance.show(() => {
wrapper.remove()
instance.hide(() => {
expect(getElements()).toHaveSize(0)
resolve()
})
})
})
})
})
describe('click callback', () => {
it('it should execute callback on click', done => {
const spy = jasmine.createSpy('spy')
it('should execute callback on click', () => {
return new Promise(resolve => {
const spy = jasmine.createSpy('spy')
const instance = new Backdrop({
isVisible: true,
isAnimated: false,
clickCallback: () => spy()
})
const endTest = () => {
setTimeout(() => {
expect(spy).toHaveBeenCalled()
done()
}, 10)
}
instance.show(() => {
const clickEvent = document.createEvent('MouseEvents')
clickEvent.initEvent('mousedown', true, true)
document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent)
endTest()
})
})
})
describe('animation callbacks', () => {
it('if it is animated, should show and hide backdrop after counting transition duration', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const spy2 = jasmine.createSpy('spy2')
const execDone = () => {
setTimeout(() => {
expect(spy2).toHaveBeenCalledTimes(2)
done()
}, 10)
}
instance.show(spy2)
instance.hide(() => {
spy2()
execDone()
})
expect(spy2).not.toHaveBeenCalled()
})
it('if it is not animated, should show and hide backdrop without delay', done => {
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const spy2 = jasmine.createSpy('spy2')
instance.show(spy2)
instance.hide(spy2)
setTimeout(() => {
expect(spy2).toHaveBeenCalled()
expect(spy).not.toHaveBeenCalled()
done()
}, 10)
})
it('if it is not "shown", should not call delay callbacks', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
instance.show()
instance.hide(() => {
expect(spy).not.toHaveBeenCalled()
done()
})
})
})
describe('Config', () => {
describe('rootElement initialization', () => {
it('Should be appended on "document.body" by default', done => {
const instance = new Backdrop({
isVisible: true
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(document.body)
done()
})
})
it('Should find the rootElement if passed as a string', done => {
const instance = new Backdrop({
isVisible: true,
rootElement: 'body'
isAnimated: false,
clickCallback: () => spy()
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(document.body)
done()
})
})
const endTest = () => {
setTimeout(() => {
expect(spy).toHaveBeenCalled()
resolve()
}, 10)
}
it('Should appended on any element given by the proper config', done => {
fixtureEl.innerHTML = [
'<div id="wrapper">',
'</div>'
].join('')
const wrapper = fixtureEl.querySelector('#wrapper')
const instance = new Backdrop({
isVisible: true,
rootElement: wrapper
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(wrapper)
done()
const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true })
document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent)
endTest()
})
})
})
describe('ClassName', () => {
it('Should be able to have different classNames than default', done => {
const instance = new Backdrop({
isVisible: true,
className: 'foo'
describe('animation callbacks', () => {
it('should show and hide backdrop after counting transition duration if it is animated', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const spy2 = jasmine.createSpy('spy2')
const execDone = () => {
setTimeout(() => {
expect(spy2).toHaveBeenCalledTimes(2)
resolve()
}, 10)
}
instance.show(spy2)
instance.hide(() => {
spy2()
execDone()
})
expect(spy2).not.toHaveBeenCalled()
})
const getElement = () => document.querySelector('.foo')
instance.show(() => {
expect(getElement()).toEqual(instance._getElement())
instance.dispose()
done()
})
it('should show and hide backdrop without a delay if it is not animated', () => {
return new Promise(resolve => {
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const spy2 = jasmine.createSpy('spy2')
instance.show(spy2)
instance.hide(spy2)
setTimeout(() => {
expect(spy2).toHaveBeenCalled()
expect(spy).not.toHaveBeenCalled()
resolve()
}, 10)
})
})
it('should not call delay callbacks if it is not "shown"', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
instance.show()
instance.hide(() => {
expect(spy).not.toHaveBeenCalled()
resolve()
})
})
})
})
describe('Config', () => {
describe('rootElement initialization', () => {
it('should be appended on "document.body" by default', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(document.body)
resolve()
})
})
})
it('should find the rootElement if passed as a string', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
rootElement: 'body'
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(document.body)
resolve()
})
})
})
it('should be appended on any element given by the proper config', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div id="wrapper"></div>'
const wrapper = fixtureEl.querySelector('#wrapper')
const instance = new Backdrop({
isVisible: true,
rootElement: wrapper
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(wrapper)
resolve()
})
})
})
})
describe('ClassName', () => {
it('should allow configuring className', () => {
return new Promise(resolve => {
const instance = new Backdrop({
isVisible: true,
className: 'foo'
})
const getElement = () => document.querySelector('.foo')
instance.show(() => {
expect(getElement()).toEqual(instance._getElement())
instance.dispose()
resolve()
})
})
})
})
})
@@ -33,7 +33,7 @@ describe('Plugin functions', () => {
it('should get Plugin and execute the given method, when a click occurred on data-bs-dismiss="PluginName"', () => {
fixtureEl.innerHTML = [
'<div id="foo" class="test">',
' <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>',
' <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>',
'</div>'
].join('')
@@ -53,7 +53,7 @@ describe('Plugin functions', () => {
it('if data-bs-dismiss="PluginName" hasn\'t got "data-bs-target", "getOrCreateInstance" has to be initialized by closest "plugin.Name" class', () => {
fixtureEl.innerHTML = [
'<div id="foo" class="test">',
' <button type="button" data-bs-dismiss="test"></button>',
' <button type="button" data-bs-dismiss="test"></button>',
'</div>'
].join('')
@@ -73,7 +73,7 @@ describe('Plugin functions', () => {
it('if data-bs-dismiss="PluginName" is disabled, must not trigger function', () => {
fixtureEl.innerHTML = [
'<div id="foo" class="test">',
' <button type="button" disabled data-bs-dismiss="test"></button>',
' <button type="button" disabled data-bs-dismiss="test"></button>',
'</div>'
].join('')
@@ -90,7 +90,7 @@ describe('Plugin functions', () => {
it('should prevent default when the trigger is <a> or <area>', () => {
fixtureEl.innerHTML = [
'<div id="foo" class="test">',
' <a type="button" data-bs-dismiss="test"></a>',
' <a type="button" data-bs-dismiss="test"></a>',
'</div>'
].join('')
+78
View File
@@ -0,0 +1,78 @@
import Config from '../../../src/util/config'
class DummyConfigClass extends Config {
static get NAME() {
return 'dummy'
}
}
describe('Config', () => {
const name = 'dummy'
describe('NAME', () => {
it('should return plugin NAME', () => {
expect(DummyConfigClass.NAME).toEqual(name)
})
})
describe('DefaultType', () => {
it('should return plugin default type', () => {
expect(DummyConfigClass.DefaultType).toEqual(jasmine.any(Object))
})
})
describe('Default', () => {
it('should return plugin defaults', () => {
expect(DummyConfigClass.Default).toEqual(jasmine.any(Object))
})
})
describe('typeCheckConfig', () => {
it('should check type of the config object', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(string|element)'
})
const config = {
toggle: true,
parent: 777
}
const obj = new DummyConfigClass()
expect(() => {
obj._typeCheckConfig(config)
}).toThrowError(TypeError, obj.constructor.NAME.toUpperCase() + ': Option "parent" provided type "number" but expected type "(string|element)".')
})
it('should return null stringified when null is passed', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(null|element)'
})
const obj = new DummyConfigClass()
const config = {
toggle: true,
parent: null
}
obj._typeCheckConfig(config)
expect().nothing()
})
it('should return undefined stringified when undefined is passed', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(undefined|element)'
})
const obj = new DummyConfigClass()
const config = {
toggle: true,
parent: undefined
}
obj._typeCheckConfig(config)
expect().nothing()
})
})
})
+127 -119
View File
@@ -1,7 +1,7 @@
import FocusTrap from '../../../src/util/focustrap'
import EventHandler from '../../../src/dom/event-handler'
import SelectorEngine from '../../../src/dom/selector-engine'
import { clearFixture, getFixture, createEvent } from '../../helpers/fixture'
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
describe('FocusTrap', () => {
let fixtureEl
@@ -41,140 +41,148 @@ describe('FocusTrap', () => {
expect(trapElement.focus).not.toHaveBeenCalled()
})
it('should force focus inside focus trap if it can', done => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="inside">inside</a>',
'</div>'
].join('')
it('should force focus inside focus trap if it can', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="inside">inside</a>',
'</div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const inside = document.getElementById('inside')
const inside = document.getElementById('inside')
const focusInListener = () => {
expect(inside.focus).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
done()
}
const focusInListener = () => {
expect(inside.focus).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
resolve()
}
spyOn(inside, 'focus')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside])
spyOn(inside, 'focus')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside])
document.addEventListener('focusin', focusInListener)
document.addEventListener('focusin', focusInListener)
const focusInEvent = createEvent('focusin', { bubbles: true })
Object.defineProperty(focusInEvent, 'target', {
value: document.getElementById('outside')
const focusInEvent = createEvent('focusin', { bubbles: true })
Object.defineProperty(focusInEvent, 'target', {
value: document.getElementById('outside')
})
document.dispatchEvent(focusInEvent)
})
document.dispatchEvent(focusInEvent)
})
it('should wrap focus around forward on tab', done => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="first">first</a>',
' <a href="#" id="inside">inside</a>',
' <a href="#" id="last">last</a>',
'</div>'
].join('')
it('should wrap focus around forward on tab', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="first">first</a>',
' <a href="#" id="inside">inside</a>',
' <a href="#" id="last">last</a>',
'</div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const first = document.getElementById('first')
const inside = document.getElementById('inside')
const last = document.getElementById('last')
const outside = document.getElementById('outside')
const first = document.getElementById('first')
const inside = document.getElementById('inside')
const last = document.getElementById('last')
const outside = document.getElementById('outside')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
spyOn(first, 'focus').and.callThrough()
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
spyOn(first, 'focus').and.callThrough()
const focusInListener = () => {
expect(first.focus).toHaveBeenCalled()
first.removeEventListener('focusin', focusInListener)
done()
}
const focusInListener = () => {
expect(first.focus).toHaveBeenCalled()
first.removeEventListener('focusin', focusInListener)
resolve()
}
first.addEventListener('focusin', focusInListener)
first.addEventListener('focusin', focusInListener)
const keydown = createEvent('keydown')
keydown.key = 'Tab'
const keydown = createEvent('keydown')
keydown.key = 'Tab'
document.dispatchEvent(keydown)
outside.focus()
})
it('should wrap focus around backwards on shift-tab', done => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="first">first</a>',
' <a href="#" id="inside">inside</a>',
' <a href="#" id="last">last</a>',
'</div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const first = document.getElementById('first')
const inside = document.getElementById('inside')
const last = document.getElementById('last')
const outside = document.getElementById('outside')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
spyOn(last, 'focus').and.callThrough()
const focusInListener = () => {
expect(last.focus).toHaveBeenCalled()
last.removeEventListener('focusin', focusInListener)
done()
}
last.addEventListener('focusin', focusInListener)
const keydown = createEvent('keydown')
keydown.key = 'Tab'
keydown.shiftKey = true
document.dispatchEvent(keydown)
outside.focus()
})
it('should force focus on itself if there is no focusable content', done => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1"></div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const focusInListener = () => {
expect(focustrap._config.trapElement.focus).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
done()
}
spyOn(focustrap._config.trapElement, 'focus')
document.addEventListener('focusin', focusInListener)
const focusInEvent = createEvent('focusin', { bubbles: true })
Object.defineProperty(focusInEvent, 'target', {
value: document.getElementById('outside')
document.dispatchEvent(keydown)
outside.focus()
})
})
document.dispatchEvent(focusInEvent)
it('should wrap focus around backwards on shift-tab', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1">',
' <a href="#" id="first">first</a>',
' <a href="#" id="inside">inside</a>',
' <a href="#" id="last">last</a>',
'</div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const first = document.getElementById('first')
const inside = document.getElementById('inside')
const last = document.getElementById('last')
const outside = document.getElementById('outside')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
spyOn(last, 'focus').and.callThrough()
const focusInListener = () => {
expect(last.focus).toHaveBeenCalled()
last.removeEventListener('focusin', focusInListener)
resolve()
}
last.addEventListener('focusin', focusInListener)
const keydown = createEvent('keydown')
keydown.key = 'Tab'
keydown.shiftKey = true
document.dispatchEvent(keydown)
outside.focus()
})
})
it('should force focus on itself if there is no focusable content', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a href="#" id="outside">outside</a>',
'<div id="focustrap" tabindex="-1"></div>'
].join('')
const trapElement = fixtureEl.querySelector('div')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
const focusInListener = () => {
expect(focustrap._config.trapElement.focus).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
resolve()
}
spyOn(focustrap._config.trapElement, 'focus')
document.addEventListener('focusin', focusInListener)
const focusInEvent = createEvent('focusin', { bubbles: true })
Object.defineProperty(focusInEvent, 'target', {
value: document.getElementById('outside')
})
document.dispatchEvent(focusInEvent)
})
})
})
@@ -182,10 +190,10 @@ describe('FocusTrap', () => {
it('should flag itself as no longer active', () => {
const focustrap = new FocusTrap({ trapElement: fixtureEl })
focustrap.activate()
expect(focustrap._isActive).toBe(true)
expect(focustrap._isActive).toBeTrue()
focustrap.deactivate()
expect(focustrap._isActive).toBe(false)
expect(focustrap._isActive).toBeFalse()
})
it('should remove all event listeners', () => {
+186 -189
View File
@@ -1,7 +1,6 @@
import * as Util from '../../../src/util/index'
/** Test helpers */
import { clearFixture, getFixture } from '../../helpers/fixture'
import { noop } from '../../../src/util/index'
describe('Util', () => {
let fixtureEl
@@ -156,34 +155,35 @@ describe('Util', () => {
})
describe('triggerTransitionEnd', () => {
it('should trigger transitionend event', done => {
fixtureEl.innerHTML = '<div></div>'
it('should trigger transitionend event', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const el = fixtureEl.querySelector('div')
const spy = spyOn(el, 'dispatchEvent').and.callThrough()
const el = fixtureEl.querySelector('div')
const spy = spyOn(el, 'dispatchEvent').and.callThrough()
el.addEventListener('transitionend', () => {
expect(spy).toHaveBeenCalled()
done()
el.addEventListener('transitionend', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
Util.triggerTransitionEnd(el)
})
Util.triggerTransitionEnd(el)
})
})
describe('isElement', () => {
it('should detect if the parameter is an element or not and return Boolean', () => {
fixtureEl.innerHTML =
[
'<div id="foo" class="test"></div>',
'<div id="bar" class="test"></div>'
].join('')
fixtureEl.innerHTML = [
'<div id="foo" class="test"></div>',
'<div id="bar" class="test"></div>'
].join('')
const el = fixtureEl.querySelector('#foo')
expect(Util.isElement(el)).toEqual(true)
expect(Util.isElement({})).toEqual(false)
expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toEqual(false)
expect(Util.isElement(el)).toBeTrue()
expect(Util.isElement({})).toBeFalse()
expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toBeFalse()
})
it('should detect jQuery element', () => {
@@ -195,17 +195,16 @@ describe('Util', () => {
jquery: 'foo'
}
expect(Util.isElement(fakejQuery)).toEqual(true)
expect(Util.isElement(fakejQuery)).toBeTrue()
})
})
describe('getElement', () => {
it('should try to parse element', () => {
fixtureEl.innerHTML =
[
'<div id="foo" class="test"></div>',
'<div id="bar" class="test"></div>'
].join('')
fixtureEl.innerHTML = [
'<div id="foo" class="test"></div>',
'<div id="bar" class="test"></div>'
].join('')
const el = fixtureEl.querySelector('div')
@@ -227,61 +226,14 @@ describe('Util', () => {
})
})
describe('typeCheckConfig', () => {
const namePlugin = 'collapse'
it('should check type of the config object', () => {
const defaultType = {
toggle: 'boolean',
parent: '(string|element)'
}
const config = {
toggle: true,
parent: 777
}
expect(() => {
Util.typeCheckConfig(namePlugin, config, defaultType)
}).toThrowError(TypeError, 'COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".')
})
it('should return null stringified when null is passed', () => {
const defaultType = {
toggle: 'boolean',
parent: '(null|element)'
}
const config = {
toggle: true,
parent: null
}
Util.typeCheckConfig(namePlugin, config, defaultType)
expect().nothing()
})
it('should return undefined stringified when undefined is passed', () => {
const defaultType = {
toggle: 'boolean',
parent: '(undefined|element)'
}
const config = {
toggle: true,
parent: undefined
}
Util.typeCheckConfig(namePlugin, config, defaultType)
expect().nothing()
})
})
describe('isVisible', () => {
it('should return false if the element is not defined', () => {
expect(Util.isVisible(null)).toEqual(false)
expect(Util.isVisible(undefined)).toEqual(false)
expect(Util.isVisible(null)).toBeFalse()
expect(Util.isVisible(undefined)).toBeFalse()
})
it('should return false if the element provided is not a dom element', () => {
expect(Util.isVisible({})).toEqual(false)
expect(Util.isVisible({})).toBeFalse()
})
it('should return false if the element is not visible with display none', () => {
@@ -289,7 +241,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('div')
expect(Util.isVisible(div)).toEqual(false)
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if the element is not visible with visibility hidden', () => {
@@ -297,7 +249,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('div')
expect(Util.isVisible(div)).toEqual(false)
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if an ancestor element is display none', () => {
@@ -313,7 +265,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toEqual(false)
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if an ancestor element is visibility hidden', () => {
@@ -329,7 +281,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toEqual(false)
expect(Util.isVisible(div)).toBeFalse()
})
it('should return true if an ancestor element is visibility hidden, but reverted', () => {
@@ -345,7 +297,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toEqual(true)
expect(Util.isVisible(div)).toBeTrue()
})
it('should return true if the element is visible', () => {
@@ -357,7 +309,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('#element')
expect(Util.isVisible(div)).toEqual(true)
expect(Util.isVisible(div)).toBeTrue()
})
it('should return false if the element is hidden, but not via display or visibility', () => {
@@ -369,20 +321,56 @@ describe('Util', () => {
const div = fixtureEl.querySelector('#element')
expect(Util.isVisible(div)).toEqual(false)
expect(Util.isVisible(div)).toBeFalse()
})
it('should return true if its a closed details element', () => {
fixtureEl.innerHTML = '<details id="element"></details>'
const div = fixtureEl.querySelector('#element')
expect(Util.isVisible(div)).toBeTrue()
})
it('should return true if the element is visible inside an open details element', () => {
fixtureEl.innerHTML = [
'<details open>',
' <div id="element"></div>',
'</details>'
].join('')
const div = fixtureEl.querySelector('#element')
expect(Util.isVisible(div)).toBeTrue()
})
it('should return true if the element is a visible summary in a closed details element', () => {
fixtureEl.innerHTML = [
'<details>',
' <summary id="element-1">',
' <span id="element-2"></span>',
' </summary>',
'</details>'
].join('')
const element1 = fixtureEl.querySelector('#element-1')
const element2 = fixtureEl.querySelector('#element-2')
expect(Util.isVisible(element1)).toBeTrue()
expect(Util.isVisible(element2)).toBeTrue()
})
})
describe('isDisabled', () => {
it('should return true if the element is not defined', () => {
expect(Util.isDisabled(null)).toEqual(true)
expect(Util.isDisabled(undefined)).toEqual(true)
expect(Util.isDisabled()).toEqual(true)
expect(Util.isDisabled(null)).toBeTrue()
expect(Util.isDisabled(undefined)).toBeTrue()
expect(Util.isDisabled()).toBeTrue()
})
it('should return true if the element provided is not a dom element', () => {
expect(Util.isDisabled({})).toEqual(true)
expect(Util.isDisabled('test')).toEqual(true)
expect(Util.isDisabled({})).toBeTrue()
expect(Util.isDisabled('test')).toBeTrue()
})
it('should return true if the element has disabled attribute', () => {
@@ -398,9 +386,9 @@ describe('Util', () => {
const div1 = fixtureEl.querySelector('#element1')
const div2 = fixtureEl.querySelector('#element2')
expect(Util.isDisabled(div)).toEqual(true)
expect(Util.isDisabled(div1)).toEqual(true)
expect(Util.isDisabled(div2)).toEqual(true)
expect(Util.isDisabled(div)).toBeTrue()
expect(Util.isDisabled(div1)).toBeTrue()
expect(Util.isDisabled(div2)).toBeTrue()
})
it('should return false if the element has disabled attribute with "false" value, or doesn\'t have attribute', () => {
@@ -414,8 +402,8 @@ describe('Util', () => {
const div = fixtureEl.querySelector('#element')
const div1 = fixtureEl.querySelector('#element1')
expect(Util.isDisabled(div)).toEqual(false)
expect(Util.isDisabled(div1)).toEqual(false)
expect(Util.isDisabled(div)).toBeFalse()
expect(Util.isDisabled(div1)).toBeFalse()
})
it('should return false if the element is not disabled ', () => {
@@ -429,10 +417,11 @@ describe('Util', () => {
const el = selector => fixtureEl.querySelector(selector)
expect(Util.isDisabled(el('#button'))).toEqual(false)
expect(Util.isDisabled(el('#select'))).toEqual(false)
expect(Util.isDisabled(el('#input'))).toEqual(false)
expect(Util.isDisabled(el('#button'))).toBeFalse()
expect(Util.isDisabled(el('#select'))).toBeFalse()
expect(Util.isDisabled(el('#input'))).toBeFalse()
})
it('should return true if the element has disabled attribute', () => {
fixtureEl.innerHTML = [
'<div>',
@@ -448,12 +437,12 @@ describe('Util', () => {
const el = selector => fixtureEl.querySelector(selector)
expect(Util.isDisabled(el('#input'))).toEqual(true)
expect(Util.isDisabled(el('#input1'))).toEqual(true)
expect(Util.isDisabled(el('#button'))).toEqual(true)
expect(Util.isDisabled(el('#button1'))).toEqual(true)
expect(Util.isDisabled(el('#button2'))).toEqual(true)
expect(Util.isDisabled(el('#input'))).toEqual(true)
expect(Util.isDisabled(el('#input'))).toBeTrue()
expect(Util.isDisabled(el('#input1'))).toBeTrue()
expect(Util.isDisabled(el('#button'))).toBeTrue()
expect(Util.isDisabled(el('#button1'))).toBeTrue()
expect(Util.isDisabled(el('#button2'))).toBeTrue()
expect(Util.isDisabled(el('#input'))).toBeTrue()
})
it('should return true if the element has class "disabled"', () => {
@@ -465,7 +454,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('#element')
expect(Util.isDisabled(div)).toEqual(true)
expect(Util.isDisabled(div)).toBeTrue()
})
it('should return true if the element has class "disabled" but disabled attribute is false', () => {
@@ -477,7 +466,7 @@ describe('Util', () => {
const div = fixtureEl.querySelector('#input')
expect(Util.isDisabled(div)).toEqual(true)
expect(Util.isDisabled(div)).toBeTrue()
})
})
@@ -495,7 +484,7 @@ describe('Util', () => {
spyOn(document.documentElement, 'attachShadow').and.returnValue(null)
expect(Util.findShadowRoot(div)).toEqual(null)
expect(Util.findShadowRoot(div)).toBeNull()
})
it('should return null when we do not find a shadow root', () => {
@@ -507,7 +496,7 @@ describe('Util', () => {
spyOn(document, 'getRootNode').and.returnValue(undefined)
expect(Util.findShadowRoot(document)).toEqual(null)
expect(Util.findShadowRoot(document)).toBeNull()
})
it('should return the shadow root when found', () => {
@@ -534,7 +523,7 @@ describe('Util', () => {
describe('noop', () => {
it('should be a function', () => {
expect(typeof Util.noop).toEqual('function')
expect(Util.noop).toEqual(jasmine.any(Function))
})
})
@@ -571,14 +560,14 @@ describe('Util', () => {
document.body.setAttribute('data-bs-no-jquery', '')
expect(window.jQuery).toEqual(fakejQuery)
expect(Util.getjQuery()).toEqual(null)
expect(Util.getjQuery()).toBeNull()
document.body.removeAttribute('data-bs-no-jquery')
})
it('should not return jQuery if not present', () => {
window.jQuery = undefined
expect(Util.getjQuery()).toEqual(null)
expect(Util.getjQuery()).toBeNull()
})
})
@@ -625,14 +614,14 @@ describe('Util', () => {
})
it('should define a plugin on the jQuery instance', () => {
const pluginMock = function () {}
const pluginMock = Util.noop
pluginMock.NAME = 'test'
pluginMock.jQueryInterface = function () {}
pluginMock.jQueryInterface = Util.noop
Util.defineJQueryPlugin(pluginMock)
expect(fakejQuery.fn.test).toBe(pluginMock.jQueryInterface)
expect(fakejQuery.fn.test.Constructor).toBe(pluginMock)
expect(typeof fakejQuery.fn.test.noConflict).toEqual('function')
expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface)
expect(fakejQuery.fn.test.Constructor).toEqual(pluginMock)
expect(fakejQuery.fn.test.noConflict).toEqual(jasmine.any(Function))
})
})
@@ -672,96 +661,104 @@ describe('Util', () => {
expect(callbackSpy).toHaveBeenCalled()
})
it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', done => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', () => {
return new Promise(resolve => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(callbackSpy, el)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalled()
resolve()
}, 70)
})
Util.executeAfterTransition(callbackSpy, el)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalled()
done()
}, 70)
})
it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', done => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', () => {
return new Promise(resolve => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(callbackSpy, el)
setTimeout(() => {
el.dispatchEvent(new TransitionEvent('transitionend'))
}, 50)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalledTimes(1)
resolve()
}, 70)
})
})
Util.executeAfterTransition(callbackSpy, el)
it('should not trigger a transitionend event if another transitionend event had already happened', () => {
return new Promise(resolve => {
const el = document.createElement('div')
setTimeout(() => {
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(noop, el)
// simulate a event dispatched by the browser
el.dispatchEvent(new TransitionEvent('transitionend'))
}, 50)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalledTimes(1)
done()
}, 70)
const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough()
setTimeout(() => {
// setTimeout should not have triggered another transitionend event.
expect(dispatchSpy).not.toHaveBeenCalled()
resolve()
}, 70)
})
})
it('should not trigger a transitionend event if another transitionend event had already happened', done => {
const el = document.createElement('div')
it('should ignore transitionend events from nested elements', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="outer">',
' <div class="nested"></div>',
'</div>'
].join('')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
const outer = fixtureEl.querySelector('.outer')
const nested = fixtureEl.querySelector('.nested')
const callbackSpy = jasmine.createSpy('callback spy')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(callbackSpy, outer)
nested.dispatchEvent(new TransitionEvent('transitionend', {
bubbles: true
}))
setTimeout(() => {
expect(callbackSpy).not.toHaveBeenCalled()
}, 20)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalled()
resolve()
}, 70)
})
Util.executeAfterTransition(() => {}, el)
// simulate a event dispatched by the browser
el.dispatchEvent(new TransitionEvent('transitionend'))
const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough()
setTimeout(() => {
// setTimeout should not have triggered another transitionend event.
expect(dispatchSpy).not.toHaveBeenCalled()
done()
}, 70)
})
it('should ignore transitionend events from nested elements', done => {
fixtureEl.innerHTML = [
'<div class="outer">',
' <div class="nested"></div>',
'</div>'
].join('')
const outer = fixtureEl.querySelector('.outer')
const nested = fixtureEl.querySelector('.nested')
const callbackSpy = jasmine.createSpy('callback spy')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(callbackSpy, outer)
nested.dispatchEvent(new TransitionEvent('transitionend', {
bubbles: true
}))
setTimeout(() => {
expect(callbackSpy).not.toHaveBeenCalled()
}, 20)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalled()
done()
}, 70)
})
})
+25
View File
@@ -23,6 +23,31 @@ describe('Sanitizer', () => {
expect(result).not.toContain('href="javascript:alert(7)')
})
it('should sanitize template and work with multiple regex', () => {
const template = [
'<div>',
' <a href="javascript:alert(7)" aria-label="This is a link" data-foo="bar">Click me</a>',
' <span>Some content</span>',
'</div>'
].join('')
const myDefaultAllowList = DefaultAllowlist
// With the default allow list
let result = sanitizeHtml(template, myDefaultAllowList, null)
// `data-foo` won't be present
expect(result).not.toContain('data-foo="bar"')
// Add the following regex too
myDefaultAllowList['*'].push(/^data-foo/)
result = sanitizeHtml(template, myDefaultAllowList, null)
expect(result).not.toContain('href="javascript:alert(7)') // This is in the default list
expect(result).toContain('aria-label="This is a link"') // This is in the default list
expect(result).toContain('data-foo="bar"') // We explicitly allow this
})
it('should allow aria attributes and safe attributes', () => {
const template = [
'<div aria-pressed="true">',
+75 -65
View File
@@ -5,9 +5,9 @@ import ScrollBarHelper from '../../../src/util/scrollbar'
describe('ScrollBar', () => {
let fixtureEl
const doc = document.documentElement
const parseInt = arg => Number.parseInt(arg, 10)
const getPaddingX = el => parseInt(window.getComputedStyle(el).paddingRight)
const getMarginX = el => parseInt(window.getComputedStyle(el).marginRight)
const parseIntDecimal = arg => Number.parseInt(arg, 10)
const getPaddingX = el => parseIntDecimal(window.getComputedStyle(el).paddingRight)
const getMarginX = el => parseIntDecimal(window.getComputedStyle(el).marginRight)
const getOverFlow = el => el.style.overflow
const getPaddingAttr = el => Manipulator.getDataAttribute(el, 'padding-right')
const getMarginAttr = el => Manipulator.getDataAttribute(el, 'margin-right')
@@ -24,7 +24,9 @@ describe('ScrollBar', () => {
}
}
const isScrollBarHidden = () => { // IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling. So the tests for scrollbar would fail
// iOS, Android devices and macOS browsers hide scrollbar by default and show it only while scrolling.
// So the tests for scrollbar would fail
const isScrollBarHidden = () => {
const calc = windowCalculations()
return calc.htmlClient === calc.htmlOffset && calc.htmlClient === calc.window
}
@@ -52,28 +54,24 @@ describe('ScrollBar', () => {
it('should return true if body is overflowing', () => {
document.documentElement.style.overflowY = 'scroll'
document.body.style.overflowY = 'scroll'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
const result = new ScrollBarHelper().isOverflowing()
if (isScrollBarHidden()) {
expect(result).toEqual(false)
expect(result).toBeFalse()
} else {
expect(result).toEqual(true)
expect(result).toBeTrue()
}
})
it('should return false if body is not overflowing', () => {
doc.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
const scrollBar = new ScrollBarHelper()
const result = scrollBar.isOverflowing()
expect(result).toEqual(false)
expect(result).toBeFalse()
})
})
@@ -81,13 +79,11 @@ describe('ScrollBar', () => {
it('should return an integer greater than zero, if body is overflowing', () => {
doc.style.overflowY = 'scroll'
document.body.style.overflowY = 'scroll'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
const result = new ScrollBarHelper().getWidth()
if (isScrollBarHidden()) {
expect(result).toBe(0)
expect(result).toEqual(0)
} else {
expect(result).toBeGreaterThan(1)
}
@@ -96,9 +92,7 @@ describe('ScrollBar', () => {
it('should return 0 if body is not overflowing', () => {
document.documentElement.style.overflowY = 'hidden'
document.body.style.overflowY = 'hidden'
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%"></div>'
].join('')
fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
const result = new ScrollBarHelper().getWidth()
@@ -107,11 +101,11 @@ describe('ScrollBar', () => {
})
describe('hide - reset', () => {
it('should adjust the inline padding of fixed elements which are full-width', done => {
it('should adjust the inline padding of fixed elements which are full-width', () => {
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%">' +
'<div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>',
'<div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>',
'<div style="height: 110vh; width: 100%">',
' <div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>',
' <div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>',
'</div>'
].join('')
doc.style.overflowY = 'scroll'
@@ -128,25 +122,44 @@ describe('ScrollBar', () => {
let currentPadding = getPaddingX(fixedEl)
let currentPadding2 = getPaddingX(fixedEl2)
expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`, 'original fixed element padding should be stored in data-bs-padding-right')
expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}px`, 'original fixed element padding should be stored in data-bs-padding-right')
expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening')
expect(currentPadding2).toEqual(expectedPadding2, 'fixed element padding should be adjusted while opening')
expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`)
expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}px`)
expect(currentPadding).toEqual(expectedPadding)
expect(currentPadding2).toEqual(expectedPadding2)
scrollBar.reset()
currentPadding = getPaddingX(fixedEl)
currentPadding2 = getPaddingX(fixedEl2)
expect(getPaddingAttr(fixedEl)).toEqual(null, 'data-bs-padding-right should be cleared after closing')
expect(getPaddingAttr(fixedEl2)).toEqual(null, 'data-bs-padding-right should be cleared after closing')
expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing')
expect(currentPadding2).toEqual(originalPadding2, 'fixed element padding should be reset after closing')
done()
expect(getPaddingAttr(fixedEl)).toBeNull()
expect(getPaddingAttr(fixedEl2)).toBeNull()
expect(currentPadding).toEqual(originalPadding)
expect(currentPadding2).toEqual(originalPadding2)
})
it('should adjust the inline margin and padding of sticky elements', done => {
it('should remove padding & margin if not existed before adjustment', () => {
fixtureEl.innerHTML = [
'<div style="height: 110vh">' +
'<div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>',
'<div style="height: 110vh; width: 100%">',
' <div class="fixed" id="fixed" style="width: 100vw;"></div>',
' <div class="sticky-top" id="sticky" style=" width: 100vw;"></div>',
'</div>'
].join('')
doc.style.overflowY = 'scroll'
const fixedEl = fixtureEl.querySelector('#fixed')
const stickyEl = fixtureEl.querySelector('#sticky')
const scrollBar = new ScrollBarHelper()
scrollBar.hide()
scrollBar.reset()
expect(fixedEl.getAttribute('style').includes('padding-right')).toBeFalse()
expect(stickyEl.getAttribute('style').includes('margin-right')).toBeFalse()
})
it('should adjust the inline margin and padding of sticky elements', () => {
fixtureEl.innerHTML = [
'<div style="height: 110vh">',
' <div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>',
'</div>'
].join('')
doc.style.overflowY = 'scroll'
@@ -159,23 +172,20 @@ describe('ScrollBar', () => {
const expectedPadding = originalPadding + scrollBar.getWidth()
scrollBar.hide()
expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`, 'original sticky element margin should be stored in data-bs-margin-right')
expect(getMarginX(stickyTopEl)).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening')
expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`, 'original sticky element margin should be stored in data-bs-margin-right')
expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding, 'sticky element margin should be adjusted while opening')
expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`)
expect(getMarginX(stickyTopEl)).toEqual(expectedMargin)
expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`)
expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding)
scrollBar.reset()
expect(getMarginAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing')
expect(getMarginX(stickyTopEl)).toEqual(originalMargin, 'sticky element margin should be reset after closing')
expect(getPaddingAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing')
expect(getPaddingX(stickyTopEl)).toEqual(originalPadding, 'sticky element margin should be reset after closing')
done()
expect(getMarginAttr(stickyTopEl)).toBeNull()
expect(getMarginX(stickyTopEl)).toEqual(originalMargin)
expect(getPaddingAttr(stickyTopEl)).toBeNull()
expect(getPaddingX(stickyTopEl)).toEqual(originalPadding)
})
it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => {
fixtureEl.innerHTML = [
'<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>'
].join('')
fixtureEl.innerHTML = '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>'
const stickyTopEl = fixtureEl.querySelector('.sticky-top')
const originalMargin = getMarginX(stickyTopEl)
@@ -187,16 +197,16 @@ describe('ScrollBar', () => {
const currentMargin = getMarginX(stickyTopEl)
const currentPadding = getPaddingX(stickyTopEl)
expect(currentMargin).toEqual(originalMargin, 'sticky element\'s margin should not be adjusted while opening')
expect(currentPadding).toEqual(originalPadding, 'sticky element\'s padding should not be adjusted while opening')
expect(currentMargin).toEqual(originalMargin)
expect(currentPadding).toEqual(originalPadding)
scrollBar.reset()
})
it('should not put data-attribute if element doesn\'t have the proper style property, should just remove style property if element didn\'t had one', () => {
fixtureEl.innerHTML = [
'<div style="height: 110vh; width: 100%">' +
'<div class="sticky-top" id="sticky" style="width: 100vw"></div>',
'<div style="height: 110vh; width: 100%">',
' <div class="sticky-top" id="sticky" style="width: 100vw"></div>',
'</div>'
].join('')
@@ -232,8 +242,8 @@ describe('ScrollBar', () => {
const scrollBarWidth = scrollBar.getWidth()
scrollBar.hide()
expect(getPaddingX(document.body)).toEqual(scrollBarWidth, 'body does not have inline padding set')
expect(document.body.style.color).toEqual('red', 'body still has other inline styles set')
expect(getPaddingX(document.body)).toEqual(scrollBarWidth)
expect(document.body.style.color).toEqual('red')
scrollBar.reset()
})
@@ -243,7 +253,7 @@ describe('ScrollBar', () => {
fixtureEl.innerHTML = [
'<style>',
' body {',
` padding-right: ${styleSheetPadding} }`,
` padding-right: ${styleSheetPadding}`,
' }',
'</style>'
].join('')
@@ -253,7 +263,7 @@ describe('ScrollBar', () => {
el.style.paddingRight = inlineStylePadding
const originalPadding = getPaddingX(el)
expect(originalPadding).toEqual(parseInt(inlineStylePadding)) // Respect only the inline style as it has prevails this of css
expect(originalPadding).toEqual(parseIntDecimal(inlineStylePadding)) // Respect only the inline style as it has prevails this of css
const originalOverFlow = 'auto'
el.style.overflow = originalOverFlow
const scrollBar = new ScrollBarHelper()
@@ -264,7 +274,7 @@ describe('ScrollBar', () => {
const currentPadding = getPaddingX(el)
expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
expect(currentPadding).toEqual(scrollBarWidth + parseInt(inlineStylePadding))
expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(inlineStylePadding))
expect(getPaddingAttr(el)).toEqual(inlineStylePadding)
expect(getOverFlow(el)).toEqual('hidden')
expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
@@ -273,9 +283,9 @@ describe('ScrollBar', () => {
const currentPadding1 = getPaddingX(el)
expect(currentPadding1).toEqual(originalPadding)
expect(getPaddingAttr(el)).toEqual(null)
expect(getPaddingAttr(el)).toBeNull()
expect(getOverFlow(el)).toEqual(originalOverFlow)
expect(getOverFlowAttr(el)).toEqual(null)
expect(getOverFlowAttr(el)).toBeNull()
})
it('should hide scrollbar and reset it to its initial value - respecting css rules', () => {
@@ -283,7 +293,7 @@ describe('ScrollBar', () => {
fixtureEl.innerHTML = [
'<style>',
' body {',
` padding-right: ${styleSheetPadding} }`,
` padding-right: ${styleSheetPadding}`,
' }',
'</style>'
].join('')
@@ -299,7 +309,7 @@ describe('ScrollBar', () => {
const currentPadding = getPaddingX(el)
expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
expect(currentPadding).toEqual(scrollBarWidth + parseInt(styleSheetPadding))
expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(styleSheetPadding))
expect(getPaddingAttr(el)).toBeNull() // We do not have to keep css padding
expect(getOverFlow(el)).toEqual('hidden')
expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
@@ -308,9 +318,9 @@ describe('ScrollBar', () => {
const currentPadding1 = getPaddingX(el)
expect(currentPadding1).toEqual(originalPadding)
expect(getPaddingAttr(el)).toEqual(null)
expect(getPaddingAttr(el)).toBeNull()
expect(getOverFlow(el)).toEqual(originalOverFlow)
expect(getOverFlowAttr(el)).toEqual(null)
expect(getOverFlowAttr(el)).toBeNull()
})
it('should not adjust the inline body padding when it does not overflow', () => {
@@ -324,7 +334,7 @@ describe('ScrollBar', () => {
scrollBar.hide()
const currentPadding = getPaddingX(document.body)
expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted')
expect(currentPadding).toEqual(originalPadding)
scrollBar.reset()
})
@@ -344,7 +354,7 @@ describe('ScrollBar', () => {
const currentPadding = getPaddingX(document.body)
expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted')
expect(currentPadding).toEqual(originalPadding)
scrollBar.reset()
})
+291
View File
@@ -0,0 +1,291 @@
import { clearFixture, getFixture } from '../../helpers/fixture'
import EventHandler from '../../../src/dom/event-handler'
import Swipe from '../../../src/util/swipe'
import { noop } from '../../../src/util'
describe('Swipe', () => {
const { Simulator, PointerEvent } = window
const originWinPointerEvent = PointerEvent
const supportPointerEvent = Boolean(PointerEvent)
let fixtureEl
let swipeEl
const clearPointerEvents = () => {
window.PointerEvent = null
}
const restorePointerEvents = () => {
window.PointerEvent = originWinPointerEvent
}
// The headless browser does not support touch events, so we need to fake it
// in order to test that touch events are added properly
const defineDocumentElementOntouchstart = () => {
document.documentElement.ontouchstart = noop
}
const deleteDocumentElementOntouchstart = () => {
delete document.documentElement.ontouchstart
}
const mockSwipeGesture = (element, options = {}, type = 'touch') => {
Simulator.setType(type)
const _options = { deltaX: 0, deltaY: 0, ...options }
Simulator.gestures.swipe(element, _options)
}
beforeEach(() => {
fixtureEl = getFixture()
const cssStyle = [
'<style>',
' #fixture .pointer-event {',
' touch-action: pan-y;',
' }',
' #fixture div {',
' width: 300px;',
' height: 300px;',
' }',
'</style>'
].join('')
fixtureEl.innerHTML = `<div id="swipeEl"></div>${cssStyle}`
swipeEl = fixtureEl.querySelector('div')
})
afterEach(() => {
clearFixture()
deleteDocumentElementOntouchstart()
})
describe('constructor', () => {
it('should add touch event listeners by default', () => {
defineDocumentElementOntouchstart()
spyOn(Swipe.prototype, '_initEvents').and.callThrough()
const swipe = new Swipe(swipeEl)
expect(swipe._initEvents).toHaveBeenCalled()
})
it('should not add touch event listeners if touch is not supported', () => {
spyOn(Swipe, 'isSupported').and.returnValue(false)
spyOn(Swipe.prototype, '_initEvents').and.callThrough()
const swipe = new Swipe(swipeEl)
expect(swipe._initEvents).not.toHaveBeenCalled()
})
})
describe('Config', () => {
it('Test leftCallback', () => {
return new Promise(resolve => {
const spyRight = jasmine.createSpy('spy')
clearPointerEvents()
defineDocumentElementOntouchstart()
// eslint-disable-next-line no-new
new Swipe(swipeEl, {
leftCallback() {
expect(spyRight).not.toHaveBeenCalled()
restorePointerEvents()
resolve()
},
rightCallback: spyRight
})
mockSwipeGesture(swipeEl, {
pos: [300, 10],
deltaX: -300
})
})
})
it('Test rightCallback', () => {
return new Promise(resolve => {
const spyLeft = jasmine.createSpy('spy')
clearPointerEvents()
defineDocumentElementOntouchstart()
// eslint-disable-next-line no-new
new Swipe(swipeEl, {
rightCallback() {
expect(spyLeft).not.toHaveBeenCalled()
restorePointerEvents()
resolve()
},
leftCallback: spyLeft
})
mockSwipeGesture(swipeEl, {
pos: [10, 10],
deltaX: 300
})
})
})
it('Test endCallback', () => {
return new Promise(resolve => {
clearPointerEvents()
defineDocumentElementOntouchstart()
let isFirstTime = true
const callback = () => {
if (isFirstTime) {
isFirstTime = false
return
}
expect().nothing()
restorePointerEvents()
resolve()
}
// eslint-disable-next-line no-new
new Swipe(swipeEl, {
endCallback: callback
})
mockSwipeGesture(swipeEl, {
pos: [10, 10],
deltaX: 300
})
mockSwipeGesture(swipeEl, {
pos: [300, 10],
deltaX: -300
})
})
})
})
describe('Functionality on PointerEvents', () => {
it('should not allow pinch with touch events', () => {
Simulator.setType('touch')
clearPointerEvents()
deleteDocumentElementOntouchstart()
const swipe = new Swipe(swipeEl)
spyOn(swipe, '_handleSwipe')
mockSwipeGesture(swipeEl, {
pos: [300, 10],
deltaX: -300,
deltaY: 0,
touches: 2
})
restorePointerEvents()
expect(swipe._handleSwipe).not.toHaveBeenCalled()
})
it('should allow swipeRight and call "rightCallback" with pointer events', () => {
return new Promise(resolve => {
if (!supportPointerEvent) {
expect().nothing()
resolve()
return
}
const style = '#fixture .pointer-event { touch-action: none !important; }'
fixtureEl.innerHTML += style
defineDocumentElementOntouchstart()
// eslint-disable-next-line no-new
new Swipe(swipeEl, {
rightCallback() {
deleteDocumentElementOntouchstart()
expect().nothing()
resolve()
}
})
mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer')
})
})
it('should allow swipeLeft and call "leftCallback" with pointer events', () => {
return new Promise(resolve => {
if (!supportPointerEvent) {
expect().nothing()
resolve()
return
}
const style = '#fixture .pointer-event { touch-action: none !important; }'
fixtureEl.innerHTML += style
defineDocumentElementOntouchstart()
// eslint-disable-next-line no-new
new Swipe(swipeEl, {
leftCallback() {
expect().nothing()
deleteDocumentElementOntouchstart()
resolve()
}
})
mockSwipeGesture(swipeEl, {
pos: [300, 10],
deltaX: -300
}, 'pointer')
})
})
})
describe('Dispose', () => {
it('should call EventHandler.off', () => {
defineDocumentElementOntouchstart()
spyOn(EventHandler, 'off').and.callThrough()
const swipe = new Swipe(swipeEl)
swipe.dispose()
expect(EventHandler.off).toHaveBeenCalledWith(swipeEl, '.bs.swipe')
})
it('should destroy', () => {
const addEventSpy = spyOn(fixtureEl, 'addEventListener').and.callThrough()
const removeEventSpy = spyOn(EventHandler, 'off').and.callThrough()
defineDocumentElementOntouchstart()
const swipe = new Swipe(fixtureEl)
const expectedArgs =
swipe._supportPointerEvents ?
[
['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
] :
[
['touchstart', jasmine.any(Function), jasmine.any(Boolean)],
['touchmove', jasmine.any(Function), jasmine.any(Boolean)],
['touchend', jasmine.any(Function), jasmine.any(Boolean)]
]
expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs)
swipe.dispose()
expect(removeEventSpy).toHaveBeenCalledWith(fixtureEl, '.bs.swipe')
deleteDocumentElementOntouchstart()
})
})
describe('"isSupported" static', () => {
it('should return "true" if "touchstart" exists in document element)', () => {
Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
defineDocumentElementOntouchstart()
expect(Swipe.isSupported()).toBeTrue()
})
it('should return "false" if "touchstart" not exists in document element and "navigator.maxTouchPoints" are zero (0)', () => {
Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
deleteDocumentElementOntouchstart()
if ('ontouchstart' in document.documentElement) {
expect().nothing()
return
}
expect(Swipe.isSupported()).toBeFalse()
})
})
})
+306
View File
@@ -0,0 +1,306 @@
import { clearFixture, getFixture } from '../../helpers/fixture'
import TemplateFactory from '../../../src/util/template-factory'
describe('TemplateFactory', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('NAME', () => {
it('should return plugin NAME', () => {
expect(TemplateFactory.NAME).toEqual('TemplateFactory')
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(TemplateFactory.Default).toEqual(jasmine.any(Object))
})
})
describe('toHtml', () => {
describe('Sanitization', () => {
it('should use "sanitizeHtml" to sanitize template', () => {
const factory = new TemplateFactory({
sanitize: true,
template: '<div><a href="javascript:alert(7)">Click me</a></div>'
})
const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
expect(factory.toHtml().innerHTML).not.toContain('href="javascript:alert(7)')
expect(spy).toHaveBeenCalled()
})
it('should not sanitize template', () => {
const factory = new TemplateFactory({
sanitize: false,
template: '<div><a href="javascript:alert(7)">Click me</a></div>'
})
const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
expect(factory.toHtml().innerHTML).toContain('href="javascript:alert(7)')
expect(spy).toHaveBeenCalled()
})
it('should use "sanitizeHtml" to sanitize content', () => {
const factory = new TemplateFactory({
sanitize: true,
html: true,
template: '<div id="foo"></div>',
content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
})
expect(factory.toHtml().innerHTML).not.toContain('href="javascript:alert(7)')
})
it('should not sanitize content', () => {
const factory = new TemplateFactory({
sanitize: false,
html: true,
template: '<div id="foo"></div>',
content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
})
expect(factory.toHtml().innerHTML).toContain('href="javascript:alert(7)')
})
it('should sanitize content only if "config.html" is enabled', () => {
const factory = new TemplateFactory({
sanitize: true,
html: false,
template: '<div id="foo"></div>',
content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
})
const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
expect(spy).not.toHaveBeenCalled()
})
})
describe('Extra Class', () => {
it('should add extra class', () => {
const factory = new TemplateFactory({
extraClass: 'testClass'
})
expect(factory.toHtml()).toHaveClass('testClass')
})
it('should add extra classes', () => {
const factory = new TemplateFactory({
extraClass: 'testClass testClass2'
})
expect(factory.toHtml()).toHaveClass('testClass')
expect(factory.toHtml()).toHaveClass('testClass2')
})
it('should resolve class if function is given', () => {
const factory = new TemplateFactory({
extraClass(arg) {
expect(arg).toEqual(factory)
return 'testClass'
}
})
expect(factory.toHtml()).toHaveClass('testClass')
})
})
})
describe('Content', () => {
it('add simple text content', () => {
const template = [
'<div>',
' <div class="foo"></div>',
' <div class="foo2"></div>',
'</div>'
].join('')
const factory = new TemplateFactory({
template,
content: {
'.foo': 'bar',
'.foo2': 'bar2'
}
})
const html = factory.toHtml()
expect(html.querySelector('.foo').textContent).toEqual('bar')
expect(html.querySelector('.foo2').textContent).toEqual('bar2')
})
it('should not fill template if selector not exists', () => {
const factory = new TemplateFactory({
sanitize: true,
html: true,
template: '<div id="foo"></div>',
content: { '#bar': 'test' }
})
expect(factory.toHtml().outerHTML).toEqual('<div id="foo"></div>')
})
it('should remove template selector, if content is null', () => {
const factory = new TemplateFactory({
sanitize: true,
html: true,
template: '<div><div id="foo"></div></div>',
content: { '#foo': null }
})
expect(factory.toHtml().outerHTML).toEqual('<div></div>')
})
it('should resolve content if is function', () => {
const factory = new TemplateFactory({
sanitize: true,
html: true,
template: '<div><div id="foo"></div></div>',
content: { '#foo': () => null }
})
expect(factory.toHtml().outerHTML).toEqual('<div></div>')
})
it('if content is element and "config.html=false", should put content\'s textContent', () => {
fixtureEl.innerHTML = '<div>foo<span>bar</span></div>'
const contentElement = fixtureEl.querySelector('div')
const factory = new TemplateFactory({
html: false,
template: '<div><div id="foo"></div></div>',
content: { '#foo': contentElement }
})
const fooEl = factory.toHtml().querySelector('#foo')
expect(fooEl.innerHTML).not.toEqual(contentElement.innerHTML)
expect(fooEl.textContent).toEqual(contentElement.textContent)
expect(fooEl.textContent).toEqual('foobar')
})
it('if content is element and "config.html=true", should put content\'s outerHtml as child', () => {
fixtureEl.innerHTML = '<div>foo<span>bar</span></div>'
const contentElement = fixtureEl.querySelector('div')
const factory = new TemplateFactory({
html: true,
template: '<div><div id="foo"></div></div>',
content: { '#foo': contentElement }
})
const fooEl = factory.toHtml().querySelector('#foo')
expect(fooEl.innerHTML).toEqual(contentElement.outerHTML)
expect(fooEl.textContent).toEqual(contentElement.textContent)
})
})
describe('getContent', () => {
it('should get content as array', () => {
const factory = new TemplateFactory({
content: {
'.foo': 'bar',
'.foo2': 'bar2'
}
})
expect(factory.getContent()).toEqual(['bar', 'bar2'])
})
it('should filter empties', () => {
const factory = new TemplateFactory({
content: {
'.foo': 'bar',
'.foo2': '',
'.foo3': null,
'.foo4': () => 2,
'.foo5': () => null
}
})
expect(factory.getContent()).toEqual(['bar', 2])
})
})
describe('hasContent', () => {
it('should return true, if it has', () => {
const factory = new TemplateFactory({
content: {
'.foo': 'bar',
'.foo2': 'bar2',
'.foo3': ''
}
})
expect(factory.hasContent()).toBeTrue()
})
it('should return false, if filtered content is empty', () => {
const factory = new TemplateFactory({
content: {
'.foo2': '',
'.foo3': null,
'.foo4': () => null
}
})
expect(factory.hasContent()).toBeFalse()
})
})
describe('changeContent', () => {
it('should change Content', () => {
const template = [
'<div>',
' <div class="foo"></div>',
' <div class="foo2"></div>',
'</div>'
].join('')
const factory = new TemplateFactory({
template,
content: {
'.foo': 'bar',
'.foo2': 'bar2'
}
})
const html = selector => factory.toHtml().querySelector(selector).textContent
expect(html('.foo')).toEqual('bar')
expect(html('.foo2')).toEqual('bar2')
factory.changeContent({
'.foo': 'test',
'.foo2': 'test2'
})
expect(html('.foo')).toEqual('test')
expect(html('.foo2')).toEqual('test2')
})
it('should change only the given, content', () => {
const template = [
'<div>',
' <div class="foo"></div>',
' <div class="foo2"></div>',
'</div>'
].join('')
const factory = new TemplateFactory({
template,
content: {
'.foo': 'bar',
'.foo2': 'bar2'
}
})
const html = selector => factory.toHtml().querySelector(selector).textContent
expect(html('.foo')).toEqual('bar')
expect(html('.foo2')).toEqual('bar2')
factory.changeContent({
'.foo': 'test',
'.wrong': 'wrong'
})
expect(html('.foo')).toEqual('test')
expect(html('.foo2')).toEqual('bar2')
})
})
})
+1 -5
View File
@@ -43,10 +43,6 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/alert.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+1 -5
View File
@@ -44,10 +44,6 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/button.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+1 -6
View File
@@ -45,12 +45,7 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/carousel.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
<script>
var t0
var t1
+1 -6
View File
@@ -71,11 +71,6 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/collapse.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+2 -9
View File
@@ -10,7 +10,7 @@
<div class="container">
<h1>Dropdown <small>Bootstrap Visual Test</small></h1>
<nav class="navbar navbar-expand-md navbar-light bg-light">
<nav class="navbar navbar-expand-md bg-light">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@@ -200,13 +200,6 @@
</div>
</div>
<script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/dropdown.js"></script>
<script src="../../dist/collapse.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+10 -20
View File
@@ -199,16 +199,7 @@
</button>
</div>
<script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/modal.js"></script>
<script src="../../dist/collapse.js"></script>
<script src="../../dist/tooltip.js"></script>
<script src="../../dist/popover.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
<script>
var ffBugTestResult = document.getElementById('ff-bug-test-result')
var firefoxTestDone = false
@@ -220,15 +211,14 @@
}
}
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
.forEach(function (popover) {
new Popover(popover)
})
var tooltipList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipList.forEach(function (tooltip) {
new Tooltip(tooltip)
})
var popoverElements = document.querySelectorAll('[data-bs-toggle="popover"]')
for (const popoverEl of popoverElements) {
new bootstrap.Popover(popoverEl)
}
var tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]')
for (const tooltipEl of tooltipElements) {
new bootstrap.Tooltip(tooltipEl)
}
var tallToggle = document.getElementById('tall-toggle')
var tall = document.getElementById('tall')
@@ -250,7 +240,7 @@
ffBugInput.addEventListener('focus', handlerClickFfBugInput)
var btnPreventModal = document.getElementById('btnPreventModal')
var modalFf = new Modal(firefoxModal)
var modalFf = new bootstrap.Modal(firefoxModal)
btnPreventModal.addEventListener('click', function () {
function shownFirefoxModal() {
+5 -12
View File
@@ -31,19 +31,12 @@
</button>
</div>
<script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/tooltip.js"></script>
<script src="../../dist/popover.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
<script>
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
.forEach(function (popover) {
new Popover(popover)
})
var popoverElements = document.querySelectorAll('[data-bs-toggle="popover"]')
for (const popoverEl of popoverElements) {
new bootstrap.Popover(popoverEl)
}
</script>
</body>
</html>
+1 -8
View File
@@ -86,13 +86,6 @@
<p>Ad leggings keytar, brunch id art party dolor labore.</p>
</div>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/scrollspy.js"></script>
<script src="../../dist/dropdown.js"></script>
<script src="../../dist/collapse.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+1 -6
View File
@@ -218,11 +218,6 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/tab.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
</body>
</html>
+19 -23
View File
@@ -50,33 +50,29 @@
</div>
</div>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/toast.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
<script>
window.addEventListener('load', function () {
Array.from(document.querySelectorAll('.toast'))
.forEach(function (toastNode) {
new Toast(toastNode)
})
var toastElements = document.querySelectorAll('.toast')
for (const toastEl of toastElements) {
new bootstrap.Toast(toastEl)
}
document.getElementById('btnShowToast').addEventListener('click', function () {
Array.from(document.querySelectorAll('.toast'))
.forEach(function (toastNode) {
var toast = Toast.getInstance(toastNode)
toast.show()
})
})
document.getElementById('btnShowToast').addEventListener('click', function () {
var toastElements = document.querySelectorAll('.toast')
for (const toastEl of toastElements) {
var toast = bootstrap.Toast.getInstance(toastEl)
toast.show()
}
})
document.getElementById('btnHideToast').addEventListener('click', function () {
Array.from(document.querySelectorAll('.toast'))
.forEach(function (toastNode) {
var toast = Toast.getInstance(toastNode)
toast.hide()
})
})
document.getElementById('btnHideToast').addEventListener('click', function () {
var toastElements = document.querySelectorAll('.toast')
for (const toastEl of toastElements) {
var toast = bootstrap.Toast.getInstance(toastEl)
toast.hide()
}
})
})
</script>
</body>
+9 -15
View File
@@ -68,13 +68,7 @@
<div id="customContainer"></div>
</div>
<script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/base-component.js"></script>
<script src="../../dist/tooltip.js"></script>
<script src="../../../dist/js/bootstrap.bundle.js"></script>
<script>
if (typeof document.body.attachShadow === 'function') {
var shadowRoot = document.getElementById('shadow').attachShadow({ mode: 'open' })
@@ -86,24 +80,24 @@
' Tooltip on top in a shadow dom' +
'</button>'
var firstChildTooltip = new Tooltip(shadowRoot.firstChild)
var secondChildTooltip = new Tooltip(shadowRoot.getElementById('secondTooltip'), {
var firstChildTooltip = new bootstrap.Tooltip(shadowRoot.firstChild)
var secondChildTooltip = new bootstrap.Tooltip(shadowRoot.getElementById('secondTooltip'), {
container: shadowRoot
})
}
[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.forEach(function (tooltip) {
new Tooltip(tooltip)
})
var tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]')
for (const tooltipEl of tooltipElements) {
new bootstrap.Tooltip(tooltipEl)
}
var tooltipElement = document.getElementById('tooltipElement')
var tooltipElementInstance = new Tooltip(tooltipElement, {
var tooltipElementInstance = new bootstrap.Tooltip(tooltipElement, {
container: document.getElementById('customContainer')
})
var target = document.getElementById('target')
var targetTooltip = new Tooltip(target, {
var targetTooltip = new bootstrap.Tooltip(target, {
placement : 'top',
trigger : 'manual'
})
+1 -1
View File
@@ -14,7 +14,7 @@
<projectUrl>https://getbootstrap.com/</projectUrl>
<icon>bootstrap.png</icon>
<license type="file">LICENSE.txt</license>
<copyright>Copyright 2017-2021</copyright>
<copyright>Copyright 2017-2022</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<tags>css mobile-first responsive front-end framework web</tags>
<contentFiles>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

+1 -1
View File
@@ -14,7 +14,7 @@
<projectUrl>https://getbootstrap.com/</projectUrl>
<icon>bootstrap.png</icon>
<license type="file">LICENSE.txt</license>
<copyright>Copyright 2017-2021</copyright>
<copyright>Copyright 2017-2022</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<tags>css sass mobile-first responsive front-end framework web</tags>
<contentFiles>
+12231 -3697
View File
File diff suppressed because it is too large Load Diff

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