move to scss (incomplete)
All checks were successful
Build and Push Docker Image / build (push) Successful in 42s

incomplete, moving to scss
This commit is contained in:
Corban-Lee Jones 2024-10-13 23:36:42 +01:00
parent 36a744159f
commit 38b8184499
218 changed files with 36744 additions and 49 deletions

View File

@ -1,3 +1,5 @@
@import "./sidebar.scss";
@keyframes bump {
0% {
transform: translateY(0);

View File

@ -1,4 +1,104 @@
// .sidebar-backdrop {
// position: fixed;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// backdrop-filter: blur(10px);
// background-color: rgba(var(--bs-secondary-rgb), 0.50);
// z-index: 998;
// display: none; /* Must start as hidden! */
// }
// .reveal-sidebar-btn {
// z-index: 998;
// display: none;
// position: fixed;
// bottom: 1rem;
// right: 1rem;
// // @include media-breakpoint-down(xl) {
// // display: block;
// // }
// }
// .sidebar {
// display: flex;
// flex-direction: column;
// flex-shrink: 0;
// width: 300px;
// color: var(--bs-body-color);
// background-color: var(--bs-secondary-bg-subtle);
// transition: transform 0.3s ease-in-out;
// z-index: 999;
// .sidebar-divider {
// }
// .sidebar-header {
// .sidebar-header-link {
// }
// .sidebar-logo {
// }
// .sidebar-title {
// }
// }
// .sidebar-content {
// .sidebar-placeholder {
// .sidebar-placeholder-image {
// }
// .sidebar-placeholder-data {
// >.placeholder {
// }
// }
// }
// .sidebar-item {
// &:not(:disabled):hover,
// &:not(:disabled):focus,
// &.active {
// }
// &.spot::before {
// &:not(:disabled):hover,
// &:not(:disabled):focus,
// &.active {
// }
// }
// }
// }
// }
/* Backdrop */
.sidebar-backdrop {
@ -25,7 +125,7 @@
}
/* Hide Sidebar Button */
/* Hide Sidebar Button */
.sidebar .btn-close {
display: none;
@ -212,28 +312,6 @@
background-color: var(--bs-info);
}
/* .sidebar .sidebar-content .sidebar-item.is-not-operational::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translate(-100%, -50%);
background-color: var(--bs-danger);
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
transition: 0.1s ease;
}
.sidebar .sidebar-content .sidebar-item.is-not-operational:not(:disabled):hover::before,
.sidebar .sidebar-content .sidebar-item.is-not-operational:not(:disabled):focus::before,
.sidebar .sidebar-content .sidebar-item.is-not-operational.active::before {
transition: 0.15s ease;
width: 0.25rem;
height: 60px;
border-radius: 0.25rem 0 0 0.25rem;
} */
.sidebar .sidebar-content .sidebar-item .sidebar-item-image {
display: flex;
justify-content: center;

View File

@ -1,12 +1,14 @@
{% extends 'base.html' %}
{% load static %}
{% load compress %}
{% block title %}{% endblock title %}
{% block stylesheets %}
<link type="text/css" rel="stylesheet" href="{% static '/home/css/index.css' %}">
<link type="text/css" rel="stylesheet" href="{% static '/home/css/sidebar.css' %}">
{% compress css %}
<link type="text/x-scss" rel="stylesheet" href="{% static '/home/scss/index.scss' %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% static '/home/css/tables.css' %}">
<link type="text/css" rel="stylesheet" href="{% static '/css/select2.css' %}">
{% endblock stylesheets %}

View File

@ -1,6 +1,5 @@
# -*- encoding: utf-8 -*-
import os
import environ
import logging
from pathlib import Path
@ -14,6 +13,9 @@ VERSION = "0.3.4"
# BASE_DIR is the root of the project, all paths should be constructed from it using pathlib
BASE_DIR = Path(__file__).parent.parent
# region Env Vars
# Create an environment and read variables from .env file
env = environ.Env(DEBUG=(bool, True))
environ.Env.read_env(BASE_DIR / ".env")
@ -27,6 +29,9 @@ for var in required_env_vars:
if not env(var, default=None):
log.warning("Required environment variable %s is not set, the application will fail!", var)
# region Security
# SECURITY WARNING: This is sensitive data, keep secure!
SECRET_KEY = env('SECRET_KEY', default="unsecure-default-secret-key")
@ -37,7 +42,8 @@ DEBUG = env('DEBUG')
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "pyrss-website", env("HOST", default="127.0.0.1")]
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "http://pyrss-website", "https://" + env("HOST", default="127.0.0.1")]
# Application definition
# region App definition
INSTALLED_APPS = [
'django.contrib.admin',
@ -49,6 +55,7 @@ INSTALLED_APPS = [
'rest_framework',
"rest_framework.authtoken",
"django_filters",
"compressor",
'apps.api',
'apps.home',
'apps.authentication',
@ -95,7 +102,7 @@ TEMPLATES = [
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# region Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DB_ENGINE = env("DB_ENGINE", default=None)
@ -117,7 +124,7 @@ else:
DATABASES = { "default": db_data }
# Password validation
# region Passwd validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
@ -128,16 +135,15 @@ AUTH_PASSWORD_VALIDATORS = [
]
AUTHENTICATION_BACKENDS = [
# "django.contrib.auth.backends.ModelBackend",
"apps.authentication.backends.DiscordAuthenticationBackend"
]
# Discord Related Settings
# region Discord Settings
BOT_TOKEN = env("BOT_TOKEN", default=None)
DISCORD_KEY = env("DISCORD_KEY", default=None)
DISCORD_SECRET = env("DISCORD_SECRET", default=None)
DISCORD_SCOPES = env("DISCORD_SCOPES", default="identity,guilds").split(",") # ["identify", "guilds"]
DISCORD_SCOPES = env("DISCORD_SCOPES", default="identity,guilds").split(",") # ["identify", "guilds"]
DISCORD_CODE_EXCHANGE_REQUEST = {
"headers": {"Content-Type": "application/x-www-form-urlencoded"},
"data": {
@ -160,12 +166,11 @@ DISCORD_API_URL = env("DISCORD_API_URL", default="https://discord.com/api/v10")
DISCORD_OAUTH2_URL = env("DISCORD_OAUTH2_URL", default=None)
SUPERUSER_IDS = env("SUPERUSER_IDS", default="").split(",")
# Logging
# region Logging
# https://docs.djangoproject.com/en/5.0/topics/logging/
LOGGING_DIR = BASE_DIR / "logs"
LOGGING_DIR.mkdir(exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -218,34 +223,41 @@ LOGGING = {
}
# Internationalization
# region Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-gb'
TIME_ZONE = 'Europe/London'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# region Static files
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = '/static/'
# Extra places for collectstatic to find static files.
STATICFILES_DIRS = (
BASE_DIR / 'static',
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
STATICFILES_DIRS = (BASE_DIR / "static",) # Extra places for collectstatic to find static files.
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"compressor.finders.CompressorFinder",
)
# Media Files
# region SASS Compression
COMPRESS_ENABLED = True
COMPRESS_PRECOMPILERS = [("text/x-scss", "django_libsass.SassCompiler")]
COMPRESS_CSS_FILTERS = ["compressor.filters.css_default.CssAbsoluteIdentifier"]
# region Media Files
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'
# Django Rest Framework
# region Rest Framework
# https://www.django-rest-framework.org/
REST_FRAMEWORK = {
@ -265,7 +277,9 @@ REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "apps.api.exceptions.conflict_exception_handler"
}
# Data logic
# region Data logic
MAX_SUBSCRIPTIONS_PER_SERVER = env("MAX_SUBSCRIPTIONS_PER_SERVER", default=15)
MAX_FILTERS_PER_SERVER = env("MAX_FILTERS_PER_SERVER", default=15)
MAX_MESSAGE_STYLES_PER_SERVER = env("MAX_MESSAGE_STYLES_PER_SERVER", default=15)

View File

@ -4,17 +4,23 @@ bump2version==1.0.1
certifi==2024.2.2
charset-normalizer==3.3.2
Django==5.0.4
django-appconf==1.0.6
django-compressor==4.5.1
django-environ==0.11.2
django-filter==24.2
django-libsass==0.9
djangorestframework==3.15.1
gunicorn==23.0.0
h11==0.14.0
httpcore==1.0.5
httpx==0.27.2
idna==3.7
libsass==0.23.0
packaging==24.1
psycopg2==2.9.9
rcssmin==1.1.2
requests==2.31.0
rjsmin==1.2.2
setuptools==72.1.0
sniffio==1.3.1
sqlparse==0.5.0

12
static/bootstrap-5.3.3/.babelrc.js vendored Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
bugfixes: true,
modules: false
}
]
]
};

View File

@ -0,0 +1,11 @@
# https://github.com/browserslist/browserslist#readme
>= 0.5%
last 2 major versions
not dead
Chrome >= 60
Firefox >= 60
Firefox ESR
iOS >= 12
Safari >= 12
not Explorer <= 11

View File

@ -0,0 +1,66 @@
{
"files": [
{
"path": "./dist/css/bootstrap-grid.css",
"maxSize": "6.5 kB"
},
{
"path": "./dist/css/bootstrap-grid.min.css",
"maxSize": "6.0 kB"
},
{
"path": "./dist/css/bootstrap-reboot.css",
"maxSize": "3.5 kB"
},
{
"path": "./dist/css/bootstrap-reboot.min.css",
"maxSize": "3.25 kB"
},
{
"path": "./dist/css/bootstrap-utilities.css",
"maxSize": "11.75 kB"
},
{
"path": "./dist/css/bootstrap-utilities.min.css",
"maxSize": "10.75 kB"
},
{
"path": "./dist/css/bootstrap.css",
"maxSize": "32.5 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
"maxSize": "30.25 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
"maxSize": "43.0 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "23.25 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",
"maxSize": "28.0 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "18.25 kB"
},
{
"path": "./dist/js/bootstrap.js",
"maxSize": "28.75 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
"maxSize": "16.25 kB"
}
],
"ci": {
"trackBranches": [
"main",
"v4-dev"
]
}
}

View File

@ -0,0 +1,133 @@
{
"version": "0.2",
"words": [
"affordance",
"allowfullscreen",
"Analyser",
"autohide",
"autohiding",
"autoplay",
"autoplays",
"autoplaying",
"blazingly",
"Blockquotes",
"Bootstrappers",
"borderless",
"Brotli",
"browserslist",
"browserslistrc",
"btncheck",
"btnradio",
"callout",
"callouts",
"camelCase",
"clearfix",
"Codesniffer",
"combinator",
"Contentful",
"Cpath",
"Crossfade",
"crossfading",
"cssgrid",
"Csvg",
"Datalists",
"Deque",
"discoverability",
"docsearch",
"docsref",
"dropend",
"dropleft",
"dropright",
"dropstart",
"dropup",
"dgst",
"errorf",
"favicon",
"favicons",
"fieldsets",
"flexbox",
"fullscreen",
"getbootstrap",
"Grayscale",
"Hoverable",
"hreflang",
"hstack",
"importmap",
"jsdelivr",
"Jumpstart",
"keyframes",
"libera",
"libman",
"Libsass",
"lightboxes",
"Lowercased",
"markdownify",
"mediaqueries",
"minifiers",
"misfunction",
"mkdir",
"monospace",
"mouseleave",
"navbars",
"navs",
"Neue",
"noindex",
"Noto",
"offcanvas",
"offcanvases",
"Packagist",
"popperjs",
"prebuild",
"prefersreducedmotion",
"prepended",
"printf",
"rects",
"relref",
"rgba",
"roboto",
"RTLCSS",
"ruleset",
"screenreaders",
"scrollbars",
"scrollspy",
"Segoe",
"semibold",
"socio",
"srcset",
"stackblitz",
"stickied",
"Stylelint",
"subnav",
"tabbable",
"textareas",
"toggleable",
"topbar",
"touchend",
"twbs",
"unitless",
"unstylable",
"unstyled",
"Uppercased",
"urlize",
"urlquery",
"vbtn",
"viewports",
"Vite",
"vstack",
"walkthroughs",
"WCAG",
"zindex"
],
"language": "en-US",
"files": [
"**/*.md"
],
"ignorePaths": [
".cspell.json",
"dist/",
"*.min.*",
"**/*rtl*",
"**/tests/**"
],
"useGitignore": true
}

View File

@ -0,0 +1,11 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -0,0 +1,7 @@
**/*.min.js
**/dist/
**/vendor/
/_site/
/js/coverage/
/site/static/sw.js
/site/layouts/partials/

View File

@ -0,0 +1,222 @@
{
"root": true,
"extends": [
"plugin:import/errors",
"plugin:import/warnings",
"plugin:unicorn/recommended",
"xo",
"xo/browser"
],
"rules": {
"arrow-body-style": "off",
"capitalized-comments": "off",
"comma-dangle": [
"error",
"never"
],
"import/extensions": [
"error",
"ignorePackages",
{
"js": "always"
}
],
"import/first": "error",
"import/newline-after-import": "error",
"import/no-absolute-path": "error",
"import/no-amd": "error",
"import/no-cycle": [
"error",
{
"ignoreExternal": true
}
],
"import/no-duplicates": "error",
"import/no-extraneous-dependencies": "error",
"import/no-mutable-exports": "error",
"import/no-named-as-default": "error",
"import/no-named-as-default-member": "error",
"import/no-named-default": "error",
"import/no-self-import": "error",
"import/no-unassigned-import": [
"error"
],
"import/no-useless-path-segments": "error",
"import/order": "error",
"indent": [
"error",
2,
{
"MemberExpression": "off",
"SwitchCase": 1
}
],
"logical-assignment-operators": "off",
"max-params": [
"warn",
5
],
"multiline-ternary": [
"error",
"always-multiline"
],
"new-cap": [
"error",
{
"properties": false
}
],
"no-console": "error",
"no-negated-condition": "off",
"object-curly-spacing": [
"error",
"always"
],
"operator-linebreak": [
"error",
"after"
],
"prefer-object-has-own": "off",
"prefer-template": "error",
"semi": [
"error",
"never"
],
"strict": "error",
"unicorn/explicit-length-check": "off",
"unicorn/filename-case": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-method-this-argument": "off",
"unicorn/no-null": "off",
"unicorn/no-typeof-undefined": "off",
"unicorn/no-unused-properties": "error",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-array-flat": "off",
"unicorn/prefer-at": "off",
"unicorn/prefer-dom-node-dataset": "off",
"unicorn/prefer-module": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-spread": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prevent-abbreviations": "off"
},
"overrides": [
{
"files": [
"build/**"
],
"env": {
"browser": false,
"node": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-console": "off",
"unicorn/prefer-top-level-await": "off"
}
},
{
"files": [
"js/**"
],
"parserOptions": {
"sourceType": "module"
}
},
{
"files": [
"js/tests/*.js",
"js/tests/integration/rollup*.js"
],
"env": {
"node": true
},
"parserOptions": {
"sourceType": "script"
}
},
{
"files": [
"js/tests/unit/**"
],
"env": {
"jasmine": true
},
"rules": {
"no-console": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-useless-undefined": "off",
"unicorn/prefer-add-event-listener": "off"
}
},
{
"files": [
"js/tests/visual/**"
],
"plugins": [
"html"
],
"settings": {
"html/html-extensions": [
".html"
]
},
"rules": {
"no-console": "off",
"no-new": "off",
"unicorn/no-array-for-each": "off"
}
},
{
"files": [
"scss/tests/**"
],
"env": {
"node": true
},
"parserOptions": {
"sourceType": "script"
}
},
{
"files": [
"site/**"
],
"env": {
"browser": true,
"node": false
},
"parserOptions": {
"sourceType": "script",
"ecmaVersion": 2019
},
"rules": {
"no-new": "off",
"unicorn/no-array-for-each": "off"
}
},
{
"files": [
"**/*.md"
],
"plugins": [
"markdown"
],
"processor": "markdown/markdown"
},
{
"files": [
"**/*.md/*.js"
],
"extends": "plugin:markdown/recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"unicorn/prefer-node-protocol": "off"
}
}
]
}

8
static/bootstrap-5.3.3/.gitattributes vendored Normal file
View File

@ -0,0 +1,8 @@
# Enforce Unix newlines
* text=auto eol=lf
# Don't diff or textually merge source maps
*.map binary
bootstrap.css linguist-vendored=false
bootstrap.js linguist-vendored=false

View File

@ -0,0 +1,3 @@
*.js @twbs/js-review
*.css @twbs/css-review
*.scss @twbs/css-review

View File

@ -0,0 +1,244 @@
# Contributing to Bootstrap
Looking to contribute something to Bootstrap? **Here's how you can help.**
Please take a moment to review this document in order to make the contribution
process easy and effective for everyone involved.
Following these guidelines helps to communicate that you respect the time of
the developers managing and developing this open source project. In return,
they should reciprocate that respect in addressing your issue or assessing
patches and features.
## Using the issue tracker
The [issue tracker](https://github.com/twbs/bootstrap/issues) is
the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests)
and [submitting pull requests](#pull-requests), but please respect the following
restrictions:
- Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help.
- Please **do not** derail or troll issues. Keep the discussion on topic and
respect the opinions of others.
- Please **do not** post comments consisting solely of "+1" or ":thumbsup:".
Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/)
instead. We reserve the right to delete comments which violate this rule.
- Please **do not** open issues regarding the official themes offered on <https://themes.getbootstrap.com/>.
Instead, please email any questions or feedback regarding those themes to `themes AT getbootstrap DOT com`.
## Issues assignment
The core team will be looking at the open issues, analyze them, and provide guidance on how to proceed. **Issues won't be assigned to anyone outside the core team.** However, contributors are welcome to participate in the discussion and provide their input on how to best solve the issue, and even submit a PR if they want to. Please wait that the issue is ready to be worked on before submitting a PR, we don't want to waste your time.
Please keep in mind that the core team is small, has limited resources and that we are not always able to respond immediately. We will try to provide feedback as soon as possible, but please be patient. If you don't get a response immediately, it doesn't mean that we are ignoring you or that we don't care about your issue or PR. We will get back to you as soon as we can.
## Issues and labels
Our bug tracker utilizes several labels to help organize and identify issues. Here's what they represent and how we use them:
- `browser bug` - Issues that are reported to us, but actually are the result of a browser-specific bug. These are diagnosed with reduced test cases and result in an issue opened on that browser's own bug tracker.
- `confirmed` - Issues that have been confirmed with a reduced test case and identify a bug in Bootstrap.
- `css` - Issues stemming from our compiled CSS or source Sass files.
- `docs` - Issues for improving or updating our documentation.
- `examples` - Issues involving the example templates included in our docs.
- `feature` - Issues asking for a new feature to be added, or an existing one to be extended or modified. New features require a minor version bump (e.g., `v3.0.0` to `v3.1.0`).
- `build` - Issues with our build system, which is used to run all our tests, concatenate and compile source files, and more.
- `help wanted` - Issues we need or would love help from the community to resolve.
- `js` - Issues stemming from our compiled or source JavaScript files.
- `meta` - Issues with the project itself or our GitHub repository.
For a complete look at our labels, see the [project labels page](https://github.com/twbs/bootstrap/labels).
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository.
Good bug reports are extremely helpful, so thanks!
Guidelines for bug reports:
0. **[Validate your HTML](https://html5.validator.nu/)** to ensure your
problem isn't caused by a simple error in your own code.
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `main` (or `v4-dev` branch if the issue is about v4) in the repository.
3. **Isolate the problem** &mdash; ideally create a [reduced test
case](https://css-tricks.com/reduced-test-cases/) and a live example.
These [v4 CodePen](https://codepen.io/team/bootstrap/pen/yLabNQL) and [v5 CodePen](https://codepen.io/team/bootstrap/pen/qBamdLj) are helpful templates.
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report. What is
your environment? What steps will reproduce the issue? What browser(s) and OS
experience the problem? Do other browsers show the bug differently? What
would you expect to be the outcome? All these details will help people to fix
any potential bugs.
Example:
> Short and descriptive example bug report title
>
> A summary of the issue and the browser/OS environment in which it occurs. If
> suitable, include the steps required to reproduce the bug.
>
> 1. This is the first step
> 2. This is the second step
> 3. Further steps, etc.
>
> `<url>` - a link to the reduced test case
>
> Any other information you want to share that is relevant to the issue being
> reported. This might include the lines of code that you have identified as
> causing the bug, and potential solutions (and your opinions on their
> merits).
### Reporting upstream browser bugs
Sometimes bugs reported to us are actually caused by bugs in the browser(s) themselves, not bugs in Bootstrap per se.
| Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes |
| ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- |
| Mozilla | Firefox | Gecko | <https://bugzilla.mozilla.org/enter_bug.cgi> | "Core" is normally the right product option to choose. |
| Apple | Safari | WebKit | <https://bugs.webkit.org/enter_bug.cgi?product=WebKit> | In Apple's bug reporter, choose "Safari" as the product. |
| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | <https://bugs.chromium.org/p/chromium/issues/list> | Click the "New issue" button. |
| Microsoft | Edge | Blink | <https://developer.microsoft.com/en-us/microsoft-edge/> | Go to "Help > Send Feedback" from the browser |
## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to _you_ to make a strong
case to convince the project's developers of the merits of this feature. Please
provide as much detail and context as possible.
## Pull requests
Good pull requests—patches, improvements, new features—are a fantastic
help. They should remain focused in scope and avoid containing unrelated
commits.
**Please ask first** before embarking on any **significant** pull request (e.g.
implementing features, refactoring code, porting to a different language),
otherwise you risk spending a lot of time working on something that the
project's developers might not want to merge into the project. For trivial
things, or things that don't require a lot of your time, you can go ahead and
make a PR.
Please adhere to the [coding guidelines](#code-guidelines) used throughout the
project (indentation, accurate comments, etc.) and any other requirements
(such as test coverage).
**Do not edit `bootstrap.css` or `bootstrap.js`, and do not commit
any dist files (`dist/` or `js/dist`).** Those files are automatically generated by our build tools. You should
edit the source files in [`/bootstrap/scss/`](https://github.com/twbs/bootstrap/tree/main/scss)
and/or [`/bootstrap/js/src/`](https://github.com/twbs/bootstrap/tree/main/js/src) instead.
Similarly, when contributing to Bootstrap's documentation, you should edit the
documentation source files in
[the `/bootstrap/site/content/docs/` directory of the `main` branch](https://github.com/twbs/bootstrap/tree/main/site/content/docs).
**Do not edit the `gh-pages` branch.** That branch is generated from the
documentation source files and is managed separately by the Bootstrap Core Team.
Adhering to the following process is the best way to get your work
included in the project:
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork,
and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/bootstrap.git
# Navigate to the newly cloned directory
cd bootstrap
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/twbs/bootstrap.git
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout main
git pull upstream main
```
3. Create a new topic branch (off the main project development branch) to
contain your feature, change, or fix:
```bash
git checkout -b <topic-branch-name>
```
4. Commit your changes in logical chunks. Please adhere to these [git commit
message guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://help.github.com/articles/about-git-rebase/)
feature to tidy up your commits before making them public.
5. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream main
```
6. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/)
with a clear title and description against the `main` branch.
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to
license your work under the terms of the [MIT License](../LICENSE) (if it
includes code changes) and under the terms of the
[Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
(if it includes documentation changes).
## Code guidelines
### HTML
[Adhere to the Code Guide.](https://codeguide.co/#html)
- Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags).
- Use CDNs and HTTPS for third-party JS when possible. We don't use protocol-relative URLs in this case because they break when viewing the page locally via `file://`.
- Use [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes in documentation examples to promote accessibility.
### CSS
[Adhere to the Code Guide.](https://codeguide.co/#css)
- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG20/#visual-audio-contrast).
- Except in rare cases, don't remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details.
### JS
- No semicolons (in client-side JS)
- 2 spaces (no tabs)
- strict mode
- "Attractive"
### Checking coding style
Run `npm run test` before committing to ensure your changes follow our coding standards.
## License
By contributing your code, you agree to license your contribution under the [MIT License](../LICENSE).
By contributing to the documentation, you agree to license your contribution under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).
Prior to v3.1.0, Bootstrap's code was released under the Apache License v2.0.

View File

@ -0,0 +1,62 @@
name: Report a bug
description: Tell us about a bug or issue you may have identified in Bootstrap.
title: "Provide a general summary of the issue"
labels: [bug]
assignees: "-"
body:
- type: checkboxes
attributes:
label: Prerequisites
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed issues
required: true
- label: I have [validated](https://html5.validator.nu/) any HTML to avoid common problems
required: true
- label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
required: true
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: Provide a summary of the issue and what you expected to happen, including specific steps to reproduce.
validations:
required: true
- type: textarea
id: reduced-test-case
attributes:
label: Reduced test cases
description: Include links [reduced test case](https://css-tricks.com/reduced-test-cases/) links or suggested fixes using CodePen ([v4 template](https://codepen.io/team/bootstrap/pen/yLabNQL) or [v5 template](https://codepen.io/team/bootstrap/pen/qBamdLj)).
validations:
required: true
- type: dropdown
id: os
attributes:
label: What operating system(s) are you seeing the problem on?
multiple: true
options:
- Windows
- macOS
- Android
- iOS
- Linux
validations:
required: true
- type: dropdown
id: browser
attributes:
label: What browser(s) are you seeing the problem on?
multiple: true
options:
- Chrome
- Safari
- Firefox
- Microsoft Edge
- Opera
- type: input
id: version
attributes:
label: What version of Bootstrap are you using?
placeholder: "e.g., v5.1.0 or v4.5.2"
validations:
required: true

View File

@ -0,0 +1,4 @@
contact_links:
- name: Ask the community
url: https://github.com/twbs/bootstrap/discussions/new
about: Ask and discuss questions with other Bootstrap community members.

View File

@ -0,0 +1,29 @@
name: Feature request
description: Suggest new or updated features to include in Bootstrap.
title: "Suggest a new feature"
labels: [feature]
assignees: []
body:
- type: checkboxes
attributes:
label: Prerequisites
description: Take a couple minutes to help our maintainers work faster.
options:
- label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed feature requests
required: true
- label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
required: true
- type: textarea
id: proposal
attributes:
label: Proposal
description: Provide detailed information for what we should add, including relevant links to prior art, screenshots, or live demos whenever possible.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation and context
description: Tell us why this change is needed or helpful, and what problems it may help solve.
validations:
required: true

View File

@ -0,0 +1,38 @@
### Description
<!-- Describe your changes in detail -->
### Motivation & Context
<!-- Why is this change required? What problem does it solve? -->
### Type of changes
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Refactoring (non-breaking change)
- [ ] Breaking change (fix or feature that would change existing functionality)
### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md)
- [ ] My code follows the code style of the project _(using `npm run lint`)_
- [ ] My change introduces changes to the documentation
- [ ] I have updated the documentation accordingly
- [ ] I have added tests to cover my changes
- [ ] All new and existing tests passed
#### Live previews
<!-- Please add direct links where your modifications can be seen in the documentation -->
- <https://deploy-preview-{your_pr_number}--twbs-bootstrap.netlify.app/>
### Related issues
<!-- Please link any related issues here. -->

View File

@ -0,0 +1,11 @@
### Bug reports
See the [contributing guidelines](CONTRIBUTING.md) for sharing bug reports.
### How-to
For general troubleshooting or help getting started:
- Ask and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions).
- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel.
- Ask and explore Stack Overflow with the [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag.

View File

@ -0,0 +1,3 @@
name: "CodeQL config"
paths-ignore:
- dist

View File

@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
day: tuesday
time: "12:00"
timezone: Europe/Athens
- package-ecosystem: npm
directory: "/"
reviewers:
- XhmikosR
labels:
- dependencies
- v5
schedule:
interval: weekly
day: tuesday
time: "12:00"
timezone: Europe/Athens
versioning-strategy: increase
rebase-strategy: disabled

View File

@ -0,0 +1,60 @@
name-template: 'v$NEXT_MAJOR_VERSION'
tag-template: 'v$NEXT_MAJOR_VERSION'
prerelease: true
exclude-labels:
- 'skip-changelog'
categories:
- title: '❗ Breaking Changes'
labels:
- 'breaking-change'
- title: '🚀 Highlights'
labels:
- 'release-highlight'
- title: '🚀 Features'
labels:
- 'new-feature'
- 'feature'
- 'enhancement'
- title: '🐛 Bug fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '⚡ Performance improvements'
labels:
- 'performance'
- title: '🎨 CSS'
labels:
- 'css'
- title: '☕️ JavaScript'
labels:
- 'js'
- title: '📖 Docs'
labels:
- 'docs'
- title: '🛠 Examples'
labels:
- 'examples'
- title: '🌎 Accessibility'
labels:
- 'accessibility'
- title: '🔧 Utility API'
labels:
- 'utility API'
- 'utilities'
- title: '🏭 Tests'
labels:
- 'tests'
- title: '🧰 Misc'
labels:
- 'build'
- 'meta'
- 'chore'
- 'CI'
- title: '📦 Dependencies'
labels:
- 'dependencies'
change-template: '- #$NUMBER: $TITLE'
template: |
## Changes
$CHANGES

View File

@ -0,0 +1,46 @@
name: BrowserStack
on:
push:
branches:
- "**"
- "!dependabot/**"
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
browserstack:
runs-on: ubuntu-latest
if: github.repository == 'twbs/bootstrap'
timeout-minutes: 30
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Run dist
run: npm run dist
- name: Run BrowserStack tests
run: npm run js-test-cloud
env:
BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}"
BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}"
GITHUB_SHA: "${{ github.sha }}"

View File

@ -0,0 +1,43 @@
name: Bundlewatch
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
bundlewatch:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Run dist
run: npm run dist
- name: Run bundlewatch
run: npm run bundlewatch
env:
BUNDLEWATCH_GITHUB_TOKEN: "${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}"
CI_BRANCH_BASE: main

View File

@ -0,0 +1,32 @@
name: Compress Images
on:
pull_request:
paths:
- '**.jpg'
- '**.jpeg'
- '**.png'
- '**.webp'
permissions:
contents: read
jobs:
build:
# Only run on Pull Requests within the same repository, and not from forks.
if: github.event.pull_request.head.repo.full_name == github.repository
name: calibreapp/image-actions
runs-on: ubuntu-latest
permissions:
# allow calibreapp/image-actions to update PRs
pull-requests: write
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Compress Images
uses: calibreapp/image-actions@1.1.0
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,44 @@
name: "CodeQL"
on:
push:
branches:
- main
- v4-dev
- "!dependabot/**"
pull_request:
branches:
- main
- v4-dev
- "!dependabot/**"
schedule:
- cron: "0 2 * * 4"
workflow_dispatch:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
config-file: ./.github/codeql/codeql-config.yml
languages: "javascript"
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:javascript"

View File

@ -0,0 +1,36 @@
name: cspell
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
permissions:
contents: read
jobs:
cspell:
permissions:
# allow streetsidesoftware/cspell-action to fetch files for commits and PRs
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Run cspell
uses: streetsidesoftware/cspell-action@v5
with:
config: ".cspell.json"
files: "**/*.md"
inline: error
incremental_files_only: false

View File

@ -0,0 +1,40 @@
name: CSS
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
css:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Build CSS
run: npm run css
- name: Run CSS tests
run: npm run css-test

View File

@ -0,0 +1,50 @@
name: Docs
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
docs:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
cache: npm
- run: java -version
- name: Install npm dependencies
run: npm ci
- 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)"

View File

@ -0,0 +1,26 @@
name: Close Issue Awaiting Reply
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
issue-close-require:
permissions:
# allow actions-cool/issues-helper to update issues and PRs
issues: write
pull-requests: write
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.

View File

@ -0,0 +1,26 @@
name: Issue Labeled
on:
issues:
types: [labeled]
permissions:
contents: read
jobs:
issue-labeled:
permissions:
# allow actions-cool/issues-helper to update issues and PRs
issues: write
pull-requests: write
if: github.repository == 'twbs/bootstrap'
runs-on: ubuntu-latest
steps:
- name: awaiting reply
if: github.event.label.name == 'needs-example'
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 [StackBlitz](https://stackblitz.com/) and report back with your link, Bootstrap version, and specific browser and Operating System details.

View File

@ -0,0 +1,52 @@
name: JS Tests
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
run:
permissions:
# allow coverallsapp/github-action to create new checks issues and fetch code
checks: write
contents: read
name: JS Tests
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE }}
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Run dist
run: npm run js
- name: Run JS tests
run: npm run js-test
- name: Run Coveralls
uses: coverallsapp/github-action@v2
if: ${{ !github.event.repository.fork }}
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
path-to-lcov: "./js/coverage/lcov.info"

View File

@ -0,0 +1,37 @@
name: Lint
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
cache: npm
- name: Install npm dependencies
run: npm ci
- name: Lint
run: npm run lint

View File

@ -0,0 +1,49 @@
name: CSS (node-sass)
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: 2
NODE: 20
permissions:
contents: read
jobs:
css:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE }}"
- name: Build CSS with node-sass
run: |
npx --package node-sass@latest node-sass --version
npx --package node-sass@latest node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/ -o dist-sass/css/
ls -Al dist-sass/css
- name: Check built CSS files for Sass variables
shell: bash
run: |
SASS_VARS_FOUND=$(find "dist-sass/css/" -type f -name "*.css" -print0 | xargs -0 --no-run-if-empty grep -F "\$" || true)
if [[ -z "$SASS_VARS_FOUND" ]]; then
echo "All good, no Sass variables found!"
exit 0
else
echo "Found $(echo "$SASS_VARS_FOUND" | wc -l | bc) Sass variables:"
echo "$SASS_VARS_FOUND"
exit 1
fi

View File

@ -0,0 +1,23 @@
name: Release notes
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
update_release_draft:
permissions:
# allow release-drafter/release-drafter to create GitHub releases and add labels to PRs
contents: write
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'twbs/bootstrap'
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

43
static/bootstrap-5.3.3/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Ignore docs files
/_site/
# Hugo files
/resources/
/.hugo_build.lock
# Numerous always-ignore extensions
*.diff
*.err
*.log
*.orig
*.rej
*.swo
*.swp
*.vi
*.zip
*~
# OS or Editor folders
._*
.cache
.DS_Store
.idea
.project
.settings
.tmproj
*.esproj
*.sublime-project
*.sublime-workspace
nbproject
Thumbs.db
/.vscode/
# Local Netlify folder
.netlify
# Komodo
.komodotools
*.komodoproject
# Folders to ignore
/dist-sass/
/js/coverage/
/node_modules/

View File

@ -0,0 +1 @@
lockfile-version=2

View File

@ -0,0 +1,5 @@
**/*.min.css
**/dist/
**/vendor/
/_site/
/js/coverage/

View File

@ -0,0 +1,60 @@
{
"extends": [
"stylelint-config-twbs-bootstrap"
],
"reportInvalidScopeDisables": true,
"reportNeedlessDisables": true,
"overrides": [
{
"files": "**/*.scss",
"rules": {
"declaration-property-value-disallowed-list": {
"border": "none",
"outline": "none"
},
"function-disallowed-list": [
"calc",
"lighten",
"darken"
],
"property-disallowed-list": [
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"transition"
],
"scss/dollar-variable-default": [
true,
{
"ignore": "local"
}
],
"scss/selector-no-union-class-name": true
}
},
{
"files": "scss/**/*.{test,spec}.scss",
"rules": {
"scss/dollar-variable-default": null,
"declaration-no-important": null
}
},
{
"files": "site/**/*.scss",
"rules": {
"scss/dollar-variable-default": null
}
},
{
"files": "site/**/examples/**/*.css",
"rules": {
"comment-empty-line-before": null,
"property-no-vendor-prefix": null,
"selector-no-qualifying-type": null,
"value-no-vendor-prefix": null
}
}
]
}

19
static/bootstrap-5.3.3/js/index.esm.js vendored Normal file
View File

@ -0,0 +1,19 @@
/**
* --------------------------------------------------------------------------
* Bootstrap index.esm.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
export { default as Alert } from './src/alert.js'
export { default as Button } from './src/button.js'
export { default as Carousel } from './src/carousel.js'
export { default as Collapse } from './src/collapse.js'
export { default as Dropdown } from './src/dropdown.js'
export { default as Modal } from './src/modal.js'
export { default as Offcanvas } from './src/offcanvas.js'
export { default as Popover } from './src/popover.js'
export { default as ScrollSpy } from './src/scrollspy.js'
export { default as Tab } from './src/tab.js'
export { default as Toast } from './src/toast.js'
export { default as Tooltip } from './src/tooltip.js'

34
static/bootstrap-5.3.3/js/index.umd.js vendored Normal file
View File

@ -0,0 +1,34 @@
/**
* --------------------------------------------------------------------------
* Bootstrap index.umd.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Alert from './src/alert.js'
import Button from './src/button.js'
import Carousel from './src/carousel.js'
import Collapse from './src/collapse.js'
import Dropdown from './src/dropdown.js'
import Modal from './src/modal.js'
import Offcanvas from './src/offcanvas.js'
import Popover from './src/popover.js'
import ScrollSpy from './src/scrollspy.js'
import Tab from './src/tab.js'
import Toast from './src/toast.js'
import Tooltip from './src/tooltip.js'
export default {
Alert,
Button,
Carousel,
Collapse,
Dropdown,
Modal,
Offcanvas,
Popover,
ScrollSpy,
Tab,
Toast,
Tooltip
}

87
static/bootstrap-5.3.3/js/src/alert.js vendored Normal file
View File

@ -0,0 +1,87 @@
/**
* --------------------------------------------------------------------------
* Bootstrap alert.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import { enableDismissTrigger } from './util/component-functions.js'
import { defineJQueryPlugin } from './util/index.js'
/**
* Constants
*/
const NAME = 'alert'
const DATA_KEY = 'bs.alert'
const EVENT_KEY = `.${DATA_KEY}`
const EVENT_CLOSE = `close${EVENT_KEY}`
const EVENT_CLOSED = `closed${EVENT_KEY}`
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
/**
* Class definition
*/
class Alert extends BaseComponent {
// Getters
static get NAME() {
return NAME
}
// Public
close() {
const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)
if (closeEvent.defaultPrevented) {
return
}
this._element.classList.remove(CLASS_NAME_SHOW)
const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)
this._queueCallback(() => this._destroyElement(), this._element, isAnimated)
}
// Private
_destroyElement() {
this._element.remove()
EventHandler.trigger(this._element, EVENT_CLOSED)
this.dispose()
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Alert.getOrCreateInstance(this)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
}
/**
* Data API implementation
*/
enableDismissTrigger(Alert, 'close')
/**
* jQuery
*/
defineJQueryPlugin(Alert)
export default Alert

View File

@ -0,0 +1,85 @@
/**
* --------------------------------------------------------------------------
* Bootstrap base-component.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Data from './dom/data.js'
import EventHandler from './dom/event-handler.js'
import Config from './util/config.js'
import { executeAfterTransition, getElement } from './util/index.js'
/**
* Constants
*/
const VERSION = '5.3.3'
/**
* 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)
for (const propertyName of Object.getOwnPropertyNames(this)) {
this[propertyName] = null
}
}
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated)
}
_getConfig(config) {
config = this._mergeConfigObj(config, this._element)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
// Static
static getInstance(element) {
return Data.get(getElement(element), this.DATA_KEY)
}
static getOrCreateInstance(element, config = {}) {
return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)
}
static get VERSION() {
return VERSION
}
static get DATA_KEY() {
return `bs.${this.NAME}`
}
static get EVENT_KEY() {
return `.${this.DATA_KEY}`
}
static eventName(name) {
return `${name}${this.EVENT_KEY}`
}
}
export default BaseComponent

72
static/bootstrap-5.3.3/js/src/button.js vendored Normal file
View File

@ -0,0 +1,72 @@
/**
* --------------------------------------------------------------------------
* Bootstrap button.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import { defineJQueryPlugin } from './util/index.js'
/**
* Constants
*/
const NAME = 'button'
const DATA_KEY = 'bs.button'
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 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)
if (config === 'toggle') {
data[config]()
}
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
event.preventDefault()
const button = event.target.closest(SELECTOR_DATA_TOGGLE)
const data = Button.getOrCreateInstance(button)
data.toggle()
})
/**
* jQuery
*/
defineJQueryPlugin(Button)
export default Button

View File

@ -0,0 +1,474 @@
/**
* --------------------------------------------------------------------------
* Bootstrap carousel.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin,
getNextActiveElement,
isRTL,
isVisible,
reflow,
triggerTransitionEnd
} from './util/index.js'
import Swipe from './util/swipe.js'
/**
* Constants
*/
const NAME = 'carousel'
const DATA_KEY = 'bs.carousel'
const EVENT_KEY = `.${DATA_KEY}`
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 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_ITEM = '.carousel-item'
const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM
const SELECTOR_ITEM_IMG = '.carousel-item img'
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,
keyboard: true,
pause: 'hover',
ride: false,
touch: true,
wrap: true
}
const DefaultType = {
interval: '(number|boolean)', // TODO:v6 remove boolean support
keyboard: 'boolean',
pause: '(string|boolean)',
ride: '(boolean|string)',
touch: 'boolean',
wrap: 'boolean'
}
/**
* Class definition
*/
class Carousel extends BaseComponent {
constructor(element, config) {
super(element, config)
this._interval = null
this._activeElement = null
this._isSliding = false
this.touchTimeout = null
this._swipeHelper = null
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
this._addEventListeners()
if (this._config.ride === CLASS_NAME_CAROUSEL) {
this.cycle()
}
}
// 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)) {
this.next()
}
}
prev() {
this._slide(ORDER_PREV)
}
pause() {
if (this._isSliding) {
triggerTransitionEnd(this._element)
}
this._clearInterval()
}
cycle() {
this._clearInterval()
this._updateInterval()
this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)
}
_maybeEnableCycle() {
if (!this._config.ride) {
return
}
if (this._isSliding) {
EventHandler.one(this._element, EVENT_SLID, () => this.cycle())
return
}
this.cycle()
}
to(index) {
const items = this._getItems()
if (index > items.length - 1 || index < 0) {
return
}
if (this._isSliding) {
EventHandler.one(this._element, EVENT_SLID, () => this.to(index))
return
}
const activeIndex = this._getItemIndex(this._getActive())
if (activeIndex === index) {
return
}
const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV
this._slide(order, items[index])
}
dispose() {
if (this._swipeHelper) {
this._swipeHelper.dispose()
}
super.dispose()
}
// Private
_configAfterMerge(config) {
config.defaultInterval = config.interval
return config
}
_addEventListeners() {
if (this._config.keyboard) {
EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
}
if (this._config.pause === 'hover') {
EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())
EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())
}
if (this._config.touch && Swipe.isSupported()) {
this._addTouchEventListeners()
}
}
_addTouchEventListeners() {
for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())
}
const endCallBack = () => {
if (this._config.pause !== 'hover') {
return
}
// 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(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
}
const swipeConfig = {
leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),
endCallback: endCallBack
}
this._swipeHelper = new Swipe(this._element, swipeConfig)
}
_keydown(event) {
if (/input|textarea/i.test(event.target.tagName)) {
return
}
const direction = KEY_TO_DIRECTION[event.key]
if (direction) {
event.preventDefault()
this._slide(this._directionToOrder(direction))
}
}
_getItemIndex(element) {
return this._getItems().indexOf(element)
}
_setActiveIndicatorElement(index) {
if (!this._indicatorsElement) {
return
}
const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
activeIndicator.removeAttribute('aria-current')
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 || this._getActive()
if (!element) {
return
}
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)
this._config.interval = elementInterval || this._config.defaultInterval
}
_slide(order, element = null) {
if (this._isSliding) {
return
}
const activeElement = this._getActive()
const isNext = order === ORDER_NEXT
const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)
if (nextElement === activeElement) {
return
}
const nextElementIndex = this._getItemIndex(nextElement)
const triggerEvent = eventName => {
return EventHandler.trigger(this._element, eventName, {
relatedTarget: nextElement,
direction: this._orderToDirection(order),
from: this._getItemIndex(activeElement),
to: nextElementIndex
})
}
const slideEvent = triggerEvent(EVENT_SLIDE)
if (slideEvent.defaultPrevented) {
return
}
if (!activeElement || !nextElement) {
// Some weirdness is happening, so we bail
// TODO: change tests that use empty divs to avoid this check
return
}
const isCycling = Boolean(this._interval)
this.pause()
this._isSliding = true
this._setActiveIndicatorElement(nextElementIndex)
this._activeElement = nextElement
const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV
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
triggerEvent(EVENT_SLID)
}
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)
}
_getItems() {
return SelectorEngine.find(SELECTOR_ITEM, this._element)
}
_clearInterval() {
if (this._interval) {
clearInterval(this._interval)
this._interval = null
}
}
_directionToOrder(direction) {
if (isRTL()) {
return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
}
return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV
}
_orderToDirection(order) {
if (isRTL()) {
return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
}
return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Carousel.getOrCreateInstance(this, config)
if (typeof config === 'number') {
data.to(config)
return
}
if (typeof config === 'string') {
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {
const target = SelectorEngine.getElementFromSelector(this)
if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
return
}
event.preventDefault()
const carousel = Carousel.getOrCreateInstance(target)
const slideIndex = this.getAttribute('data-bs-slide-to')
if (slideIndex) {
carousel.to(slideIndex)
carousel._maybeEnableCycle()
return
}
if (Manipulator.getDataAttribute(this, 'slide') === 'next') {
carousel.next()
carousel._maybeEnableCycle()
return
}
carousel.prev()
carousel._maybeEnableCycle()
})
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (const carousel of carousels) {
Carousel.getOrCreateInstance(carousel)
}
})
/**
* jQuery
*/
defineJQueryPlugin(Carousel)
export default Carousel

View File

@ -0,0 +1,297 @@
/**
* --------------------------------------------------------------------------
* Bootstrap collapse.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin,
getElement,
reflow
} from './util/index.js'
/**
* Constants
*/
const NAME = 'collapse'
const DATA_KEY = 'bs.collapse'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
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 CLASS_NAME_SHOW = 'show'
const CLASS_NAME_COLLAPSE = 'collapse'
const CLASS_NAME_COLLAPSING = 'collapsing'
const CLASS_NAME_COLLAPSED = 'collapsed'
const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`
const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'
const WIDTH = 'width'
const HEIGHT = 'height'
const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'
const Default = {
parent: null,
toggle: true
}
const DefaultType = {
parent: '(null|element)',
toggle: 'boolean'
}
/**
* Class definition
*/
class Collapse extends BaseComponent {
constructor(element, config) {
super(element, config)
this._isTransitioning = false
this._triggerArray = []
const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (const elem of toggleList) {
const selector = SelectorEngine.getSelectorFromElement(elem)
const filterElement = SelectorEngine.find(selector)
.filter(foundElement => foundElement === this._element)
if (selector !== null && filterElement.length) {
this._triggerArray.push(elem)
}
}
this._initializeChildren()
if (!this._config.parent) {
this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())
}
if (this._config.toggle) {
this.toggle()
}
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
if (this._isShown()) {
this.hide()
} else {
this.show()
}
}
show() {
if (this._isTransitioning || this._isShown()) {
return
}
let activeChildren = []
// find active children
if (this._config.parent) {
activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)
.filter(element => element !== this._element)
.map(element => Collapse.getOrCreateInstance(element, { toggle: false }))
}
if (activeChildren.length && activeChildren[0]._isTransitioning) {
return
}
const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)
if (startEvent.defaultPrevented) {
return
}
for (const activeInstance of activeChildren) {
activeInstance.hide()
}
const dimension = this._getDimension()
this._element.classList.remove(CLASS_NAME_COLLAPSE)
this._element.classList.add(CLASS_NAME_COLLAPSING)
this._element.style[dimension] = 0
this._addAriaAndCollapsedClass(this._triggerArray, true)
this._isTransitioning = true
const complete = () => {
this._isTransitioning = false
this._element.classList.remove(CLASS_NAME_COLLAPSING)
this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
this._element.style[dimension] = ''
EventHandler.trigger(this._element, EVENT_SHOWN)
}
const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)
const scrollSize = `scroll${capitalizedDimension}`
this._queueCallback(complete, this._element, true)
this._element.style[dimension] = `${this._element[scrollSize]}px`
}
hide() {
if (this._isTransitioning || !this._isShown()) {
return
}
const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)
if (startEvent.defaultPrevented) {
return
}
const dimension = this._getDimension()
this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`
reflow(this._element)
this._element.classList.add(CLASS_NAME_COLLAPSING)
this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
for (const trigger of this._triggerArray) {
const element = SelectorEngine.getElementFromSelector(trigger)
if (element && !this._isShown(element)) {
this._addAriaAndCollapsedClass([trigger], false)
}
}
this._isTransitioning = true
const complete = () => {
this._isTransitioning = false
this._element.classList.remove(CLASS_NAME_COLLAPSING)
this._element.classList.add(CLASS_NAME_COLLAPSE)
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
this._element.style[dimension] = ''
this._queueCallback(complete, this._element, true)
}
_isShown(element = this._element) {
return element.classList.contains(CLASS_NAME_SHOW)
}
// Private
_configAfterMerge(config) {
config.toggle = Boolean(config.toggle) // Coerce string values
config.parent = getElement(config.parent)
return config
}
_getDimension() {
return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT
}
_initializeChildren() {
if (!this._config.parent) {
return
}
const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)
for (const element of children) {
const selected = SelectorEngine.getElementFromSelector(element)
if (selected) {
this._addAriaAndCollapsedClass([element], this._isShown(selected))
}
}
}
_getFirstLevelChildren(selector) {
const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)
// remove children if greater depth
return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element))
}
_addAriaAndCollapsedClass(triggerArray, isOpen) {
if (!triggerArray.length) {
return
}
for (const element of triggerArray) {
element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)
element.setAttribute('aria-expanded', isOpen)
}
}
// Static
static jQueryInterface(config) {
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') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
// preventDefault only for <a> elements (which change the URL) not inside the collapsible element
if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {
event.preventDefault()
}
for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {
Collapse.getOrCreateInstance(element, { toggle: false }).toggle()
}
})
/**
* jQuery
*/
defineJQueryPlugin(Collapse)
export default Collapse

View File

@ -0,0 +1,55 @@
/**
* --------------------------------------------------------------------------
* Bootstrap dom/data.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
/**
* Constants
*/
const elementMap = new Map()
export default {
set(element, key, instance) {
if (!elementMap.has(element)) {
elementMap.set(element, new Map())
}
const instanceMap = elementMap.get(element)
// make it clear we only want one instance per element
// can be removed later when multiple key/instances are fine to be used
if (!instanceMap.has(key) && instanceMap.size !== 0) {
// eslint-disable-next-line no-console
console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)
return
}
instanceMap.set(key, instance)
},
get(element, key) {
if (elementMap.has(element)) {
return elementMap.get(element).get(key) || null
}
return null
},
remove(element, key) {
if (!elementMap.has(element)) {
return
}
const instanceMap = elementMap.get(element)
instanceMap.delete(key)
// free up element references if there are no instances left for an element
if (instanceMap.size === 0) {
elementMap.delete(element)
}
}
}

View File

@ -0,0 +1,317 @@
/**
* --------------------------------------------------------------------------
* Bootstrap dom/event-handler.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { getjQuery } from '../util/index.js'
/**
* Constants
*/
const namespaceRegex = /[^.]*(?=\..*)\.|.*/
const stripNameRegex = /\..*/
const stripUidRegex = /::\d+$/
const eventRegistry = {} // Events storage
let uidEvent = 1
const customEvents = {
mouseenter: 'mouseover',
mouseleave: 'mouseout'
}
const nativeEvents = new Set([
'click',
'dblclick',
'mouseup',
'mousedown',
'contextmenu',
'mousewheel',
'DOMMouseScroll',
'mouseover',
'mouseout',
'mousemove',
'selectstart',
'selectend',
'keydown',
'keypress',
'keyup',
'orientationchange',
'touchstart',
'touchmove',
'touchend',
'touchcancel',
'pointerdown',
'pointermove',
'pointerup',
'pointerleave',
'pointercancel',
'gesturestart',
'gesturechange',
'gestureend',
'focus',
'blur',
'change',
'reset',
'select',
'submit',
'focusin',
'focusout',
'load',
'unload',
'beforeunload',
'resize',
'move',
'DOMContentLoaded',
'readystatechange',
'error',
'abort',
'scroll'
])
/**
* Private methods
*/
function makeEventUid(element, uid) {
return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
}
function getElementEvents(element) {
const uid = makeEventUid(element)
element.uidEvent = uid
eventRegistry[uid] = eventRegistry[uid] || {}
return eventRegistry[uid]
}
function bootstrapHandler(element, fn) {
return function handler(event) {
hydrateObj(event, { delegateTarget: element })
if (handler.oneOff) {
EventHandler.off(element, event.type, fn)
}
return fn.apply(element, [event])
}
}
function bootstrapDelegationHandler(element, selector, fn) {
return function handler(event) {
const domElements = element.querySelectorAll(selector)
for (let { target } = event; target && target !== this; target = target.parentNode) {
for (const domElement of domElements) {
if (domElement !== target) {
continue
}
hydrateObj(event, { delegateTarget: target })
if (handler.oneOff) {
EventHandler.off(element, event.type, selector, fn)
}
return fn.apply(target, [event])
}
}
}
}
function findHandler(events, callable, delegationSelector = null) {
return Object.values(events)
.find(event => event.callable === callable && event.delegationSelector === delegationSelector)
}
function normalizeParameters(originalTypeEvent, handler, delegationFunction) {
const isDelegated = typeof handler === 'string'
// TODO: tooltip passes `false` instead of selector, so we need to check
const callable = isDelegated ? delegationFunction : (handler || delegationFunction)
let typeEvent = getTypeEvent(originalTypeEvent)
if (!nativeEvents.has(typeEvent)) {
typeEvent = originalTypeEvent
}
return [isDelegated, callable, typeEvent]
}
function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {
if (typeof originalTypeEvent !== 'string' || !element) {
return
}
let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
// in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
// this prevents the handler from being dispatched the same way as mouseover or mouseout does
if (originalTypeEvent in customEvents) {
const wrapFunction = fn => {
return function (event) {
if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {
return fn.call(this, event)
}
}
}
callable = wrapFunction(callable)
}
const events = getElementEvents(element)
const handlers = events[typeEvent] || (events[typeEvent] = {})
const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)
if (previousFunction) {
previousFunction.oneOff = previousFunction.oneOff && oneOff
return
}
const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))
const fn = isDelegated ?
bootstrapDelegationHandler(element, handler, callable) :
bootstrapHandler(element, callable)
fn.delegationSelector = isDelegated ? handler : null
fn.callable = callable
fn.oneOff = oneOff
fn.uidEvent = uid
handlers[uid] = fn
element.addEventListener(typeEvent, fn, isDelegated)
}
function removeHandler(element, events, typeEvent, handler, delegationSelector) {
const fn = findHandler(events[typeEvent], handler, delegationSelector)
if (!fn) {
return
}
element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
delete events[typeEvent][fn.uidEvent]
}
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
const storeElementEvent = events[typeEvent] || {}
for (const [handlerKey, event] of Object.entries(storeElementEvent)) {
if (handlerKey.includes(namespace)) {
removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
}
}
}
function getTypeEvent(event) {
// allow to get the native events from namespaced events ('click.bs.button' --> 'click')
event = event.replace(stripNameRegex, '')
return customEvents[event] || event
}
const EventHandler = {
on(element, event, handler, delegationFunction) {
addHandler(element, event, handler, delegationFunction, false)
},
one(element, event, handler, delegationFunction) {
addHandler(element, event, handler, delegationFunction, true)
},
off(element, originalTypeEvent, handler, delegationFunction) {
if (typeof originalTypeEvent !== 'string' || !element) {
return
}
const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const inNamespace = typeEvent !== originalTypeEvent
const events = getElementEvents(element)
const storeElementEvent = events[typeEvent] || {}
const isNamespace = originalTypeEvent.startsWith('.')
if (typeof callable !== 'undefined') {
// Simplest case: handler is passed, remove that listener ONLY.
if (!Object.keys(storeElementEvent).length) {
return
}
removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)
return
}
if (isNamespace) {
for (const elementEvent of Object.keys(events)) {
removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
}
}
for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {
const handlerKey = keyHandlers.replace(stripUidRegex, '')
if (!inNamespace || originalTypeEvent.includes(handlerKey)) {
removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
}
}
},
trigger(element, event, args) {
if (typeof event !== 'string' || !element) {
return null
}
const $ = getjQuery()
const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent
let jQueryEvent = null
let bubbles = true
let nativeDispatch = true
let defaultPrevented = false
if (inNamespace && $) {
jQueryEvent = $.Event(event, args)
$(element).trigger(jQueryEvent)
bubbles = !jQueryEvent.isPropagationStopped()
nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
defaultPrevented = jQueryEvent.isDefaultPrevented()
}
const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)
if (defaultPrevented) {
evt.preventDefault()
}
if (nativeDispatch) {
element.dispatchEvent(evt)
}
if (evt.defaultPrevented && jQueryEvent) {
jQueryEvent.preventDefault()
}
return evt
}
}
function hydrateObj(obj, meta = {}) {
for (const [key, value] of Object.entries(meta)) {
try {
obj[key] = value
} catch {
Object.defineProperty(obj, key, {
configurable: true,
get() {
return value
}
})
}
}
return obj
}
export default EventHandler

View File

@ -0,0 +1,71 @@
/**
* --------------------------------------------------------------------------
* Bootstrap dom/manipulator.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
function normalizeData(value) {
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
if (value === Number(value).toString()) {
return Number(value)
}
if (value === '' || value === 'null') {
return null
}
if (typeof value !== 'string') {
return value
}
try {
return JSON.parse(decodeURIComponent(value))
} catch {
return value
}
}
function normalizeDataKey(key) {
return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)
}
const Manipulator = {
setDataAttribute(element, key, value) {
element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)
},
removeDataAttribute(element, key) {
element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)
},
getDataAttributes(element) {
if (!element) {
return {}
}
const attributes = {}
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))
for (const key of bsKeys) {
let pureKey = key.replace(/^bs/, '')
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)
attributes[pureKey] = normalizeData(element.dataset[key])
}
return attributes
},
getDataAttribute(element, key) {
return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))
}
}
export default Manipulator

View File

@ -0,0 +1,126 @@
/**
* --------------------------------------------------------------------------
* Bootstrap dom/selector-engine.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { isDisabled, isVisible, parseSelector } from '../util/index.js'
const getSelector = element => {
let selector = element.getAttribute('data-bs-target')
if (!selector || selector === '#') {
let hrefAttribute = element.getAttribute('href')
// The only valid content that could double as a selector are IDs or classes,
// so everything starting with `#` or `.`. If a "real" URL is used as the selector,
// `document.querySelector` will rightfully complain it is invalid.
// See https://github.com/twbs/bootstrap/issues/32273
if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
return null
}
// Just in case some CMS puts out a full URL with the anchor appended
if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
hrefAttribute = `#${hrefAttribute.split('#')[1]}`
}
selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
}
return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null
}
const SelectorEngine = {
find(selector, element = document.documentElement) {
return [].concat(...Element.prototype.querySelectorAll.call(element, selector))
},
findOne(selector, element = document.documentElement) {
return Element.prototype.querySelector.call(element, selector)
},
children(element, selector) {
return [].concat(...element.children).filter(child => child.matches(selector))
},
parents(element, selector) {
const parents = []
let ancestor = element.parentNode.closest(selector)
while (ancestor) {
parents.push(ancestor)
ancestor = ancestor.parentNode.closest(selector)
}
return parents
},
prev(element, selector) {
let previous = element.previousElementSibling
while (previous) {
if (previous.matches(selector)) {
return [previous]
}
previous = previous.previousElementSibling
}
return []
},
// TODO: this is now unused; remove later along with prev()
next(element, selector) {
let next = element.nextElementSibling
while (next) {
if (next.matches(selector)) {
return [next]
}
next = next.nextElementSibling
}
return []
},
focusableChildren(element) {
const focusables = [
'a',
'button',
'input',
'textarea',
'select',
'details',
'[tabindex]',
'[contenteditable="true"]'
].map(selector => `${selector}:not([tabindex^="-"])`).join(',')
return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))
},
getSelectorFromElement(element) {
const selector = getSelector(element)
if (selector) {
return SelectorEngine.findOne(selector) ? selector : null
}
return null
},
getElementFromSelector(element) {
const selector = getSelector(element)
return selector ? SelectorEngine.findOne(selector) : null
},
getMultipleElementsFromSelector(element) {
const selector = getSelector(element)
return selector ? SelectorEngine.find(selector) : []
}
}
export default SelectorEngine

View File

@ -0,0 +1,455 @@
/**
* --------------------------------------------------------------------------
* Bootstrap dropdown.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import * as Popper from '@popperjs/core'
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin,
execute,
getElement,
getNextActiveElement,
isDisabled,
isElement,
isRTL,
isVisible,
noop
} from './util/index.js'
/**
* Constants
*/
const NAME = 'dropdown'
const DATA_KEY = 'bs.dropdown'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ESCAPE_KEY = 'Escape'
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 EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_DROPUP = 'dropup'
const CLASS_NAME_DROPEND = 'dropend'
const CLASS_NAME_DROPSTART = 'dropstart'
const CLASS_NAME_DROPUP_CENTER = 'dropup-center'
const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'
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)'
const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'
const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'
const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'
const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'
const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'
const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
const PLACEMENT_TOPCENTER = 'top'
const PLACEMENT_BOTTOMCENTER = 'bottom'
const Default = {
autoClose: true,
boundary: 'clippingParents',
display: 'dynamic',
offset: [0, 2],
popperConfig: null,
reference: 'toggle'
}
const DefaultType = {
autoClose: '(boolean|string)',
boundary: '(string|element)',
display: 'string',
offset: '(array|string|function)',
popperConfig: '(null|object|function)',
reference: '(string|element|object)'
}
/**
* Class definition
*/
class Dropdown extends BaseComponent {
constructor(element, config) {
super(element, config)
this._popper = null
this._parent = this._element.parentNode // dropdown wrapper
// TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.findOne(SELECTOR_MENU, this._parent)
this._inNavbar = this._detectNavbar()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
return this._isShown() ? this.hide() : this.show()
}
show() {
if (isDisabled(this._element) || this._isShown()) {
return
}
const relatedTarget = {
relatedTarget: this._element
}
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
if (showEvent.defaultPrevented) {
return
}
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 && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
this._element.focus()
this._element.setAttribute('aria-expanded', true)
this._menu.classList.add(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_SHOW)
EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)
}
hide() {
if (isDisabled(this._element) || !this._isShown()) {
return
}
const relatedTarget = {
relatedTarget: this._element
}
this._completeHide(relatedTarget)
}
dispose() {
if (this._popper) {
this._popper.destroy()
}
super.dispose()
}
update() {
this._inNavbar = this._detectNavbar()
if (this._popper) {
this._popper.update()
}
}
// Private
_completeHide(relatedTarget) {
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
if (hideEvent.defaultPrevented) {
return
}
// If this is a touch-enabled device we remove the extra
// empty mouseover listeners we added for iOS support
if ('ontouchstart' in document.documentElement) {
for (const element of [].concat(...document.body.children)) {
EventHandler.off(element, 'mouseover', noop)
}
}
if (this._popper) {
this._popper.destroy()
}
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', 'false')
Manipulator.removeDataAttribute(this._menu, 'popper')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
}
_getConfig(config) {
config = super._getConfig(config)
if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'
) {
// Popper virtual elements require a getBoundingClientRect method
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
}
return config
}
_createPopper() {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
}
let referenceElement = this._element
if (this._config.reference === 'parent') {
referenceElement = this._parent
} else if (isElement(this._config.reference)) {
referenceElement = getElement(this._config.reference)
} else if (typeof this._config.reference === 'object') {
referenceElement = this._config.reference
}
const popperConfig = this._getPopperConfig()
this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
}
_isShown() {
return this._menu.classList.contains(CLASS_NAME_SHOW)
}
_getPlacement() {
const parentDropdown = this._parent
if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
return PLACEMENT_RIGHT
}
if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
return PLACEMENT_LEFT
}
if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {
return PLACEMENT_TOPCENTER
}
if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {
return PLACEMENT_BOTTOMCENTER
}
// We need to trim the value because custom properties can also include spaces
const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {
return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP
}
return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM
}
_detectNavbar() {
return this._element.closest(SELECTOR_NAVBAR) !== null
}
_getOffset() {
const { offset } = this._config
if (typeof offset === 'string') {
return offset.split(',').map(value => Number.parseInt(value, 10))
}
if (typeof offset === 'function') {
return popperData => offset(popperData, this._element)
}
return offset
}
_getPopperConfig() {
const defaultBsPopperConfig = {
placement: this._getPlacement(),
modifiers: [{
name: 'preventOverflow',
options: {
boundary: this._config.boundary
}
},
{
name: 'offset',
options: {
offset: this._getOffset()
}
}]
}
// 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
}]
}
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [defaultBsPopperConfig])
}
}
_selectMenuItem({ key, target }) {
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
if (!items.length) {
return
}
// if target isn't included in items (e.g. when expanding the dropdown)
// allow cycling to get the last item in case key equals ARROW_UP_KEY
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Dropdown.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
static clearMenus(event) {
if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
return
}
const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)
for (const toggle of openToggles) {
const context = Dropdown.getInstance(toggle)
if (!context || context._config.autoClose === false) {
continue
}
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
}
// 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
}
const relatedTarget = { relatedTarget: context._element }
if (event.type === 'click') {
relatedTarget.clickEvent = event
}
context._completeHide(relatedTarget)
}
}
static dataApiKeydownHandler(event) {
// If not an UP | DOWN | ESCAPE key => not a dropdown command
// If input/textarea && if key is other than ESCAPE => not a dropdown command
const isInput = /input|textarea/i.test(event.target.tagName)
const isEscapeEvent = event.key === ESCAPE_KEY
const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)
if (!isUpOrDownEvent && !isEscapeEvent) {
return
}
if (isInput && !isEscapeEvent) {
return
}
event.preventDefault()
// TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?
this :
(SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||
SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||
SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))
const instance = Dropdown.getOrCreateInstance(getToggleButton)
if (isUpOrDownEvent) {
event.stopPropagation()
instance.show()
instance._selectMenuItem(event)
return
}
if (instance._isShown()) { // else is escape and we check if it is shown
event.stopPropagation()
instance.hide()
getToggleButton.focus()
}
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
Dropdown.getOrCreateInstance(this).toggle()
})
/**
* jQuery
*/
defineJQueryPlugin(Dropdown)
export default Dropdown

378
static/bootstrap-5.3.3/js/src/modal.js vendored Normal file
View File

@ -0,0 +1,378 @@
/**
* --------------------------------------------------------------------------
* Bootstrap modal.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import Backdrop from './util/backdrop.js'
import { enableDismissTrigger } from './util/component-functions.js'
import FocusTrap from './util/focustrap.js'
import {
defineJQueryPlugin, isRTL, isVisible, reflow
} from './util/index.js'
import ScrollBarHelper from './util/scrollbar.js'
/**
* Constants
*/
const NAME = 'modal'
const DATA_KEY = 'bs.modal'
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_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${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,
focus: true,
keyboard: true
}
const DefaultType = {
backdrop: '(boolean|string)',
focus: 'boolean',
keyboard: 'boolean'
}
/**
* Class definition
*/
class Modal extends BaseComponent {
constructor(element, config) {
super(element, config)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
this._isShown = false
this._isTransitioning = false
this._scrollBar = new ScrollBarHelper()
this._addEventListeners()
}
// 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)
}
show(relatedTarget) {
if (this._isShown || this._isTransitioning) {
return
}
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
relatedTarget
})
if (showEvent.defaultPrevented) {
return
}
this._isShown = true
this._isTransitioning = true
this._scrollBar.hide()
document.body.classList.add(CLASS_NAME_OPEN)
this._adjustDialog()
this._backdrop.show(() => this._showElement(relatedTarget))
}
hide() {
if (!this._isShown || this._isTransitioning) {
return
}
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
if (hideEvent.defaultPrevented) {
return
}
this._isShown = false
this._isTransitioning = true
this._focustrap.deactivate()
this._element.classList.remove(CLASS_NAME_SHOW)
this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())
}
dispose() {
EventHandler.off(window, EVENT_KEY)
EventHandler.off(this._dialog, EVENT_KEY)
this._backdrop.dispose()
this._focustrap.deactivate()
super.dispose()
}
handleUpdate() {
this._adjustDialog()
}
// Private
_initializeBackDrop() {
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,
isAnimated: this._isAnimated()
})
}
_initializeFocusTrap() {
return new FocusTrap({
trapElement: this._element
})
}
_showElement(relatedTarget) {
// try to append dynamic modal
if (!document.body.contains(this._element)) {
document.body.append(this._element)
}
this._element.style.display = 'block'
this._element.removeAttribute('aria-hidden')
this._element.setAttribute('aria-modal', true)
this._element.setAttribute('role', 'dialog')
this._element.scrollTop = 0
const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
if (modalBody) {
modalBody.scrollTop = 0
}
reflow(this._element)
this._element.classList.add(CLASS_NAME_SHOW)
const transitionComplete = () => {
if (this._config.focus) {
this._focustrap.activate()
}
this._isTransitioning = false
EventHandler.trigger(this._element, EVENT_SHOWN, {
relatedTarget
})
}
this._queueCallback(transitionComplete, this._dialog, this._isAnimated())
}
_addEventListeners() {
EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
if (event.key !== ESCAPE_KEY) {
return
}
if (this._config.keyboard) {
this.hide()
return
}
this._triggerBackdropTransition()
})
EventHandler.on(window, EVENT_RESIZE, () => {
if (this._isShown && !this._isTransitioning) {
this._adjustDialog()
}
})
EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {
// a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {
if (this._element !== event.target || this._element !== event2.target) {
return
}
if (this._config.backdrop === 'static') {
this._triggerBackdropTransition()
return
}
if (this._config.backdrop) {
this.hide()
}
})
})
}
_hideModal() {
this._element.style.display = 'none'
this._element.setAttribute('aria-hidden', true)
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
this._isTransitioning = false
this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments()
this._scrollBar.reset()
EventHandler.trigger(this._element, EVENT_HIDDEN)
})
}
_isAnimated() {
return this._element.classList.contains(CLASS_NAME_FADE)
}
_triggerBackdropTransition() {
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
if (hideEvent.defaultPrevented) {
return
}
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 (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
return
}
if (!isModalOverflowing) {
this._element.style.overflowY = 'hidden'
}
this._element.classList.add(CLASS_NAME_STATIC)
this._queueCallback(() => {
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
*/
_adjustDialog() {
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const scrollbarWidth = this._scrollBar.getWidth()
const isBodyOverflowing = scrollbarWidth > 0
if (isBodyOverflowing && !isModalOverflowing) {
const property = isRTL() ? 'paddingLeft' : 'paddingRight'
this._element.style[property] = `${scrollbarWidth}px`
}
if (!isBodyOverflowing && isModalOverflowing) {
const property = isRTL() ? 'paddingRight' : 'paddingLeft'
this._element.style[property] = `${scrollbarWidth}px`
}
}
_resetAdjustments() {
this._element.style.paddingLeft = ''
this._element.style.paddingRight = ''
}
// Static
static jQueryInterface(config, relatedTarget) {
return this.each(function () {
const data = Modal.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](relatedTarget)
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = SelectorEngine.getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}
EventHandler.one(target, EVENT_SHOW, showEvent => {
if (showEvent.defaultPrevented) {
// only register focus restorer if modal will actually get shown
return
}
EventHandler.one(target, EVENT_HIDDEN, () => {
if (isVisible(this)) {
this.focus()
}
})
})
// 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)
data.toggle(this)
})
enableDismissTrigger(Modal)
/**
* jQuery
*/
defineJQueryPlugin(Modal)
export default Modal

View File

@ -0,0 +1,282 @@
/**
* --------------------------------------------------------------------------
* Bootstrap offcanvas.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import Backdrop from './util/backdrop.js'
import { enableDismissTrigger } from './util/component-functions.js'
import FocusTrap from './util/focustrap.js'
import {
defineJQueryPlugin,
isDisabled,
isVisible
} from './util/index.js'
import ScrollBarHelper from './util/scrollbar.js'
/**
* Constants
*/
const NAME = 'offcanvas'
const DATA_KEY = 'bs.offcanvas'
const EVENT_KEY = `.${DATA_KEY}`
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_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_RESIZE = `resize${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,
scroll: false
}
const DefaultType = {
backdrop: '(boolean|string)',
keyboard: 'boolean',
scroll: 'boolean'
}
/**
* Class definition
*/
class Offcanvas extends BaseComponent {
constructor(element, config) {
super(element, config)
this._isShown = false
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
this._addEventListeners()
}
// 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)
}
show(relatedTarget) {
if (this._isShown) {
return
}
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })
if (showEvent.defaultPrevented) {
return
}
this._isShown = true
this._backdrop.show()
if (!this._config.scroll) {
new ScrollBarHelper().hide()
}
this._element.setAttribute('aria-modal', true)
this._element.setAttribute('role', 'dialog')
this._element.classList.add(CLASS_NAME_SHOWING)
const completeCallBack = () => {
if (!this._config.scroll || this._config.backdrop) {
this._focustrap.activate()
}
this._element.classList.add(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOWING)
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })
}
this._queueCallback(completeCallBack, this._element, true)
}
hide() {
if (!this._isShown) {
return
}
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
if (hideEvent.defaultPrevented) {
return
}
this._focustrap.deactivate()
this._element.blur()
this._isShown = false
this._element.classList.add(CLASS_NAME_HIDING)
this._backdrop.hide()
const completeCallback = () => {
this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
if (!this._config.scroll) {
new ScrollBarHelper().reset()
}
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
this._queueCallback(completeCallback, this._element, true)
}
dispose() {
this._backdrop.dispose()
this._focustrap.deactivate()
super.dispose()
}
// Private
_initializeBackDrop() {
const clickCallback = () => {
if (this._config.backdrop === 'static') {
EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
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({
className: CLASS_NAME_BACKDROP,
isVisible,
isAnimated: true,
rootElement: this._element.parentNode,
clickCallback: isVisible ? clickCallback : null
})
}
_initializeFocusTrap() {
return new FocusTrap({
trapElement: this._element
})
}
_addEventListeners() {
EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
if (event.key !== ESCAPE_KEY) {
return
}
if (this._config.keyboard) {
this.hide()
return
}
EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
})
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Offcanvas.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = SelectorEngine.getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}
if (isDisabled(this)) {
return
}
EventHandler.one(target, EVENT_HIDDEN, () => {
// focus on trigger when it is closed
if (isVisible(this)) {
this.focus()
}
})
// avoid conflict when clicking a toggler of an offcanvas, while another is open
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, () => {
for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {
Offcanvas.getOrCreateInstance(selector).show()
}
})
EventHandler.on(window, EVENT_RESIZE, () => {
for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {
if (getComputedStyle(element).position !== 'fixed') {
Offcanvas.getOrCreateInstance(element).hide()
}
}
})
enableDismissTrigger(Offcanvas)
/**
* jQuery
*/
defineJQueryPlugin(Offcanvas)
export default Offcanvas

View File

@ -0,0 +1,97 @@
/**
* --------------------------------------------------------------------------
* Bootstrap popover.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Tooltip from './tooltip.js'
import { defineJQueryPlugin } from './util/index.js'
/**
* Constants
*/
const NAME = 'popover'
const SELECTOR_TITLE = '.popover-header'
const SELECTOR_CONTENT = '.popover-body'
const Default = {
...Tooltip.Default,
content: '',
offset: [0, 8],
placement: 'right',
template: '<div class="popover" role="tooltip">' +
'<div class="popover-arrow"></div>' +
'<h3 class="popover-header"></h3>' +
'<div class="popover-body"></div>' +
'</div>',
trigger: 'click'
}
const DefaultType = {
...Tooltip.DefaultType,
content: '(null|string|element|function)'
}
/**
* Class definition
*/
class Popover extends Tooltip {
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// 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)
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Popover.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* jQuery
*/
defineJQueryPlugin(Popover)
export default Popover

View File

@ -0,0 +1,296 @@
/**
* --------------------------------------------------------------------------
* Bootstrap scrollspy.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin, getElement, isDisabled, isVisible
} from './util/index.js'
/**
* Constants
*/
const NAME = 'scrollspy'
const DATA_KEY = 'bs.scrollspy'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_ACTIVATE = `activate${EVENT_KEY}`
const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
const CLASS_NAME_ACTIVE = 'active'
const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'
const SELECTOR_TARGET_LINKS = '[href]'
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
const SELECTOR_NAV_LINKS = '.nav-link'
const SELECTOR_NAV_ITEMS = '.nav-item'
const SELECTOR_LIST_ITEMS = '.list-group-item'
const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`
const SELECTOR_DROPDOWN = '.dropdown'
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
const Default = {
offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons
rootMargin: '0px 0px -25%',
smoothScroll: false,
target: null,
threshold: [0.1, 0.5, 1]
}
const DefaultType = {
offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons
rootMargin: 'string',
smoothScroll: 'boolean',
target: 'element',
threshold: 'array'
}
/**
* Class definition
*/
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element, config)
// this._element is the observablesContainer and config.target the menu links wrapper
this._targetLinks = new Map()
this._observableSections = new Map()
this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element
this._activeTarget = null
this._observer = null
this._previousScrollData = {
visibleEntryTop: 0,
parentScrollTop: 0
}
this.refresh() // initialize
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
refresh() {
this._initializeTargetsAndObservables()
this._maybeEnableSmoothScroll()
if (this._observer) {
this._observer.disconnect()
} else {
this._observer = this._getNewObserver()
}
for (const section of this._observableSections.values()) {
this._observer.observe(section)
}
}
dispose() {
this._observer.disconnect()
super.dispose()
}
// Private
_configAfterMerge(config) {
// TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case
config.target = getElement(config.target) || document.body
// TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
if (typeof config.threshold === 'string') {
config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))
}
return config
}
_maybeEnableSmoothScroll() {
if (!this._config.smoothScroll) {
return
}
// unregister any previous listeners
EventHandler.off(this._config.target, EVENT_CLICK)
EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {
const observableSection = this._observableSections.get(event.target.hash)
if (observableSection) {
event.preventDefault()
const root = this._rootElement || window
const height = observableSection.offsetTop - this._element.offsetTop
if (root.scrollTo) {
root.scrollTo({ top: height, behavior: 'smooth' })
return
}
// Chrome 60 doesn't support `scrollTo`
root.scrollTop = height
}
})
}
_getNewObserver() {
const options = {
root: this._rootElement,
threshold: this._config.threshold,
rootMargin: this._config.rootMargin
}
return new IntersectionObserver(entries => this._observerCallback(entries), options)
}
// The logic of selection
_observerCallback(entries) {
const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)
const activate = entry => {
this._previousScrollData.visibleEntryTop = entry.target.offsetTop
this._process(targetElement(entry))
}
const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
this._previousScrollData.parentScrollTop = parentScrollTop
for (const entry of entries) {
if (!entry.isIntersecting) {
this._activeTarget = null
this._clearActiveClass(targetElement(entry))
continue
}
const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop
// if we are scrolling down, pick the bigger offsetTop
if (userScrollsDown && entryIsLowerThanPrevious) {
activate(entry)
// if parent isn't scrolled, let's keep the first visible item, breaking the iteration
if (!parentScrollTop) {
return
}
continue
}
// if we are scrolling up, pick the smallest offsetTop
if (!userScrollsDown && !entryIsLowerThanPrevious) {
activate(entry)
}
}
}
_initializeTargetsAndObservables() {
this._targetLinks = new Map()
this._observableSections = new Map()
const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)
for (const anchor of targetLinks) {
// ensure that the anchor has an id and is not disabled
if (!anchor.hash || isDisabled(anchor)) {
continue
}
const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)
// ensure that the observableSection exists & is visible
if (isVisible(observableSection)) {
this._targetLinks.set(decodeURI(anchor.hash), anchor)
this._observableSections.set(anchor.hash, observableSection)
}
}
}
_process(target) {
if (this._activeTarget === target) {
return
}
this._clearActiveClass(this._config.target)
this._activeTarget = target
target.classList.add(CLASS_NAME_ACTIVE)
this._activateParents(target)
EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })
}
_activateParents(target) {
// Activate dropdown parents
if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))
.classList.add(CLASS_NAME_ACTIVE)
return
}
for (const listGroup of SelectorEngine.parents(target, 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_LINK_ITEMS)) {
item.classList.add(CLASS_NAME_ACTIVE)
}
}
}
_clearActiveClass(parent) {
parent.classList.remove(CLASS_NAME_ACTIVE)
const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent)
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)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
ScrollSpy.getOrCreateInstance(spy)
}
})
/**
* jQuery
*/
defineJQueryPlugin(ScrollSpy)
export default ScrollSpy

315
static/bootstrap-5.3.3/js/src/tab.js vendored Normal file
View File

@ -0,0 +1,315 @@
/**
* --------------------------------------------------------------------------
* Bootstrap tab.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index.js'
/**
* Constants
*/
const NAME = 'tab'
const DATA_KEY = 'bs.tab'
const EVENT_KEY = `.${DATA_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`
const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_DOWN_KEY = 'ArrowDown'
const HOME_KEY = 'Home'
const END_KEY = 'End'
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const CLASS_DROPDOWN = 'dropdown'
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'
const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`
const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'
const SELECTOR_OUTER = '.nav-item, .list-group-item'
const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]' // TODO: could only be `tab` in v6
const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`
const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`
/**
* Class definition
*/
class Tab extends BaseComponent {
constructor(element) {
super(element)
this._parent = this._element.closest(SELECTOR_TAB_PANEL)
if (!this._parent) {
return
// TODO: should throw exception in v6
// throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)
}
// Set up initial aria attributes
this._setInitialAttributes(this._parent, this._getChildren())
EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
}
// Getters
static get NAME() {
return NAME
}
// Public
show() { // Shows this elem and deactivate the active sibling if exists
const innerElem = this._element
if (this._elemIsActive(innerElem)) {
return
}
// Search for active tab on same parent to deactivate it
const active = this._getActiveElem()
const hideEvent = active ?
EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem }) :
null
const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { relatedTarget: active })
if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) {
return
}
this._deactivate(active, innerElem)
this._activate(innerElem, active)
}
// Private
_activate(element, relatedElem) {
if (!element) {
return
}
element.classList.add(CLASS_NAME_ACTIVE)
this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section
const complete = () => {
if (element.getAttribute('role') !== 'tab') {
element.classList.add(CLASS_NAME_SHOW)
return
}
element.removeAttribute('tabindex')
element.setAttribute('aria-selected', true)
this._toggleDropDown(element, true)
EventHandler.trigger(element, EVENT_SHOWN, {
relatedTarget: relatedElem
})
}
this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE))
}
_deactivate(element, relatedElem) {
if (!element) {
return
}
element.classList.remove(CLASS_NAME_ACTIVE)
element.blur()
this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too
const complete = () => {
if (element.getAttribute('role') !== 'tab') {
element.classList.remove(CLASS_NAME_SHOW)
return
}
element.setAttribute('aria-selected', false)
element.setAttribute('tabindex', '-1')
this._toggleDropDown(element, false)
EventHandler.trigger(element, EVENT_HIDDEN, { relatedTarget: relatedElem })
}
this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE))
}
_keydown(event) {
if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key))) {
return
}
event.stopPropagation()// stopPropagation/preventDefault both added to support up/down keys without scrolling the page
event.preventDefault()
const children = this._getChildren().filter(element => !isDisabled(element))
let nextActiveElement
if ([HOME_KEY, END_KEY].includes(event.key)) {
nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]
} else {
const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
nextActiveElement = getNextActiveElement(children, event.target, isNext, true)
}
if (nextActiveElement) {
nextActiveElement.focus({ preventScroll: true })
Tab.getOrCreateInstance(nextActiveElement).show()
}
}
_getChildren() { // collection of inner elements
return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent)
}
_getActiveElem() {
return this._getChildren().find(child => this._elemIsActive(child)) || null
}
_setInitialAttributes(parent, children) {
this._setAttributeIfNotExists(parent, 'role', 'tablist')
for (const child of children) {
this._setInitialAttributesOnChild(child)
}
}
_setInitialAttributesOnChild(child) {
child = this._getInnerElement(child)
const isActive = this._elemIsActive(child)
const outerElem = this._getOuterElement(child)
child.setAttribute('aria-selected', isActive)
if (outerElem !== child) {
this._setAttributeIfNotExists(outerElem, 'role', 'presentation')
}
if (!isActive) {
child.setAttribute('tabindex', '-1')
}
this._setAttributeIfNotExists(child, 'role', 'tab')
// set attributes to the related panel too
this._setInitialAttributesOnTargetPanel(child)
}
_setInitialAttributesOnTargetPanel(child) {
const target = SelectorEngine.getElementFromSelector(child)
if (!target) {
return
}
this._setAttributeIfNotExists(target, 'role', 'tabpanel')
if (child.id) {
this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`)
}
}
_toggleDropDown(element, open) {
const outerElem = this._getOuterElement(element)
if (!outerElem.classList.contains(CLASS_DROPDOWN)) {
return
}
const toggle = (selector, className) => {
const element = SelectorEngine.findOne(selector, outerElem)
if (element) {
element.classList.toggle(className, open)
}
}
toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE)
toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW)
outerElem.setAttribute('aria-expanded', open)
}
_setAttributeIfNotExists(element, attribute, value) {
if (!element.hasAttribute(attribute)) {
element.setAttribute(attribute, value)
}
}
_elemIsActive(elem) {
return elem.classList.contains(CLASS_NAME_ACTIVE)
}
// Try to get the inner element (usually the .nav-link)
_getInnerElement(elem) {
return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem)
}
// Try to get the outer element (usually the .nav-item)
_getOuterElement(elem) {
return elem.closest(SELECTOR_OUTER) || elem
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Tab.getOrCreateInstance(this)
if (typeof config !== 'string') {
return
}
if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}
if (isDisabled(this)) {
return
}
Tab.getOrCreateInstance(this).show()
})
/**
* Initialize on focus
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {
Tab.getOrCreateInstance(element)
}
})
/**
* jQuery
*/
defineJQueryPlugin(Tab)
export default Tab

225
static/bootstrap-5.3.3/js/src/toast.js vendored Normal file
View File

@ -0,0 +1,225 @@
/**
* --------------------------------------------------------------------------
* Bootstrap toast.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import { enableDismissTrigger } from './util/component-functions.js'
import { defineJQueryPlugin, reflow } from './util/index.js'
/**
* Constants
*/
const NAME = 'toast'
const DATA_KEY = 'bs.toast'
const EVENT_KEY = `.${DATA_KEY}`
const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`
const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_HIDE = 'hide' // @deprecated - kept here only for backwards compatibility
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_SHOWING = 'showing'
const DefaultType = {
animation: 'boolean',
autohide: 'boolean',
delay: 'number'
}
const Default = {
animation: true,
autohide: true,
delay: 5000
}
/**
* Class definition
*/
class Toast extends BaseComponent {
constructor(element, config) {
super(element, config)
this._timeout = null
this._hasMouseInteraction = false
this._hasKeyboardInteraction = false
this._setListeners()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
show() {
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW)
if (showEvent.defaultPrevented) {
return
}
this._clearTimeout()
if (this._config.animation) {
this._element.classList.add(CLASS_NAME_FADE)
}
const complete = () => {
this._element.classList.remove(CLASS_NAME_SHOWING)
EventHandler.trigger(this._element, EVENT_SHOWN)
this._maybeScheduleHide()
}
this._element.classList.remove(CLASS_NAME_HIDE) // @deprecated
reflow(this._element)
this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING)
this._queueCallback(complete, this._element, this._config.animation)
}
hide() {
if (!this.isShown()) {
return
}
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
if (hideEvent.defaultPrevented) {
return
}
const complete = () => {
this._element.classList.add(CLASS_NAME_HIDE) // @deprecated
this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW)
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
this._element.classList.add(CLASS_NAME_SHOWING)
this._queueCallback(complete, this._element, this._config.animation)
}
dispose() {
this._clearTimeout()
if (this.isShown()) {
this._element.classList.remove(CLASS_NAME_SHOW)
}
super.dispose()
}
isShown() {
return this._element.classList.contains(CLASS_NAME_SHOW)
}
// Private
_maybeScheduleHide() {
if (!this._config.autohide) {
return
}
if (this._hasMouseInteraction || this._hasKeyboardInteraction) {
return
}
this._timeout = setTimeout(() => {
this.hide()
}, this._config.delay)
}
_onInteraction(event, isInteracting) {
switch (event.type) {
case 'mouseover':
case 'mouseout': {
this._hasMouseInteraction = isInteracting
break
}
case 'focusin':
case 'focusout': {
this._hasKeyboardInteraction = isInteracting
break
}
default: {
break
}
}
if (isInteracting) {
this._clearTimeout()
return
}
const nextElement = event.relatedTarget
if (this._element === nextElement || this._element.contains(nextElement)) {
return
}
this._maybeScheduleHide()
}
_setListeners() {
EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true))
EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false))
EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true))
EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false))
}
_clearTimeout() {
clearTimeout(this._timeout)
this._timeout = null
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Toast.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](this)
}
})
}
}
/**
* Data API implementation
*/
enableDismissTrigger(Toast)
/**
* jQuery
*/
defineJQueryPlugin(Toast)
export default Toast

633
static/bootstrap-5.3.3/js/src/tooltip.js vendored Normal file
View File

@ -0,0 +1,633 @@
/**
* --------------------------------------------------------------------------
* Bootstrap tooltip.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import * as Popper from '@popperjs/core'
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import {
defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop
} from './util/index.js'
import { DefaultAllowlist } from './util/sanitizer.js'
import TemplateFactory from './util/template-factory.js'
/**
* Constants
*/
const NAME = 'tooltip'
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
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',
TOP: 'top',
RIGHT: isRTL() ? 'left' : 'right',
BOTTOM: 'bottom',
LEFT: isRTL() ? 'right' : 'left'
}
const Default = {
allowList: DefaultAllowlist,
animation: true,
boundary: 'clippingParents',
container: false,
customClass: '',
delay: 0,
fallbackPlacements: ['top', 'right', 'bottom', 'left'],
html: false,
offset: [0, 6],
placement: 'top',
popperConfig: null,
sanitize: true,
sanitizeFn: null,
selector: false,
template: '<div class="tooltip" role="tooltip">' +
'<div class="tooltip-arrow"></div>' +
'<div class="tooltip-inner"></div>' +
'</div>',
title: '',
trigger: 'hover focus'
}
const DefaultType = {
allowList: 'object',
animation: 'boolean',
boundary: '(string|element)',
container: '(string|element|boolean)',
customClass: '(string|function)',
delay: '(number|object)',
fallbackPlacements: 'array',
html: 'boolean',
offset: '(array|string|function)',
placement: '(string|function)',
popperConfig: '(null|object|function)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
selector: '(string|boolean)',
template: 'string',
title: '(string|element|function)',
trigger: 'string'
}
/**
* Class definition
*/
class Tooltip extends BaseComponent {
constructor(element, config) {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)')
}
super(element, config)
// Private
this._isEnabled = true
this._timeout = 0
this._isHovered = null
this._activeTrigger = {}
this._popper = null
this._templateFactory = null
this._newContent = null
// Protected
this.tip = null
this._setListeners()
if (!this._config.selector) {
this._fixTitle()
}
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
enable() {
this._isEnabled = true
}
disable() {
this._isEnabled = false
}
toggleEnabled() {
this._isEnabled = !this._isEnabled
}
toggle() {
if (!this._isEnabled) {
return
}
this._activeTrigger.click = !this._activeTrigger.click
if (this._isShown()) {
this._leave()
return
}
this._enter()
}
dispose() {
clearTimeout(this._timeout)
EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
if (this._element.getAttribute('data-bs-original-title')) {
this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))
}
this._disposePopper()
super.dispose()
}
show() {
if (this._element.style.display === 'none') {
throw new Error('Please use show on visible elements')
}
if (!(this._isWithContent() && this._isEnabled)) {
return
}
const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))
const shadowRoot = findShadowRoot(this._element)
const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)
if (showEvent.defaultPrevented || !isInTheDom) {
return
}
// TODO: v6 remove this or make it optional
this._disposePopper()
const tip = this._getTipElement()
this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
const { container } = this._config
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.append(tip)
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
}
this._popper = this._createPopper(tip)
tip.classList.add(CLASS_NAME_SHOW)
// 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) {
for (const element of [].concat(...document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
const complete = () => {
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))
if (this._isHovered === false) {
this._leave()
}
this._isHovered = false
}
this._queueCallback(complete, this.tip, this._isAnimated())
}
hide() {
if (!this._isShown()) {
return
}
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) {
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 = null // it is a trick to support manual triggering
const complete = () => {
if (this._isWithActiveTrigger()) {
return
}
if (!this._isHovered) {
this._disposePopper()
}
this._element.removeAttribute('aria-describedby')
EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))
}
this._queueCallback(complete, this.tip, this._isAnimated())
}
update() {
if (this._popper) {
this._popper.update()
}
}
// Protected
_isWithContent() {
return Boolean(this._getTitle())
}
_getTipElement() {
if (!this.tip) {
this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())
}
return this.tip
}
_createTipElement(content) {
const tip = this._getTemplateFactory(content).toHtml()
// TODO: remove this check in v6
if (!tip) {
return null
}
tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
// TODO: 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
}
setContent(content) {
this._newContent = content
if (this._isShown()) {
this._disposePopper()
this.show()
}
}
_getTemplateFactory(content) {
if (this._templateFactory) {
this._templateFactory.changeContent(content)
} else {
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() {
return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')
}
// Private
_initializeOnDelegatedTarget(event) {
return 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 = execute(this._config.placement, [this, tip, this._element])
const attachment = AttachmentMap[placement.toUpperCase()]
return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
}
_getOffset() {
const { offset } = this._config
if (typeof offset === 'string') {
return offset.split(',').map(value => Number.parseInt(value, 10))
}
if (typeof offset === 'function') {
return popperData => offset(popperData, this._element)
}
return offset
}
_resolvePossibleFunction(arg) {
return execute(arg, [this._element])
}
_getPopperConfig(attachment) {
const defaultBsPopperConfig = {
placement: attachment,
modifiers: [
{
name: 'flip',
options: {
fallbackPlacements: this._config.fallbackPlacements
}
},
{
name: 'offset',
options: {
offset: this._getOffset()
}
},
{
name: 'preventOverflow',
options: {
boundary: this._config.boundary
}
},
{
name: 'arrow',
options: {
element: `.${this.constructor.NAME}-arrow`
}
},
{
name: 'preSetPlacement',
enabled: true,
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)
}
}
]
}
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [defaultBsPopperConfig])
}
}
_setListeners() {
const triggers = this._config.trigger.split(' ')
for (const trigger of triggers) {
if (trigger === 'click') {
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event)
context.toggle()
})
} else if (trigger !== TRIGGER_MANUAL) {
const eventIn = trigger === TRIGGER_HOVER ?
this.constructor.eventName(EVENT_MOUSEENTER) :
this.constructor.eventName(EVENT_FOCUSIN)
const eventOut = trigger === TRIGGER_HOVER ?
this.constructor.eventName(EVENT_MOUSELEAVE) :
this.constructor.eventName(EVENT_FOCUSOUT)
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) {
this.hide()
}
}
EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
}
_fixTitle() {
const title = this._element.getAttribute('title')
if (!title) {
return
}
if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {
this._element.setAttribute('aria-label', title)
}
this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility
this._element.removeAttribute('title')
}
_enter() {
if (this._isShown() || this._isHovered) {
this._isHovered = true
return
}
this._isHovered = true
this._setTimeout(() => {
if (this._isHovered) {
this.show()
}
}, this._config.delay.show)
}
_leave() {
if (this._isWithActiveTrigger()) {
return
}
this._isHovered = false
this._setTimeout(() => {
if (!this._isHovered) {
this.hide()
}
}, this._config.delay.hide)
}
_setTimeout(handler, timeout) {
clearTimeout(this._timeout)
this._timeout = setTimeout(handler, timeout)
}
_isWithActiveTrigger() {
return Object.values(this._activeTrigger).includes(true)
}
_getConfig(config) {
const dataAttributes = Manipulator.getDataAttributes(this._element)
for (const dataAttribute of Object.keys(dataAttributes)) {
if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
delete dataAttributes[dataAttribute]
}
}
config = {
...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') {
config.delay = {
show: config.delay,
hide: config.delay
}
}
if (typeof config.title === 'number') {
config.title = config.title.toString()
}
if (typeof config.content === 'number') {
config.content = config.content.toString()
}
return config
}
_getDelegateConfig() {
const config = {}
for (const [key, value] of Object.entries(this._config)) {
if (this.constructor.Default[key] !== value) {
config[key] = value
}
}
config.selector = false
config.trigger = 'manual'
// In the future can be replaced with:
// const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
// `Object.fromEntries(keysWithDifferentValues)`
return config
}
_disposePopper() {
if (this._popper) {
this._popper.destroy()
this._popper = null
}
if (this.tip) {
this.tip.remove()
this.tip = null
}
}
// Static
static jQueryInterface(config) {
return this.each(function () {
const data = Tooltip.getOrCreateInstance(this, config)
if (typeof config !== 'string') {
return
}
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
})
}
}
/**
* jQuery
*/
defineJQueryPlugin(Tooltip)
export default Tooltip

View File

@ -0,0 +1,151 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/backdrop.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler from '../dom/event-handler.js'
import Config from './config.js'
import {
execute, executeAfterTransition, getElement, reflow
} from './index.js'
/**
* 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',
clickCallback: null,
isAnimated: false,
isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
rootElement: 'body' // give the choice to place backdrop under different elements
}
const DefaultType = {
className: 'string',
clickCallback: '(function|null)',
isAnimated: 'boolean',
isVisible: 'boolean',
rootElement: '(element|string)'
}
/**
* Class definition
*/
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)
return
}
this._append()
const element = this._getElement()
if (this._config.isAnimated) {
reflow(element)
}
element.classList.add(CLASS_NAME_SHOW)
this._emulateAnimation(() => {
execute(callback)
})
}
hide(callback) {
if (!this._config.isVisible) {
execute(callback)
return
}
this._getElement().classList.remove(CLASS_NAME_SHOW)
this._emulateAnimation(() => {
this.dispose()
execute(callback)
})
}
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')
backdrop.className = this._config.className
if (this._config.isAnimated) {
backdrop.classList.add(CLASS_NAME_FADE)
}
this._element = backdrop
}
return this._element
}
_configAfterMerge(config) {
// use getElement() with the default "body" to get a fresh Element on each instantiation
config.rootElement = getElement(config.rootElement)
return config
}
_append() {
if (this._isAppended) {
return
}
const element = this._getElement()
this._config.rootElement.append(element)
EventHandler.on(element, EVENT_MOUSEDOWN, () => {
execute(this._config.clickCallback)
})
this._isAppended = true
}
_emulateAnimation(callback) {
executeAfterTransition(callback, this._getElement(), this._config.isAnimated)
}
}
export default Backdrop

View File

@ -0,0 +1,35 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/component-functions.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler from '../dom/event-handler.js'
import SelectorEngine from '../dom/selector-engine.js'
import { isDisabled } from './index.js'
const enableDismissTrigger = (component, method = 'hide') => {
const clickEvent = `click.dismiss${component.EVENT_KEY}`
const name = component.NAME
EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) {
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}
if (isDisabled(this)) {
return
}
const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)
const instance = component.getOrCreateInstance(target)
// Method argument is left, for Alert and only, as it doesn't implement the 'hide' method
instance[method]()
})
}
export {
enableDismissTrigger
}

View File

@ -0,0 +1,65 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/config.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Manipulator from '../dom/manipulator.js'
import { isElement, toType } from './index.js'
/**
* Class definition
*/
class Config {
// Getters
static get Default() {
return {}
}
static get DefaultType() {
return {}
}
static get NAME() {
throw new Error('You have to implement the static method "NAME", for each component!')
}
_getConfig(config) {
config = this._mergeConfigObj(config)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
_configAfterMerge(config) {
return config
}
_mergeConfigObj(config, element) {
const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse
return {
...this.constructor.Default,
...(typeof jsonConfig === 'object' ? jsonConfig : {}),
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
...(typeof config === 'object' ? config : {})
}
}
_typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
for (const [property, expectedTypes] of Object.entries(configTypes)) {
const value = config[property]
const valueType = isElement(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

View File

@ -0,0 +1,115 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/focustrap.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler from '../dom/event-handler.js'
import SelectorEngine from '../dom/selector-engine.js'
import Config from './config.js'
/**
* Constants
*/
const NAME = 'focustrap'
const DATA_KEY = 'bs.focustrap'
const EVENT_KEY = `.${DATA_KEY}`
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`
const TAB_KEY = 'Tab'
const TAB_NAV_FORWARD = 'forward'
const TAB_NAV_BACKWARD = 'backward'
const Default = {
autofocus: true,
trapElement: null // The element to trap focus inside of
}
const DefaultType = {
autofocus: 'boolean',
trapElement: 'element'
}
/**
* Class definition
*/
class FocusTrap extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
this._isActive = false
this._lastTabNavDirection = null
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
activate() {
if (this._isActive) {
return
}
if (this._config.autofocus) {
this._config.trapElement.focus()
}
EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop
EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))
EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
this._isActive = true
}
deactivate() {
if (!this._isActive) {
return
}
this._isActive = false
EventHandler.off(document, EVENT_KEY)
}
// Private
_handleFocusin(event) {
const { trapElement } = this._config
if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {
return
}
const elements = SelectorEngine.focusableChildren(trapElement)
if (elements.length === 0) {
trapElement.focus()
} else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {
elements[elements.length - 1].focus()
} else {
elements[0].focus()
}
}
_handleKeydown(event) {
if (event.key !== TAB_KEY) {
return
}
this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
}
}
export default FocusTrap

View File

@ -0,0 +1,306 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/index.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
const MAX_UID = 1_000_000
const MILLISECONDS_MULTIPLIER = 1000
const TRANSITION_END = 'transitionend'
/**
* Properly escape IDs selectors to handle weird IDs
* @param {string} selector
* @returns {string}
*/
const parseSelector = selector => {
if (selector && window.CSS && window.CSS.escape) {
// document.querySelector needs escaping to handle IDs (html5+) containing for instance /
selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)
}
return selector
}
// Shout-out Angus Croll (https://goo.gl/pxwQGp)
const toType = object => {
if (object === null || object === undefined) {
return `${object}`
}
return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase()
}
/**
* Public Util API
*/
const getUID = prefix => {
do {
prefix += Math.floor(Math.random() * MAX_UID)
} while (document.getElementById(prefix))
return prefix
}
const getTransitionDurationFromElement = element => {
if (!element) {
return 0
}
// Get transition-duration of the element
let { transitionDuration, transitionDelay } = window.getComputedStyle(element)
const floatTransitionDuration = Number.parseFloat(transitionDuration)
const floatTransitionDelay = Number.parseFloat(transitionDelay)
// Return 0 if element or transition duration is not found
if (!floatTransitionDuration && !floatTransitionDelay) {
return 0
}
// If multiple durations are defined, take the first
transitionDuration = transitionDuration.split(',')[0]
transitionDelay = transitionDelay.split(',')[0]
return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER
}
const triggerTransitionEnd = element => {
element.dispatchEvent(new Event(TRANSITION_END))
}
const isElement = object => {
if (!object || typeof object !== 'object') {
return false
}
if (typeof object.jquery !== 'undefined') {
object = object[0]
}
return typeof object.nodeType !== 'undefined'
}
const getElement = object => {
// it's a jQuery object or a node element
if (isElement(object)) {
return object.jquery ? object[0] : object
}
if (typeof object === 'string' && object.length > 0) {
return document.querySelector(parseSelector(object))
}
return null
}
const isVisible = element => {
if (!isElement(element) || element.getClientRects().length === 0) {
return false
}
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 => {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return true
}
if (element.classList.contains('disabled')) {
return true
}
if (typeof element.disabled !== 'undefined') {
return element.disabled
}
return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
}
const findShadowRoot = element => {
if (!document.documentElement.attachShadow) {
return null
}
// Can find the shadow root otherwise it'll return the document
if (typeof element.getRootNode === 'function') {
const root = element.getRootNode()
return root instanceof ShadowRoot ? root : null
}
if (element instanceof ShadowRoot) {
return element
}
// when we don't find a shadow root
if (!element.parentNode) {
return null
}
return findShadowRoot(element.parentNode)
}
const noop = () => {}
/**
* Trick to restart an element's animation
*
* @param {HTMLElement} element
* @return void
*
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = element => {
element.offsetHeight // eslint-disable-line no-unused-expressions
}
const getjQuery = () => {
if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
return window.jQuery
}
return null
}
const DOMContentLoadedCallbacks = []
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
for (const callback of DOMContentLoadedCallbacks) {
callback()
}
})
}
DOMContentLoadedCallbacks.push(callback)
} else {
callback()
}
}
const isRTL = () => document.documentElement.dir === 'rtl'
const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
const $ = getjQuery()
/* istanbul ignore if */
if ($) {
const name = plugin.NAME
const JQUERY_NO_CONFLICT = $.fn[name]
$.fn[name] = plugin.jQueryInterface
$.fn[name].Constructor = plugin
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT
return plugin.jQueryInterface
}
}
})
}
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue
}
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
if (!waitForTransition) {
execute(callback)
return
}
const durationPadding = 5
const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding
let called = false
const handler = ({ target }) => {
if (target !== transitionElement) {
return
}
called = true
transitionElement.removeEventListener(TRANSITION_END, handler)
execute(callback)
}
transitionElement.addEventListener(TRANSITION_END, handler)
setTimeout(() => {
if (!called) {
triggerTransitionEnd(transitionElement)
}
}, emulatedDuration)
}
/**
* Return the previous/next element of a list.
*
* @param {array} list The list of elements
* @param activeElement The active element
* @param shouldGetNext Choose to get next or previous element
* @param isCycleAllowed
* @return {Element|elem} The proper element
*/
const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
const listLength = list.length
let index = list.indexOf(activeElement)
// if the element does not exist in the list return an element
// depending on the direction and if cycle is allowed
if (index === -1) {
return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]
}
index += shouldGetNext ? 1 : -1
if (isCycleAllowed) {
index = (index + listLength) % listLength
}
return list[Math.max(0, Math.min(index, listLength - 1))]
}
export {
defineJQueryPlugin,
execute,
executeAfterTransition,
findShadowRoot,
getElement,
getjQuery,
getNextActiveElement,
getTransitionDurationFromElement,
getUID,
isDisabled,
isElement,
isRTL,
isVisible,
noop,
onDOMContentLoaded,
parseSelector,
reflow,
triggerTransitionEnd,
toType
}

View File

@ -0,0 +1,117 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/sanitizer.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
// js-docs-start allow-list
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
export const DefaultAllowlist = {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
a: ['target', 'href', 'title', 'rel'],
area: [],
b: [],
br: [],
col: [],
code: [],
dd: [],
div: [],
dl: [],
dt: [],
em: [],
hr: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
i: [],
img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
li: [],
ol: [],
p: [],
pre: [],
s: [],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
u: [],
ul: []
}
// js-docs-end allow-list
const uriAttributes = new Set([
'background',
'cite',
'href',
'itemtype',
'longdesc',
'poster',
'src',
'xlink:href'
])
/**
* A pattern that recognizes URLs that are safe wrt. XSS in URL navigation
* contexts.
*
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
*/
// eslint-disable-next-line unicorn/better-regex
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i
const allowedAttribute = (attribute, allowedAttributeList) => {
const attributeName = attribute.nodeName.toLowerCase()
if (allowedAttributeList.includes(attributeName)) {
if (uriAttributes.has(attributeName)) {
return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))
}
return true
}
// Check if a regular expression validates the attribute.
return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
.some(regex => regex.test(attributeName))
}
export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
if (!unsafeHtml.length) {
return 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 (const element of elements) {
const elementName = element.nodeName.toLowerCase()
if (!Object.keys(allowList).includes(elementName)) {
element.remove()
continue
}
const attributeList = [].concat(...element.attributes)
const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])
for (const attribute of attributeList) {
if (!allowedAttribute(attribute, allowedAttributes)) {
element.removeAttribute(attribute.nodeName)
}
}
}
return createdDocument.body.innerHTML
}

View File

@ -0,0 +1,114 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/scrollBar.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Manipulator from '../dom/manipulator.js'
import SelectorEngine from '../dom/selector-engine.js'
import { isElement } from './index.js'
/**
* 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
return Math.abs(window.innerWidth - documentWidth)
}
hide() {
const width = this.getWidth()
this._disableOverFlow()
// give padding to element to balance the hidden scrollbar 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, 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, styleProperty, callback) {
const scrollbarWidth = this.getWidth()
const manipulationCallBack = element => {
if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {
return
}
this._saveInitialAttribute(element, styleProperty)
const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)
element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)
}
this._applyManipulationCallback(selector, manipulationCallBack)
}
_saveInitialAttribute(element, styleProperty) {
const actualValue = element.style.getPropertyValue(styleProperty)
if (actualValue) {
Manipulator.setDataAttribute(element, styleProperty, actualValue)
}
}
_resetElementAttributes(selector, styleProperty) {
const manipulationCallBack = element => {
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)
}
_applyManipulationCallback(selector, callBack) {
if (isElement(selector)) {
callBack(selector)
return
}
for (const sel of SelectorEngine.find(selector, this._element)) {
callBack(sel)
}
}
}
export default ScrollBarHelper

View File

@ -0,0 +1,146 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/swipe.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler from '../dom/event-handler.js'
import Config from './config.js'
import { execute } from './index.js'
/**
* 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 = {
endCallback: null,
leftCallback: null,
rightCallback: null
}
const DefaultType = {
endCallback: '(function|null)',
leftCallback: '(function|null)',
rightCallback: '(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

View File

@ -0,0 +1,160 @@
/**
* --------------------------------------------------------------------------
* Bootstrap util/template-factory.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import SelectorEngine from '../dom/selector-engine.js'
import Config from './config.js'
import { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'
import { execute, getElement, isElement } from './index.js'
/**
* Constants
*/
const NAME = 'TemplateFactory'
const Default = {
allowList: DefaultAllowlist,
content: {}, // { selector : text , selector2 : text2 , }
extraClass: '',
html: false,
sanitize: true,
sanitizeFn: null,
template: '<div></div>'
}
const DefaultType = {
allowList: 'object',
content: 'object',
extraClass: '(string|function)',
html: 'boolean',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
template: 'string'
}
const DefaultContentType = {
entry: '(string|element|function|null)',
selector: '(string|element)'
}
/**
* 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 execute(arg, [this])
}
_putElementInTemplate(element, templateElement) {
if (this._config.html) {
templateElement.innerHTML = ''
templateElement.append(element)
return
}
templateElement.textContent = element.textContent
}
}
export default TemplateFactory

View File

@ -0,0 +1,73 @@
## How does Bootstrap's test suite work?
Bootstrap uses [Jasmine](https://jasmine.github.io/). Each plugin has a file dedicated to its tests in `tests/unit/<plugin-name>.spec.js`.
- `visual/` contains "visual" tests which are run interactively in real browsers and require manual verification by humans.
To run the unit test suite via [Karma](https://karma-runner.github.io/), run `npm run js-test`.
To run the unit test suite via [Karma](https://karma-runner.github.io/) and debug, run `npm run js-debug`.
## How do I add a new unit test?
1. Locate and open the file dedicated to the plugin which you need to add tests to (`tests/unit/<plugin-name>.spec.js`).
2. Review the [Jasmine API Documentation](https://jasmine.github.io/pages/docs_home.html) and use the existing tests as references for how to structure your new tests.
3. Write the necessary unit test(s) for the new or revised functionality.
4. Run `npm run js-test` to see the results of your newly-added test(s).
**Note:** Your new unit tests should fail before your changes are applied to the plugin, and should pass after your changes are applied to the plugin.
## What should a unit test look like?
- Each test should have a unique name clearly stating what unit is being tested.
- Each test should be in the corresponding `describe`.
- Each test should test only one unit per test, although one test can include several assertions. Create multiple tests for multiple units of functionality.
- Each test should use [`expect`](https://jasmine.github.io/api/edge/matchers.html) to ensure something is expected.
- Each test should follow the project's [JavaScript Code Guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#js)
## Code coverage
Currently we're aiming for at least 90% test coverage for our code. To ensure your changes meet or exceed this limit, run `npm run js-test-karma` and open the file in `js/coverage/lcov-report/index.html` to see the code coverage for each plugin. See more details when you select a plugin and ensure your change is fully covered by unit tests.
### Example tests
```js
// Synchronous test
describe('getInstance', () => {
it('should return null if there is no instance', () => {
// Make assertion
expect(Tab.getInstance(fixtureEl)).toBeNull()
})
it('should return this instance', () => {
fixtureEl.innerHTML = '<div></div>'
const divEl = fixtureEl.querySelector('div')
const tab = new Tab(divEl)
// Make assertion
expect(Tab.getInstance(divEl)).toEqual(tab)
})
})
// Asynchronous test
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
})
tooltipEl.addEventListener('shown.bs.tooltip', () => {
const tip = document.querySelector('.tooltip')
expect(tip).not.toBeNull()
expect(tip.classList.contains('fade')).toEqual(false)
resolve()
})
tooltip.show()
})
})
```

View File

@ -0,0 +1,80 @@
/* eslint-disable camelcase */
'use strict'
const browsers = {
safariMac: {
base: 'BrowserStack',
os: 'OS X',
os_version: 'Catalina',
browser: 'Safari',
browser_version: 'latest'
},
chromeMac: {
base: 'BrowserStack',
os: 'OS X',
os_version: 'Catalina',
browser: 'Chrome',
browser_version: 'latest'
},
firefoxMac: {
base: 'BrowserStack',
os: 'OS X',
os_version: 'Catalina',
browser: 'Firefox',
browser_version: 'latest'
},
chromeWin10: {
base: 'BrowserStack',
os: 'Windows',
os_version: '10',
browser: 'Chrome',
browser_version: '60'
},
firefoxWin10: {
base: 'BrowserStack',
os: 'Windows',
os_version: '10',
browser: 'Firefox',
browser_version: '60'
},
chromeWin10Latest: {
base: 'BrowserStack',
os: 'Windows',
os_version: '10',
browser: 'Chrome',
browser_version: 'latest'
},
firefoxWin10Latest: {
base: 'BrowserStack',
os: 'Windows',
os_version: '10',
browser: 'Firefox',
browser_version: 'latest'
},
iphone7: {
base: 'BrowserStack',
os: 'ios',
os_version: '12.0',
device: 'iPhone 7',
real_mobile: true
},
iphone12: {
base: 'BrowserStack',
os: 'ios',
os_version: '14.0',
device: 'iPhone 12',
real_mobile: true
},
pixel2: {
base: 'BrowserStack',
os: 'android',
os_version: '8.0',
device: 'Google Pixel 2',
real_mobile: true
}
}
module.exports = {
browsers
}

View File

@ -0,0 +1,47 @@
const fixtureId = 'fixture'
export const getFixture = () => {
let fixtureElement = document.getElementById(fixtureId)
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 fixtureElement
}
export const clearFixture = () => {
const fixtureElement = getFixture()
fixtureElement.innerHTML = ''
}
export const createEvent = (eventName, parameters = {}) => {
return new Event(eventName, parameters)
}
export const jQueryMock = {
elements: undefined,
fn: {},
each(fn) {
for (const element of this.elements) {
fn.call(element)
}
}
}
export const clearBodyAndDocument = () => {
const attributes = ['data-bs-padding-right', 'style']
for (const attribute of attributes) {
document.documentElement.removeAttribute(attribute)
document.body.removeAttribute(attribute)
}
}

View File

@ -0,0 +1,9 @@
/* eslint-disable import/extensions, import/no-unassigned-import */
import Tooltip from '../../dist/tooltip'
import '../../dist/carousel'
window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode))
})

View File

@ -0,0 +1,6 @@
import { Tooltip } from '../../../dist/js/bootstrap.esm.js'
window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode))
})

View File

@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="../../../dist/css/bootstrap.min.css" rel="stylesheet">
<title>Hello, world!</title>
</head>
<body>
<div class="container py-4">
<h1>Hello, world!</h1>
<div class="mt-5">
<button type="button" class="btn btn-secondary mb-3" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top">
Tooltip on top
</button>
<div id="carouselExampleIndicators" class="carousel slide mt-2" data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="0" aria-label="Slide 1"></button>
<button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="1" class="active" aria-current="true" aria-label="Slide 2"></button>
<button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="2" aria-label="Slide 3"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item">
<img class="d-block w-100" alt="First slide" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22800%22%20height%3D%22400%22%20preserveAspectRatio%3D%22none%22%20viewBox%3D%220%200%20800%20400%22%3E%3Cpath%20fill%3D%22%23777%22%20d%3D%22M0%200h800v400H0z%22%2F%3E%3Ctext%20x%3D%22285.922%22%20y%3D%22217.7%22%20fill%3D%22%23555%22%20font-family%3D%22Helvetica%2Cmonospace%22%20font-size%3D%2240pt%22%20font-weight%3D%22400%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fsvg%3E">
<div class="carousel-caption d-none d-md-block">
<h5>First slide label</h5>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div>
</div>
<div class="carousel-item active">
<img class="d-block w-100" alt="Second slide" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22800%22%20height%3D%22400%22%20preserveAspectRatio%3D%22none%22%20viewBox%3D%220%200%20800%20400%22%3E%3Cpath%20fill%3D%22%23777%22%20d%3D%22M0%200h800v400H0z%22%2F%3E%3Ctext%20x%3D%22285.922%22%20y%3D%22217.7%22%20fill%3D%22%23555%22%20font-family%3D%22Helvetica%2Cmonospace%22%20font-size%3D%2240pt%22%20font-weight%3D%22400%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fsvg%3E">
<div class="carousel-caption d-none d-md-block">
<h5>Second slide label</h5>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</div>
</div>
<div class="carousel-item">
<img class="d-block w-100" alt="Third slide" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22800%22%20height%3D%22400%22%20preserveAspectRatio%3D%22none%22%20viewBox%3D%220%200%20800%20400%22%3E%3Cpath%20fill%3D%22%23777%22%20d%3D%22M0%200h800v400H0z%22%2F%3E%3Ctext%20x%3D%22285.922%22%20y%3D%22217.7%22%20fill%3D%22%23555%22%20font-family%3D%22Helvetica%2Cmonospace%22%20font-size%3D%2240pt%22%20font-weight%3D%22400%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fsvg%3E">
<div class="carousel-caption d-none d-md-block">
<h5>Third slide label</h5>
<p>Praesent commodo cursus magna, vel scelerisque nisl consectetur.</p>
</div>
</div>
</div>
<a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</a>
</div>
</div>
</div>
<script src="../../coverage/bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,17 @@
'use strict'
const commonjs = require('@rollup/plugin-commonjs')
const configRollup = require('./rollup.bundle.js')
const config = {
...configRollup,
input: 'js/tests/integration/bundle-modularity.js',
output: {
file: 'js/coverage/bundle-modularity.js',
format: 'iife'
}
}
config.plugins.unshift(commonjs())
module.exports = config

View File

@ -0,0 +1,24 @@
'use strict'
const { babel } = require('@rollup/plugin-babel')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const replace = require('@rollup/plugin-replace')
module.exports = {
input: 'js/tests/integration/bundle.js',
output: {
file: 'js/coverage/bundle.js',
format: 'iife'
},
plugins: [
replace({
'process.env.NODE_ENV': '"production"',
preventAssignment: true
}),
nodeResolve(),
babel({
exclude: 'node_modules/**',
babelHelpers: 'bundled'
})
]
}

View File

@ -0,0 +1,169 @@
'use strict'
const path = require('node:path')
const ip = require('ip')
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 } = require('./browsers.js')
const ENV = process.env
const BROWSERSTACK = Boolean(ENV.BROWSERSTACK)
const DEBUG = Boolean(ENV.DEBUG)
const JQUERY_TEST = Boolean(ENV.JQUERY)
const frameworks = [
'jasmine'
]
const plugins = [
'karma-jasmine',
'karma-rollup-preprocessor'
]
const reporters = ['dots']
const detectBrowsers = {
usePhantomJS: false,
postDetection(availableBrowser) {
// On CI just use Chrome
if (ENV.CI === true) {
return ['ChromeHeadless']
}
if (availableBrowser.includes('Chrome')) {
return DEBUG ? ['Chrome'] : ['ChromeHeadless']
}
if (availableBrowser.includes('Chromium')) {
return DEBUG ? ['Chromium'] : ['ChromiumHeadless']
}
if (availableBrowser.includes('Firefox')) {
return DEBUG ? ['Firefox'] : ['FirefoxHeadless']
}
throw new Error('Please install Chrome, Chromium or Firefox')
}
}
const config = {
basePath: '../..',
port: 9876,
colors: true,
autoWatch: false,
singleRun: true,
concurrency: Number.POSITIVE_INFINITY,
client: {
clearContext: false
},
files: [
'node_modules/hammer-simulator/index.js',
{
pattern: 'js/tests/unit/**/!(jquery).spec.js',
watched: !BROWSERSTACK
}
],
preprocessors: {
'js/tests/unit/**/*.spec.js': ['rollup']
},
rollupPreprocessor: {
plugins: [
replace({
'process.env.NODE_ENV': '"dev"',
preventAssignment: true
}),
istanbul({
exclude: [
'node_modules/**',
'js/tests/unit/**/*.spec.js',
'js/tests/helpers/**/*.js'
]
}),
babel({
// Only transpile our source code
exclude: 'node_modules/**',
// Inline the required helpers in each file
babelHelpers: 'inline'
}),
nodeResolve()
],
output: {
format: 'iife',
name: 'bootstrapTest',
sourcemap: 'inline',
generatedCode: 'es2015'
}
}
}
if (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()}`,
project: 'Bootstrap',
retryLimit: 2
}
plugins.push('karma-browserstack-launcher', 'karma-jasmine-html-reporter')
config.customLaunchers = browsers
config.browsers = Object.keys(browsers)
reporters.push('BrowserStack', 'kjhtml')
} else if (JQUERY_TEST) {
frameworks.push('detectBrowsers')
plugins.push(
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-detect-browsers'
)
config.detectBrowsers = detectBrowsers
config.files = [
'node_modules/jquery/dist/jquery.slim.min.js',
{
pattern: 'js/tests/unit/jquery.spec.js',
watched: false
}
]
} else {
frameworks.push('detectBrowsers')
plugins.push(
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-detect-browsers',
'karma-coverage-istanbul-reporter'
)
reporters.push('coverage-istanbul')
config.detectBrowsers = detectBrowsers
config.coverageIstanbulReporter = {
dir: path.resolve(__dirname, '../coverage/'),
reports: ['lcov', 'text-summary'],
thresholds: {
emitWarning: false,
global: {
statements: 90,
branches: 89,
functions: 90,
lines: 90
}
}
}
if (DEBUG) {
config.hostname = ip.address()
plugins.push('karma-jasmine-html-reporter')
reporters.push('kjhtml')
config.singleRun = false
config.autoWatch = true
}
}
config.frameworks = frameworks
config.plugins = plugins
config.reporters = reporters
module.exports = karmaConfig => {
config.logLevel = karmaConfig.LOG_ERROR
karmaConfig.set(config)
}

View File

@ -0,0 +1,259 @@
import Alert from '../../src/alert.js'
import { getTransitionDurationFromElement } from '../../src/util/index.js'
import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js'
describe('Alert', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = fixtureEl.querySelector('.alert')
const alertBySelector = new Alert('.alert')
const alertByElement = new Alert(alertEl)
expect(alertBySelector._element).toEqual(alertEl)
expect(alertByElement._element).toEqual(alertEl)
})
it('should return version', () => {
expect(Alert.VERSION).toEqual(jasmine.any(String))
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Alert.DATA_KEY).toEqual('bs.alert')
})
})
describe('data-api', () => {
it('should close an alert without instantiating it manually', () => {
fixtureEl.innerHTML = [
'<div class="alert">',
' <button type="button" data-bs-dismiss="alert">x</button>',
'</div>'
].join('')
const button = document.querySelector('button')
button.click()
expect(document.querySelectorAll('.alert')).toHaveSize(0)
})
it('should close an alert without instantiating it manually with the parent selector', () => {
fixtureEl.innerHTML = [
'<div class="alert">',
' <button type="button" data-bs-target=".alert" data-bs-dismiss="alert">x</button>',
'</div>'
].join('')
const button = document.querySelector('button')
button.click()
expect(document.querySelectorAll('.alert')).toHaveSize(0)
})
})
describe('close', () => {
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)
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert')).toHaveSize(0)
expect(spy).not.toHaveBeenCalled()
resolve()
})
alert.close()
})
})
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)
alertEl.addEventListener('transitionend', () => {
expect().nothing()
})
alertEl.addEventListener('closed.bs.alert', () => {
expect(document.querySelectorAll('.alert')).toHaveSize(0)
resolve()
})
alert.close()
})
})
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)
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()
})
})
})
describe('dispose', () => {
it('should dispose an alert', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = document.querySelector('.alert')
const alert = new Alert(alertEl)
expect(Alert.getInstance(alertEl)).not.toBeNull()
alert.dispose()
expect(Alert.getInstance(alertEl)).toBeNull()
})
})
describe('jQueryInterface', () => {
it('should handle config passed and toggle existing alert', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = fixtureEl.querySelector('.alert')
const alert = new Alert(alertEl)
const spy = spyOn(alert, 'close')
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [alertEl]
jQueryMock.fn.alert.call(jQueryMock, 'close')
expect(spy).toHaveBeenCalled()
})
it('should create new alert instance and call close', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = fixtureEl.querySelector('.alert')
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [alertEl]
expect(Alert.getInstance(alertEl)).toBeNull()
jQueryMock.fn.alert.call(jQueryMock, 'close')
expect(fixtureEl.querySelector('.alert')).toBeNull()
})
it('should just create an alert instance without calling close', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'
const alertEl = fixtureEl.querySelector('.alert')
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [alertEl]
jQueryMock.fn.alert.call(jQueryMock)
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', () => {
it('should return alert instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const alert = new Alert(div)
expect(Alert.getInstance(div)).toEqual(alert)
expect(Alert.getInstance(div)).toBeInstanceOf(Alert)
})
it('should return null when there is no alert instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Alert.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return alert instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const alert = new Alert(div)
expect(Alert.getOrCreateInstance(div)).toEqual(alert)
expect(Alert.getInstance(div)).toEqual(Alert.getOrCreateInstance(div, {}))
expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert)
})
it('should return new instance when there is no alert instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Alert.getInstance(div)).toBeNull()
expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert)
})
})
})

View File

@ -0,0 +1,168 @@
import BaseComponent from '../../src/base-component.js'
import EventHandler from '../../src/dom/event-handler.js'
import { noop } from '../../src/util/index.js'
import { clearFixture, getFixture } from '../helpers/fixture.js'
class DummyClass extends BaseComponent {
constructor(element) {
super(element)
EventHandler.on(this._element, `click${DummyClass.EVENT_KEY}`, noop)
}
static get NAME() {
return 'dummy'
}
}
describe('Base Component', () => {
let fixtureEl
const name = 'dummy'
let element
let instance
const createInstance = () => {
fixtureEl.innerHTML = '<div id="foo"></div>'
element = fixtureEl.querySelector('#foo')
instance = new DummyClass(element)
}
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('Static Methods', () => {
describe('VERSION', () => {
it('should return version', () => {
expect(DummyClass.VERSION).toEqual(jasmine.any(String))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(DummyClass.DATA_KEY).toEqual(`bs.${name}`)
})
})
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)
})
})
describe('EVENT_KEY', () => {
it('should return plugin event key', () => {
expect(DummyClass.EVENT_KEY).toEqual(`.bs.${name}`)
})
})
})
describe('Public Methods', () => {
describe('constructor', () => {
it('should accept element, either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = [
'<div id="foo"></div>',
'<div id="bar"></div>'
].join('')
const el = fixtureEl.querySelector('#foo')
const elInstance = new DummyClass(el)
const selectorInstance = new DummyClass('#bar')
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()
expect(DummyClass.getInstance(element)).not.toBeNull()
instance.dispose()
expect(DummyClass.getInstance(element)).toBeNull()
expect(instance._element).toBeNull()
})
it('should de-register element event listeners', () => {
createInstance()
const spy = spyOn(EventHandler, 'off')
instance.dispose()
expect(spy).toHaveBeenCalledWith(element, DummyClass.EVENT_KEY)
})
})
describe('getInstance', () => {
it('should return an instance', () => {
createInstance()
expect(DummyClass.getInstance(element)).toEqual(instance)
expect(DummyClass.getInstance(element)).toBeInstanceOf(DummyClass)
})
it('should accept element, either passed as a CSS selector, jQuery element, or DOM element', () => {
createInstance()
expect(DummyClass.getInstance('#foo')).toEqual(instance)
expect(DummyClass.getInstance(element)).toEqual(instance)
const fakejQueryObject = {
0: element,
jquery: 'foo'
}
expect(DummyClass.getInstance(fakejQueryObject)).toEqual(instance)
})
it('should return null when there is no instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(DummyClass.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return an instance', () => {
createInstance()
expect(DummyClass.getOrCreateInstance(element)).toEqual(instance)
expect(DummyClass.getInstance(element)).toEqual(DummyClass.getOrCreateInstance(element, {}))
expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass)
})
it('should return new instance when there is no alert instance', () => {
fixtureEl.innerHTML = '<div id="foo"></div>'
element = fixtureEl.querySelector('#foo')
expect(DummyClass.getInstance(element)).toBeNull()
expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass)
})
})
})
})

View File

@ -0,0 +1,183 @@
import Button from '../../src/button.js'
import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js'
describe('Button', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<button data-bs-toggle="button">Placeholder</button>'
const buttonEl = fixtureEl.querySelector('[data-bs-toggle="button"]')
const buttonBySelector = new Button('[data-bs-toggle="button"]')
const buttonByElement = new Button(buttonEl)
expect(buttonBySelector._element).toEqual(buttonEl)
expect(buttonByElement._element).toEqual(buttonEl)
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(Button.VERSION).toEqual(jasmine.any(String))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Button.DATA_KEY).toEqual('bs.button')
})
})
describe('data-api', () => {
it('should toggle active class on click', () => {
fixtureEl.innerHTML = [
'<button class="btn" data-bs-toggle="button">btn</button>',
'<button class="btn testParent" data-bs-toggle="button"><div class="test"></div></button>'
].join('')
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
const btnTestParent = fixtureEl.querySelector('.testParent')
expect(btn).not.toHaveClass('active')
btn.click()
expect(btn).toHaveClass('active')
btn.click()
expect(btn).not.toHaveClass('active')
divTest.click()
expect(btnTestParent).toHaveClass('active')
})
})
describe('toggle', () => {
it('should toggle aria-pressed', () => {
fixtureEl.innerHTML = '<button class="btn" data-bs-toggle="button" aria-pressed="false"></button>'
const btnEl = fixtureEl.querySelector('.btn')
const button = new Button(btnEl)
expect(btnEl.getAttribute('aria-pressed')).toEqual('false')
expect(btnEl).not.toHaveClass('active')
button.toggle()
expect(btnEl.getAttribute('aria-pressed')).toEqual('true')
expect(btnEl).toHaveClass('active')
})
})
describe('dispose', () => {
it('should dispose a button', () => {
fixtureEl.innerHTML = '<button class="btn" data-bs-toggle="button"></button>'
const btnEl = fixtureEl.querySelector('.btn')
const button = new Button(btnEl)
expect(Button.getInstance(btnEl)).not.toBeNull()
button.dispose()
expect(Button.getInstance(btnEl)).toBeNull()
})
})
describe('jQueryInterface', () => {
it('should handle config passed and toggle existing button', () => {
fixtureEl.innerHTML = '<button class="btn" data-bs-toggle="button"></button>'
const btnEl = fixtureEl.querySelector('.btn')
const button = new Button(btnEl)
const spy = spyOn(button, 'toggle')
jQueryMock.fn.button = Button.jQueryInterface
jQueryMock.elements = [btnEl]
jQueryMock.fn.button.call(jQueryMock, 'toggle')
expect(spy).toHaveBeenCalled()
})
it('should create new button instance and call toggle', () => {
fixtureEl.innerHTML = '<button class="btn" data-bs-toggle="button"></button>'
const btnEl = fixtureEl.querySelector('.btn')
jQueryMock.fn.button = Button.jQueryInterface
jQueryMock.elements = [btnEl]
jQueryMock.fn.button.call(jQueryMock, 'toggle')
expect(Button.getInstance(btnEl)).not.toBeNull()
expect(btnEl).toHaveClass('active')
})
it('should just create a button instance without calling toggle', () => {
fixtureEl.innerHTML = '<button class="btn" data-bs-toggle="button"></button>'
const btnEl = fixtureEl.querySelector('.btn')
jQueryMock.fn.button = Button.jQueryInterface
jQueryMock.elements = [btnEl]
jQueryMock.fn.button.call(jQueryMock)
expect(Button.getInstance(btnEl)).not.toBeNull()
expect(btnEl).not.toHaveClass('active')
})
})
describe('getInstance', () => {
it('should return button instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const button = new Button(div)
expect(Button.getInstance(div)).toEqual(button)
expect(Button.getInstance(div)).toBeInstanceOf(Button)
})
it('should return null when there is no button instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Button.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return button instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const button = new Button(div)
expect(Button.getOrCreateInstance(div)).toEqual(button)
expect(Button.getInstance(div)).toEqual(Button.getOrCreateInstance(div, {}))
expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button)
})
it('should return new instance when there is no button instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
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

View File

@ -0,0 +1,104 @@
import Data from '../../../src/dom/data.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
describe('Data', () => {
const TEST_KEY = 'bs.test'
const UNKNOWN_KEY = 'bs.unknown'
const TEST_DATA = {
test: 'bsData'
}
let fixtureEl
let div
beforeAll(() => {
fixtureEl = getFixture()
})
beforeEach(() => {
fixtureEl.innerHTML = '<div></div>'
div = fixtureEl.querySelector('div')
})
afterEach(() => {
Data.remove(div, TEST_KEY)
clearFixture()
})
it('should return null for unknown elements', () => {
const data = { ...TEST_DATA }
Data.set(div, TEST_KEY, data)
expect(Data.get(null)).toBeNull()
expect(Data.get(undefined)).toBeNull()
expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
})
it('should return null for unknown keys', () => {
const data = { ...TEST_DATA }
Data.set(div, TEST_KEY, data)
expect(Data.get(div, null)).toBeNull()
expect(Data.get(div, undefined)).toBeNull()
expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
})
it('should store data for an element with a given key and return it', () => {
const data = { ...TEST_DATA }
Data.set(div, TEST_KEY, data)
expect(Data.get(div, TEST_KEY)).toEqual(data)
})
it('should overwrite data if something is already stored', () => {
const data = { ...TEST_DATA }
const copy = { ...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 has nothing stored', () => {
Data.remove(div, TEST_KEY)
expect().nothing()
})
it('should remove nothing for an unknown key', () => {
const data = { ...TEST_DATA }
Data.set(div, TEST_KEY, data)
Data.remove(div, UNKNOWN_KEY)
expect(Data.get(div, TEST_KEY)).toEqual(data)
})
it('should remove data for a given key', () => {
const data = { ...TEST_DATA }
Data.set(div, TEST_KEY, data)
Data.remove(div, TEST_KEY)
expect(Data.get(div, TEST_KEY)).toBeNull()
})
it('should console.error a message if called with multiple keys', () => {
console.error = jasmine.createSpy('console.error')
const data = { ...TEST_DATA }
const copy = { ...data }
Data.set(div, TEST_KEY, data)
Data.set(div, UNKNOWN_KEY, copy)
expect(console.error).toHaveBeenCalled()
expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
})
})

View File

@ -0,0 +1,480 @@
import EventHandler from '../../../src/dom/event-handler.js'
import { noop } from '../../../src/util/index.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
describe('EventHandler', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('on', () => {
it('should not add event listener if the event is not a string', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.on(div, null, noop)
EventHandler.on(null, 'click', noop)
expect().nothing()
})
it('should add event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'click', () => {
expect().nothing()
resolve()
})
div.click()
})
})
it('should add namespaced event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'bs.namespace', () => {
expect().nothing()
resolve()
})
EventHandler.trigger(div, 'bs.namespace')
})
})
it('should add native namespaced event listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.on(div, 'click.namespace', () => {
expect().nothing()
resolve()
})
EventHandler.trigger(div, 'click')
})
})
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()
})
})
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 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(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
}))
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()).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', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
}
}
EventHandler.one(div, 'bootstrap', obj.oneListener)
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
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++
}
}
EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
})
describe('off', () => {
it('should not remove a listener', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
EventHandler.off(div, null, noop)
EventHandler.off(null, 'click', noop)
expect().nothing()
})
it('should remove a listener', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
const handler = () => {
called++
}
EventHandler.on(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
EventHandler.off(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect(called).toEqual(1)
resolve()
}, 20)
})
})
it('should remove all the events', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
let called = 0
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)
})
})
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
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)
})
})
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
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)
})
})
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
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)
})
})
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
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)
})
})
it('should remove a listener registered by .one', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const handler = () => {
reject(new Error('called'))
}
EventHandler.one(div, 'foobar', handler)
EventHandler.off(div, 'foobar', handler)
EventHandler.trigger(div, 'foobar')
setTimeout(() => {
expect().nothing()
resolve()
}, 20)
})
})
it('should remove the correct delegated event listener', () => {
const element = document.createElement('div')
const subelement = document.createElement('span')
element.append(subelement)
const anchor = document.createElement('a')
element.append(anchor)
let i = 0
const handler = () => {
i++
}
EventHandler.on(element, 'click', 'a', handler)
EventHandler.on(element, 'click', 'span', handler)
fixtureEl.append(element)
EventHandler.trigger(anchor, 'click')
EventHandler.trigger(subelement, 'click')
// first listeners called
expect(i).toEqual(2)
EventHandler.off(element, 'click', 'span', handler)
EventHandler.trigger(subelement, 'click')
// removed listener not called
expect(i).toEqual(2)
EventHandler.trigger(anchor, 'click')
// not removed listener called
expect(i).toEqual(3)
EventHandler.on(element, 'click', 'span', handler)
EventHandler.trigger(anchor, 'click')
EventHandler.trigger(subelement, 'click')
// listener re-registered
expect(i).toEqual(5)
EventHandler.off(element, 'click', 'span')
EventHandler.trigger(subelement, 'click')
// listener removed again
expect(i).toEqual(5)
})
})
describe('general functionality', () => {
it('should hydrate properties, and make them configurable', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="div1">',
' <div id="div2"></div>',
' <div id="div3"></div>',
'</div>'
].join('')
const div1 = fixtureEl.querySelector('#div1')
const div2 = fixtureEl.querySelector('#div2')
EventHandler.on(div1, 'click', event => {
expect(event.currentTarget).toBe(div2)
expect(event.delegateTarget).toBe(div1)
expect(event.originalTarget).toBeNull()
Object.defineProperty(event, 'currentTarget', {
configurable: true,
get() {
return div1
}
})
expect(event.currentTarget).toBe(div1)
resolve()
})
expect(() => {
EventHandler.trigger(div1, 'click', { originalTarget: null, currentTarget: div2 })
}).not.toThrowError(TypeError)
})
})
})
})

View File

@ -0,0 +1,135 @@
import Manipulator from '../../../src/dom/manipulator.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
describe('Manipulator', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('setDataAttribute', () => {
it('should set data attribute prefixed with bs', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
Manipulator.setDataAttribute(div, 'key', 'value')
expect(div.getAttribute('data-bs-key')).toEqual('value')
})
it('should set data attribute in kebab case', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
Manipulator.setDataAttribute(div, 'testKey', 'value')
expect(div.getAttribute('data-bs-test-key')).toEqual('value')
})
})
describe('removeDataAttribute', () => {
it('should only remove bs-prefixed data attribute', () => {
fixtureEl.innerHTML = '<div data-bs-key="value" data-key-bs="postfixed" data-key="value"></div>'
const div = fixtureEl.querySelector('div')
Manipulator.removeDataAttribute(div, 'key')
expect(div.getAttribute('data-bs-key')).toBeNull()
expect(div.getAttribute('data-key-bs')).toEqual('postfixed')
expect(div.getAttribute('data-key')).toEqual('value')
})
it('should remove data attribute in kebab case', () => {
fixtureEl.innerHTML = '<div data-bs-test-key="value"></div>'
const div = fixtureEl.querySelector('div')
Manipulator.removeDataAttribute(div, 'testKey')
expect(div.getAttribute('data-bs-test-key')).toBeNull()
})
})
describe('getDataAttributes', () => {
it('should return an empty object for null', () => {
expect(Manipulator.getDataAttributes(null)).toEqual({})
expect().nothing()
})
it('should get only bs-prefixed data attributes without bs namespace', () => {
fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-another="value" data-target-bs="#element" data-in-bs-out="in-between"></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttributes(div)).toEqual({
toggle: 'tabs',
target: '#element'
})
})
it('should omit `bs-config` data attribute', () => {
fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-bs-config=\'{"testBool":false}\'></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttributes(div)).toEqual({
toggle: 'tabs',
target: '#element'
})
})
})
describe('getDataAttribute', () => {
it('should only get bs-prefixed data attribute', () => {
fixtureEl.innerHTML = '<div data-bs-key="value" data-test-bs="postFixed" data-toggle="tab"></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttribute(div, 'key')).toEqual('value')
expect(Manipulator.getDataAttribute(div, 'test')).toBeNull()
expect(Manipulator.getDataAttribute(div, 'toggle')).toBeNull()
})
it('should get data attribute in kebab case', () => {
fixtureEl.innerHTML = '<div data-bs-test-key="value" ></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttribute(div, 'testKey')).toEqual('value')
})
it('should normalize data', () => {
fixtureEl.innerHTML = '<div data-bs-test="false" ></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttribute(div, 'test')).toBeFalse()
div.setAttribute('data-bs-test', 'true')
expect(Manipulator.getDataAttribute(div, 'test')).toBeTrue()
div.setAttribute('data-bs-test', '1')
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1)
})
it('should normalize json data', () => {
fixtureEl.innerHTML = '<div data-bs-test=\'{"delay":{"show":100,"hide":10}}\'></div>'
const div = fixtureEl.querySelector('div')
expect(Manipulator.getDataAttribute(div, 'test')).toEqual({ delay: { show: 100, hide: 10 } })
const objectData = { 'Super Hero': ['Iron Man', 'Super Man'], testNum: 90, url: 'http://localhost:8080/test?foo=bar' }
const dataStr = JSON.stringify(objectData)
div.setAttribute('data-bs-test', encodeURIComponent(dataStr))
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
div.setAttribute('data-bs-test', dataStr)
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
})
})
})

View File

@ -0,0 +1,414 @@
import SelectorEngine from '../../../src/dom/selector-engine.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
describe('SelectorEngine', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('find', () => {
it('should find elements', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(SelectorEngine.find('div', fixtureEl)).toEqual([div])
})
it('should find elements globally', () => {
fixtureEl.innerHTML = '<div id="test"></div>'
const div = fixtureEl.querySelector('#test')
expect(SelectorEngine.find('#test')).toEqual([div])
})
it('should handle :scope selectors', () => {
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')
expect(SelectorEngine.find(':scope > li > .active', listEl)).toEqual([aActive])
})
})
describe('findOne', () => {
it('should return one element', () => {
fixtureEl.innerHTML = '<div id="test"></div>'
const div = fixtureEl.querySelector('#test')
expect(SelectorEngine.findOne('#test')).toEqual(div)
})
})
describe('children', () => {
it('should find children', () => {
fixtureEl.innerHTML = [
'<ul>',
' <li></li>',
' <li></li>',
' <li></li>',
'</ul>'
].join('')
const list = fixtureEl.querySelector('ul')
const liList = [].concat(...fixtureEl.querySelectorAll('li'))
const result = SelectorEngine.children(list, 'li')
expect(result).toEqual(liList)
})
})
describe('parents', () => {
it('should return parents', () => {
expect(SelectorEngine.parents(fixtureEl, 'body')).toHaveSize(1)
})
})
describe('prev', () => {
it('should return previous element', () => {
fixtureEl.innerHTML = '<div class="test"></div><button class="btn"></button>'
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
})
it('should return previous element with an extra element between', () => {
fixtureEl.innerHTML = [
'<div class="test"></div>',
'<span></span>',
'<button class="btn"></button>'
].join('')
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
})
it('should return previous element with comments or text nodes between', () => {
fixtureEl.innerHTML = [
'<div class="test"></div>',
'<div class="test"></div>',
'<!-- Comment-->',
'Text',
'<button class="btn"></button>'
].join('')
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelectorAll('.test')[1]
expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest])
})
})
describe('next', () => {
it('should return next element', () => {
fixtureEl.innerHTML = '<div class="test"></div><button class="btn"></button>'
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
})
it('should return next element with an extra element between', () => {
fixtureEl.innerHTML = [
'<div class="test"></div>',
'<span></span>',
'<button class="btn"></button>'
].join('')
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
})
it('should return next element with comments or text nodes between', () => {
fixtureEl.innerHTML = [
'<div class="test"></div>',
'<!-- Comment-->',
'Text',
'<button class="btn"></button>',
'<button class="btn"></button>'
].join('')
const btn = fixtureEl.querySelector('.btn')
const divTest = fixtureEl.querySelector('.test')
expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
})
})
describe('focusableChildren', () => {
it('should return only elements with specific tag names', () => {
fixtureEl.innerHTML = [
'<div>lorem</div>',
'<span>lorem</span>',
'<a>lorem</a>',
'<button>lorem</button>',
'<input>',
'<textarea></textarea>',
'<select></select>',
'<details>lorem</details>'
].join('')
const expectedElements = [
fixtureEl.querySelector('a'),
fixtureEl.querySelector('button'),
fixtureEl.querySelector('input'),
fixtureEl.querySelector('textarea'),
fixtureEl.querySelector('select'),
fixtureEl.querySelector('details')
]
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
it('should return any element with non negative tab index', () => {
fixtureEl.innerHTML = [
'<div tabindex>lorem</div>',
'<div tabindex="0">lorem</div>',
'<div tabindex="10">lorem</div>'
].join('')
const expectedElements = [
fixtureEl.querySelector('[tabindex]'),
fixtureEl.querySelector('[tabindex="0"]'),
fixtureEl.querySelector('[tabindex="10"]')
]
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
it('should return not return elements with negative tab index', () => {
fixtureEl.innerHTML = '<button tabindex="-1">lorem</button>'
const expectedElements = []
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
it('should return contenteditable elements', () => {
fixtureEl.innerHTML = '<div contenteditable="true">lorem</div>'
const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')]
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
it('should not return disabled elements', () => {
fixtureEl.innerHTML = '<button disabled="true">lorem</button>'
const expectedElements = []
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
it('should not return invisible elements', () => {
fixtureEl.innerHTML = '<button style="display:none;">lorem</button>'
const expectedElements = []
expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
})
})
describe('getSelectorFromElement', () => {
it('should get selector from data-bs-target', () => {
fixtureEl.innerHTML = [
'<div id="test" data-bs-target=".target"></div>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
})
it('should get selector from href if no data-bs-target set', () => {
fixtureEl.innerHTML = [
'<a id="test" href=".target"></a>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
})
it('should get selector from href if data-bs-target equal to #', () => {
fixtureEl.innerHTML = [
'<a id="test" data-bs-target="#" href=".target"></a>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
})
it('should return null if a selector from a href is a url without an anchor', () => {
fixtureEl.innerHTML = [
'<a id="test" data-bs-target="#" href="foo/bar.html"></a>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
})
it('should return the anchor if a selector from a href is a url', () => {
fixtureEl.innerHTML = [
'<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>',
'<div id="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('#target')
})
it('should return null if selector not found', () => {
fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
})
it('should return null if no selector', () => {
fixtureEl.innerHTML = '<div></div>'
const testEl = fixtureEl.querySelector('div')
expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
})
})
describe('getElementFromSelector', () => {
it('should get element from data-bs-target', () => {
fixtureEl.innerHTML = [
'<div id="test" data-bs-target=".target"></div>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
})
it('should get element from href if no data-bs-target set', () => {
fixtureEl.innerHTML = [
'<a id="test" href=".target"></a>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
})
it('should return null if element not found', () => {
fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull()
})
it('should return null if no selector', () => {
fixtureEl.innerHTML = '<div></div>'
const testEl = fixtureEl.querySelector('div')
expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull()
})
})
describe('getMultipleElementsFromSelector', () => {
it('should get elements from data-bs-target', () => {
fixtureEl.innerHTML = [
'<div id="test" data-bs-target=".target"></div>',
'<div class="target"></div>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
})
it('should get elements if several ids are given', () => {
fixtureEl.innerHTML = [
'<div id="test" data-bs-target="#target1,#target2"></div>',
'<div class="target" id="target1"></div>',
'<div class="target" id="target2"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
})
it('should get elements if several ids with special chars are given', () => {
fixtureEl.innerHTML = [
'<div id="test" data-bs-target="#j_id11:exampleModal,#j_id22:exampleModal"></div>',
'<div class="target" id="j_id11:exampleModal"></div>',
'<div class="target" id="j_id22:exampleModal"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
})
it('should get elements in array, from href if no data-bs-target set', () => {
fixtureEl.innerHTML = [
'<a id="test" href=".target"></a>',
'<div class="target"></div>',
'<div class="target"></div>'
].join('')
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
})
it('should return empty array if elements not found', () => {
fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
const testEl = fixtureEl.querySelector('#test')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0)
})
it('should return empty array if no selector', () => {
fixtureEl.innerHTML = '<div></div>'
const testEl = fixtureEl.querySelector('div')
expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
/* eslint-env jquery */
import Alert from '../../src/alert.js'
import Button from '../../src/button.js'
import Carousel from '../../src/carousel.js'
import Collapse from '../../src/collapse.js'
import Dropdown from '../../src/dropdown.js'
import Modal from '../../src/modal.js'
import Offcanvas from '../../src/offcanvas.js'
import Popover from '../../src/popover.js'
import ScrollSpy from '../../src/scrollspy.js'
import Tab from '../../src/tab.js'
import Toast from '../../src/toast.js'
import Tooltip from '../../src/tooltip.js'
import { clearFixture, getFixture } from '../helpers/fixture.js'
describe('jQuery', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
it('should add all plugins in jQuery', () => {
expect(Alert.jQueryInterface).toEqual(jQuery.fn.alert)
expect(Button.jQueryInterface).toEqual(jQuery.fn.button)
expect(Carousel.jQueryInterface).toEqual(jQuery.fn.carousel)
expect(Collapse.jQueryInterface).toEqual(jQuery.fn.collapse)
expect(Dropdown.jQueryInterface).toEqual(jQuery.fn.dropdown)
expect(Modal.jQueryInterface).toEqual(jQuery.fn.modal)
expect(Offcanvas.jQueryInterface).toEqual(jQuery.fn.offcanvas)
expect(Popover.jQueryInterface).toEqual(jQuery.fn.popover)
expect(ScrollSpy.jQueryInterface).toEqual(jQuery.fn.scrollspy)
expect(Tab.jQueryInterface).toEqual(jQuery.fn.tab)
expect(Toast.jQueryInterface).toEqual(jQuery.fn.toast)
expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip)
})
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')).toHaveSize(0)
resolve()
})
$(fixtureEl).find('button').trigger('click')
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,914 @@
import EventHandler from '../../src/dom/event-handler.js'
import Offcanvas from '../../src/offcanvas.js'
import { isVisible } from '../../src/util/index.js'
import ScrollBarHelper from '../../src/util/scrollbar.js'
import {
clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock
} from '../helpers/fixture.js'
describe('Offcanvas', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
document.body.classList.remove('offcanvas-open')
clearBodyAndDocument()
})
beforeEach(() => {
clearBodyAndDocument()
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(Offcanvas.VERSION).toEqual(jasmine.any(String))
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(Offcanvas.Default).toEqual(jasmine.any(Object))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Offcanvas.DATA_KEY).toEqual('bs.offcanvas')
})
})
describe('constructor', () => {
it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => {
fixtureEl.innerHTML = [
'<div class="offcanvas">',
' <a href="#" data-bs-dismiss="offcanvas">Close</a>',
'</div>'
].join('')
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const closeEl = fixtureEl.querySelector('a')
const offCanvas = new Offcanvas(offCanvasEl)
const spy = spyOn(offCanvas, 'hide')
closeEl.click()
expect(offCanvas._config.keyboard).toBeTrue()
expect(spy).toHaveBeenCalled()
})
it('should hide if esc is pressed', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
const keyDownEsc = createEvent('keydown')
keyDownEsc.key = 'Escape'
const spy = spyOn(offCanvas, 'hide')
offCanvasEl.dispatchEvent(keyDownEsc)
expect(spy).toHaveBeenCalled()
})
it('should hide if esc is pressed and backdrop is static', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' })
const keyDownEsc = createEvent('keydown')
keyDownEsc.key = 'Escape'
const spy = spyOn(offCanvas, 'hide')
offCanvasEl.dispatchEvent(keyDownEsc)
expect(spy).toHaveBeenCalled()
})
it('should not hide if esc is not pressed', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
const keydownTab = createEvent('keydown')
keydownTab.key = 'Tab'
const spy = spyOn(offCanvas, 'hide')
offCanvasEl.dispatchEvent(keydownTab)
expect(spy).not.toHaveBeenCalled()
})
it('should not hide if esc is pressed but with keyboard = false', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false })
const keyDownEsc = createEvent('keydown')
keyDownEsc.key = 'Escape'
const spy = spyOn(offCanvas, 'hide')
const hidePreventedSpy = jasmine.createSpy('hidePrevented')
offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy)
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._config.keyboard).toBeFalse()
offCanvasEl.dispatchEvent(keyDownEsc)
expect(hidePreventedSpy).toHaveBeenCalled()
expect(spy).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
it('should not hide if user clicks on static backdrop', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' })
const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true })
const spyClick = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough()
const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough()
const hidePreventedSpy = jasmine.createSpy('hidePrevented')
offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy)
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spyClick).toEqual(jasmine.any(Function))
offCanvas._backdrop._getElement().dispatchEvent(clickEvent)
expect(hidePreventedSpy).toHaveBeenCalled()
expect(spyHide).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
it('should call `hide` on resize, if element\'s position is not fixed any more', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas-lg"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
const spy = spyOn(offCanvas, 'hide').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
const resizeEvent = createEvent('resize')
offCanvasEl.style.removeProperty('position')
window.dispatchEvent(resizeEvent)
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
})
describe('config', () => {
it('should have default values', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
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>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
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>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, {
backdrop: true,
keyboard: true,
scroll: 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', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: true })
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spyHide).not.toHaveBeenCalled()
offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(spyReset).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: false })
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spyHide).toHaveBeenCalled()
offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(spyReset).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
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 clickEvent = new Event('mousedown', { bubbles: true, cancelable: true })
const spy = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function))
offCanvas._backdrop._getElement().dispatchEvent(clickEvent)
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
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,
backdrop: false
})
const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spy).not.toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
it('should trap focus if scroll is allowed OR backdrop is enabled', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, {
scroll: true,
backdrop: true
})
const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
})
describe('toggle', () => {
it('should call show method if show class is not present', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
const spy = spyOn(offCanvas, 'show')
offCanvas.toggle()
expect(spy).toHaveBeenCalled()
})
it('should call hide method if show class is present', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
const spy = spyOn(offCanvas, 'hide')
offCanvas.toggle()
expect(spy).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>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()
expect(offCanvasEl).toHaveClass('show')
const spyShow = spyOn(offCanvas._backdrop, 'show').and.callThrough()
const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough()
offCanvas.show()
expect(spyTrigger).not.toHaveBeenCalled()
expect(spyShow).not.toHaveBeenCalled()
})
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)
const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.show()
})
})
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)
const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough()
const expectEnd = () => {
setTimeout(() => {
expect(spy).not.toHaveBeenCalled()
resolve()
}, 10)
}
offCanvasEl.addEventListener('show.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
reject(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"', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const spy = spyOn(Offcanvas.prototype, 'show').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
resolve()
})
window.dispatchEvent(createEvent('load'))
const instance = Offcanvas.getInstance(offCanvasEl)
expect(instance).not.toBeNull()
expect(spy).toHaveBeenCalled()
})
})
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 spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
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>'
const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough()
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.hide()
expect(spyHide).not.toHaveBeenCalled()
expect(spyTrigger).not.toHaveBeenCalled()
})
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)
const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.show()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl).not.toHaveClass('show')
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.hide()
})
})
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)
const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough()
offCanvas.show()
const expectEnd = () => {
setTimeout(() => {
expect(spy).not.toHaveBeenCalled()
resolve()
}, 10)
}
offCanvasEl.addEventListener('hide.bs.offcanvas', event => {
event.preventDefault()
expectEnd()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
reject(new Error('should not fire hidden event'))
})
offCanvas.hide()
})
})
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)
const spy = spyOn(offCanvas._focustrap, 'deactivate').and.callThrough()
offCanvas.show()
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
offCanvas.hide()
})
})
})
describe('dispose', () => {
it('should dispose an offcanvas', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
const backdrop = offCanvas._backdrop
const spyDispose = spyOn(backdrop, 'dispose').and.callThrough()
const focustrap = offCanvas._focustrap
const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough()
expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas)
offCanvas.dispose()
expect(spyDispose).toHaveBeenCalled()
expect(offCanvas._backdrop).toBeNull()
expect(spyDeactivate).toHaveBeenCalled()
expect(offCanvas._focustrap).toBeNull()
expect(Offcanvas.getInstance(offCanvasEl)).toBeNull()
})
})
describe('data-api', () => {
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')
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvasEl).toHaveClass('show')
expect(target.checked).toBeTrue()
resolve()
})
target.click()
})
})
it('should not call toggle on disabled elements', () => {
fixtureEl.innerHTML = [
'<a href="#" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" class="disabled"></a>',
'<div id="offcanvasdiv1" class="offcanvas"></div>'
].join('')
const target = fixtureEl.querySelector('a')
const spy = spyOn(Offcanvas.prototype, 'toggle')
target.click()
expect(spy).not.toHaveBeenCalled()
})
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)
offcanvasEl1.addEventListener('shown.bs.offcanvas', () => {
trigger2.click()
})
offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => {
expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull()
resolve()
})
offcanvas1.show()
})
})
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)
const spy = spyOn(trigger, 'focus')
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
offcanvas.hide()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(spy).toHaveBeenCalled()
resolve()
}, 5)
})
trigger.click()
})
})
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)
const spy = spyOn(trigger, 'focus')
offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
trigger.style.display = 'none'
offcanvas.hide()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(isVisible(trigger)).toBeFalse()
expect(spy).not.toHaveBeenCalled()
resolve()
}, 5)
})
trigger.click()
})
})
})
describe('jQueryInterface', () => {
it('should create an offcanvas', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock)
expect(Offcanvas.getInstance(div)).not.toBeNull()
})
it('should not re create an offcanvas', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(div)
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock)
expect(Offcanvas.getInstance(div)).toEqual(offCanvas)
})
it('should throw error on undefined method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = 'undefinedMethod'
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.offcanvas.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should throw error on protected method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = '_getConfig'
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.offcanvas.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should throw error if method "constructor" is being called', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = 'constructor'
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.offcanvas.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should call offcanvas method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const spy = spyOn(Offcanvas.prototype, 'show')
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock, 'show')
expect(spy).toHaveBeenCalled()
})
it('should create a offcanvas with given config', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.offcanvas.call(jQueryMock, { scroll: true })
const offcanvas = Offcanvas.getInstance(div)
expect(offcanvas).not.toBeNull()
expect(offcanvas._config.scroll).toBeTrue()
})
})
describe('getInstance', () => {
it('should return offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(div)
expect(Offcanvas.getInstance(div)).toEqual(offCanvas)
expect(Offcanvas.getInstance(div)).toBeInstanceOf(Offcanvas)
})
it('should return null when there is no offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Offcanvas.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offcanvas = new Offcanvas(div)
expect(Offcanvas.getOrCreateInstance(div)).toEqual(offcanvas)
expect(Offcanvas.getInstance(div)).toEqual(Offcanvas.getOrCreateInstance(div, {}))
expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas)
})
it('should return new instance when there is no Offcanvas instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Offcanvas.getInstance(div)).toBeNull()
expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas)
})
it('should return new instance when there is no offcanvas instance with given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Offcanvas.getInstance(div)).toBeNull()
const offcanvas = Offcanvas.getOrCreateInstance(div, {
scroll: true
})
expect(offcanvas).toBeInstanceOf(Offcanvas)
expect(offcanvas._config.scroll).toBeTrue()
})
it('should return the instance when exists without given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const offcanvas = new Offcanvas(div, {
scroll: true
})
expect(Offcanvas.getInstance(div)).toEqual(offcanvas)
const offcanvas2 = Offcanvas.getOrCreateInstance(div, {
scroll: false
})
expect(offcanvas).toBeInstanceOf(Offcanvas)
expect(offcanvas2).toEqual(offcanvas)
expect(offcanvas2._config.scroll).toBeTrue()
})
})
})

View File

@ -0,0 +1,413 @@
import EventHandler from '../../src/dom/event-handler.js'
import Popover from '../../src/popover.js'
import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js'
describe('Popover', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
const popoverList = document.querySelectorAll('.popover')
for (const popoverEl of popoverList) {
popoverEl.remove()
}
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(Popover.VERSION).toEqual(jasmine.any(String))
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(Popover.Default).toEqual(jasmine.any(Object))
})
})
describe('NAME', () => {
it('should return plugin name', () => {
expect(Popover.NAME).toEqual(jasmine.any(String))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Popover.DATA_KEY).toEqual('bs.popover')
})
})
describe('EVENT_KEY', () => {
it('should return plugin event key', () => {
expect(Popover.EVENT_KEY).toEqual('.bs.popover')
})
})
describe('DefaultType', () => {
it('should return plugin default type', () => {
expect(Popover.DefaultType).toEqual(jasmine.any(Object))
})
})
describe('show', () => {
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)
popoverEl.addEventListener('shown.bs.popover', () => {
expect(document.querySelector('.popover')).not.toBeNull()
resolve()
})
popover.show()
})
})
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 })
const spy = spyOn(EventHandler, 'trigger').and.callThrough()
popover.show()
expect(spy).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)
popover.setContent({ '.tooltip-inner': 'foo' })
const tip = popover._getTipElement()
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', () => {
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)
popoverEl.addEventListener('shown.bs.popover', () => {
popover.hide()
})
popoverEl.addEventListener('hidden.bs.popover', () => {
expect(document.querySelector('.popover')).toBeNull()
resolve()
})
popover.show()
})
})
})
describe('jQueryInterface', () => {
it('should create a popover', () => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
jQueryMock.fn.popover.call(jQueryMock)
expect(Popover.getInstance(popoverEl)).not.toBeNull()
})
it('should create a popover with a config object', () => {
fixtureEl.innerHTML = '<a href="#" title="Popover">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
jQueryMock.fn.popover.call(jQueryMock, {
content: 'Popover content'
})
expect(Popover.getInstance(popoverEl)).not.toBeNull()
})
it('should not re create a popover', () => {
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)
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
jQueryMock.fn.popover.call(jQueryMock)
expect(Popover.getInstance(popoverEl)).toEqual(popover)
})
it('should throw error on undefined method', () => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
const action = 'undefinedMethod'
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
expect(() => {
jQueryMock.fn.popover.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should should call show method', () => {
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)
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
const spy = spyOn(popover, 'show')
jQueryMock.fn.popover.call(jQueryMock, 'show')
expect(spy).toHaveBeenCalled()
})
})
describe('getInstance', () => {
it('should return popover instance', () => {
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)
expect(Popover.getInstance(popoverEl)).toEqual(popover)
expect(Popover.getInstance(popoverEl)).toBeInstanceOf(Popover)
})
it('should return null when there is no popover instance', () => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>'
const popoverEl = fixtureEl.querySelector('a')
expect(Popover.getInstance(popoverEl)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return popover instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const popover = new Popover(div)
expect(Popover.getOrCreateInstance(div)).toEqual(popover)
expect(Popover.getInstance(div)).toEqual(Popover.getOrCreateInstance(div, {}))
expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover)
})
it('should return new instance when there is no popover instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Popover.getInstance(div)).toBeNull()
expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover)
})
it('should return new instance when there is no popover instance with given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Popover.getInstance(div)).toBeNull()
const popover = Popover.getOrCreateInstance(div, {
placement: 'top'
})
expect(popover).toBeInstanceOf(Popover)
expect(popover._config.placement).toEqual('top')
})
it('should return the instance when exists without given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const popover = new Popover(div, {
placement: 'top'
})
expect(Popover.getInstance(div)).toEqual(popover)
const popover2 = Popover.getOrCreateInstance(div, {
placement: 'bottom'
})
expect(popover).toBeInstanceOf(Popover)
expect(popover2).toEqual(popover)
expect(popover2._config.placement).toEqual('top')
})
})
})

View File

@ -0,0 +1,980 @@
import EventHandler from '../../src/dom/event-handler.js'
import ScrollSpy from '../../src/scrollspy.js'
import {
clearFixture, createEvent, getFixture, jQueryMock
} from '../helpers/fixture.js'
describe('ScrollSpy', () => {
let fixtureEl
const getElementScrollSpy = element => element.scrollTo ?
spyOn(element, 'scrollTo').and.callThrough() :
spyOnProperty(element, 'scrollTop', 'set').and.callThrough()
const scrollTo = (el, height) => {
el.scrollTop = height
}
const onScrollStop = (callback, element, timeout = 30) => {
let handle = null
const onScroll = function () {
if (handle) {
window.clearTimeout(handle)
}
handle = setTimeout(() => {
element.removeEventListener('scroll', onScroll)
callback()
}, timeout + 1)
}
element.addEventListener('scroll', onScroll)
}
const getDummyFixture = () => {
return [
'<nav id="navBar" class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>',
' </ul>',
'</nav>',
'<div class="content" data-bs-target="#navBar" style="overflow-y: auto">',
' <div id="div-jsm-1">div 1</div>',
'</div>'
].join('')
}
const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, cb }) => {
const element = fixtureEl.querySelector(elementSelector)
const target = fixtureEl.querySelector(targetSelector)
// add top padding to fix Chrome on Android failures
const paddingTop = 0
const parentOffset = getComputedStyle(contentEl).getPropertyValue('position') === 'relative' ? 0 : contentEl.offsetTop
const scrollHeight = (target.offsetTop - parentOffset) + paddingTop
contentEl.addEventListener('activate.bs.scrollspy', event => {
if (scrollSpy._activeTarget !== element) {
return
}
expect(element).toHaveClass('active')
expect(scrollSpy._activeTarget).toEqual(element)
expect(event.relatedTarget).toEqual(element)
cb()
})
setTimeout(() => { // in case we scroll something before the test
scrollTo(contentEl, scrollHeight)
}, 100)
}
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(ScrollSpy.VERSION).toEqual(jasmine.any(String))
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(ScrollSpy.Default).toEqual(jasmine.any(Object))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(ScrollSpy.DATA_KEY).toEqual('bs.scrollspy')
})
})
describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = getDummyFixture()
const sSpyEl = fixtureEl.querySelector('.content')
const sSpyBySelector = new ScrollSpy('.content')
const sSpyByElement = new ScrollSpy(sSpyEl)
expect(sSpyBySelector._element).toEqual(sSpyEl)
expect(sSpyByElement._element).toEqual(sSpyEl)
})
it('should null, if element is not scrollable', () => {
fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">' +
' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">One</a></li>' +
' </ul>',
'</nav>',
'<div id="content">',
' <div id="1" style="height: 300px;">test</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
target: '#navigation'
})
expect(scrollSpy._observer.root).toBeNull()
expect(scrollSpy._rootElement).toBeNull()
})
it('should respect threshold option', () => {
fixtureEl.innerHTML = [
'<ul id="navigation" class="navbar">',
' <a class="nav-link active" id="one-link" href="#">One</a>' +
'</ul>',
'<div id="content">',
' <div id="one-link">test</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy('#content', {
target: '#navigation',
threshold: [1]
})
expect(scrollSpy._observer.thresholds).toEqual([1])
})
it('should respect threshold option markup', () => {
fixtureEl.innerHTML = [
'<ul id="navigation" class="navbar">',
' <a class="nav-link active" id="one-link" href="#">One</a>' +
'</ul>',
'<div id="content" data-bs-threshold="0,0.2,1">',
' <div id="one-link">test</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy('#content', {
target: '#navigation'
})
// See https://stackoverflow.com/a/45592926
const expectToBeCloseToArray = (actual, expected) => {
expect(actual.length).toBe(expected.length)
for (const x of actual) {
const i = actual.indexOf(x)
expect(x).withContext(`[${i}]`).toBeCloseTo(expected[i])
}
}
expectToBeCloseToArray(scrollSpy._observer.thresholds, [0, 0.2, 1])
})
it('should not take count to not visible sections', () => {
fixtureEl.innerHTML = [
'<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: 300px;">test</div>',
' <div id="two" hidden style="height: 300px;">test</div>',
' <div id="three" style="display: none;">test</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
target: '#navigation'
})
expect(scrollSpy._observableSections.size).toBe(1)
expect(scrollSpy._targetLinks.size).toBe(1)
})
it('should not process element without target', () => {
fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">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="two" style="height: 300px;">test</div>',
' <div id="three" style="height: 10px;">test2</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
target: '#navigation'
})
expect(scrollSpy._targetLinks).toHaveSize(2)
})
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;" id="masthead">Overview</div>',
' <div style="height: 200px;" id="detail">Detail</div>',
' </div>',
'</div>'
].join('')
const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
const rootEl = fixtureEl.querySelector('#root')
const scrollSpy = new ScrollSpy(scrollSpyEl, {
target: 'ss-target'
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
onScrollStop(() => {
expect(rootEl).toHaveClass('active')
expect(spy).toHaveBeenCalled()
resolve()
}, scrollSpyEl)
scrollTo(scrollSpyEl, 350)
})
})
it('should not process data if `activeTarget` is same as given target', () => {
return new Promise((resolve, reject) => {
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 triggerSpy = spyOn(EventHandler, 'trigger').and.callThrough()
scrollSpy._activeTarget = fixtureEl.querySelector('#a-1')
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
cb: reject
})
setTimeout(() => {
expect(triggerSpy).not.toHaveBeenCalled()
resolve()
}, 100)
})
})
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;" id="masthead">Overview</div>',
' <div style="height: 200px;" id="detail">Detail</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')
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
onScrollStop(() => {
expect(rootEl).toHaveClass('active')
expect(scrollSpy._activeTarget).toEqual(fixtureEl.querySelector('[href="#detail"]'))
expect(spy).toHaveBeenCalled()
resolve()
}, scrollSpyEl)
scrollTo(scrollSpyEl, 350)
})
})
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, {
offset: 0,
target: '.navbar'
})
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
cb: resolve
})
}
})
})
})
it('should add to nav 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 contentEl = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(contentEl, {
offset: 0,
target: '.navbar'
})
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
cb: resolve
})
}
})
})
})
it('should add to list-group, 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'
})
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
cb() {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
cb: resolve
})
}
})
})
})
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: 200px;"></div>',
' <div id="one" style="height: 100px;">text</div>',
' <div id="two" style="height: 100px;">text</div>',
' <div id="three" style="height: 100px;">text</div>',
' <div id="spacer" style="height: 100px;"></div>',
'</div>'
].join('')
const contentEl = fixtureEl.querySelector('#content')
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
offset: contentEl.offsetTop
})
const spy = spyOn(scrollSpy, '_process').and.callThrough()
onScrollStop(() => {
const active = () => fixtureEl.querySelector('.active')
expect(spy).toHaveBeenCalled()
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active().getAttribute('id')).toEqual('two-link')
onScrollStop(() => {
expect(active()).toBeNull()
resolve()
}, contentEl)
scrollTo(contentEl, 0)
}, contentEl)
scrollTo(contentEl, 200)
})
})
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: 150px; overflow-y: auto;">',
' <div id="one" style="height: 100px;">test</div>',
' <div id="two" style="height: 100px;">test</div>',
' <div id="three" style="height: 100px;">test</div>',
' <div id="spacer" style="height: 100px;">test</div>',
'</div>'
].join('')
const negativeHeight = 0
const startOfSectionTwo = 101
const contentEl = fixtureEl.querySelector('#content')
// eslint-disable-next-line no-unused-vars
const scrollSpy = new ScrollSpy(contentEl, {
target: '#navigation',
rootMargin: '0px 0px -50%'
})
onScrollStop(() => {
const activeId = () => fixtureEl.querySelector('.active').getAttribute('id')
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(activeId()).toEqual('two-link')
scrollTo(contentEl, negativeHeight)
onScrollStop(() => {
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(activeId()).toEqual('one-link')
resolve()
}, contentEl)
scrollTo(contentEl, 0)
}, contentEl)
scrollTo(contentEl, 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'
})
scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-5',
targetSelector: '#div-100-5',
contentEl,
scrollSpy,
cb() {
scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-2',
targetSelector: '#div-100-2',
contentEl,
scrollSpy,
cb() {
scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-3',
targetSelector: '#div-100-3',
contentEl,
scrollSpy,
cb() {
scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-2',
targetSelector: '#div-100-2',
contentEl,
scrollSpy,
cb() {
scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-1',
targetSelector: '#div-100-1',
contentEl,
scrollSpy,
cb: resolve
})
}
})
}
})
}
})
}
})
})
})
})
describe('refresh', () => {
it('should disconnect existing observer', () => {
fixtureEl.innerHTML = getDummyFixture()
const el = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(el)
const spy = spyOn(scrollSpy._observer, 'disconnect')
scrollSpy.refresh()
expect(spy).toHaveBeenCalled()
})
})
describe('dispose', () => {
it('should dispose a scrollspy', () => {
fixtureEl.innerHTML = getDummyFixture()
const el = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(el)
expect(ScrollSpy.getInstance(el)).not.toBeNull()
scrollSpy.dispose()
expect(ScrollSpy.getInstance(el)).toBeNull()
})
})
describe('jQueryInterface', () => {
it('should create a scrollspy', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.scrollspy.call(jQueryMock, { target: '#navBar' })
expect(ScrollSpy.getInstance(div)).not.toBeNull()
})
it('should create a scrollspy with given config', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.scrollspy.call(jQueryMock, { rootMargin: '100px' })
const spy = spyOn(ScrollSpy.prototype, 'constructor')
expect(spy).not.toHaveBeenCalledWith(div, { rootMargin: '100px' })
const scrollspy = ScrollSpy.getInstance(div)
expect(scrollspy).not.toBeNull()
expect(scrollspy._config.rootMargin).toEqual('100px')
})
it('should not re create a scrollspy', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(div)
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.scrollspy.call(jQueryMock)
expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
})
it('should call a scrollspy method', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(div)
const spy = spyOn(scrollSpy, 'refresh')
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh')
expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
expect(spy).toHaveBeenCalled()
})
it('should throw error on undefined method', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const action = 'undefinedMethod'
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.scrollspy.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should throw error on protected method', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const action = '_getConfig'
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.scrollspy.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
it('should throw error if method "constructor" is being called', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const action = 'constructor'
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.scrollspy.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
})
describe('getInstance', () => {
it('should return scrollspy instance', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const scrollSpy = new ScrollSpy(div, { target: fixtureEl.querySelector('#navBar') })
expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy)
})
it('should return null if there is no instance', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
expect(ScrollSpy.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return scrollspy instance', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const scrollspy = new ScrollSpy(div)
expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy)
expect(ScrollSpy.getInstance(div)).toEqual(ScrollSpy.getOrCreateInstance(div, {}))
expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy)
})
it('should return new instance when there is no scrollspy instance', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
expect(ScrollSpy.getInstance(div)).toBeNull()
expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy)
})
it('should return new instance when there is no scrollspy instance with given configuration', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
expect(ScrollSpy.getInstance(div)).toBeNull()
const scrollspy = ScrollSpy.getOrCreateInstance(div, {
offset: 1
})
expect(scrollspy).toBeInstanceOf(ScrollSpy)
expect(scrollspy._config.offset).toEqual(1)
})
it('should return the instance when exists without given configuration', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const scrollspy = new ScrollSpy(div, {
offset: 1
})
expect(ScrollSpy.getInstance(div)).toEqual(scrollspy)
const scrollspy2 = ScrollSpy.getOrCreateInstance(div, {
offset: 2
})
expect(scrollspy).toBeInstanceOf(ScrollSpy)
expect(scrollspy2).toEqual(scrollspy)
expect(scrollspy2._config.offset).toEqual(1)
})
})
describe('event handler', () => {
it('should create scrollspy on window load event', () => {
fixtureEl.innerHTML = [
'<div id="nav"></div>' +
'<div id="wrapper" data-bs-spy="scroll" data-bs-target="#nav" style="overflow-y: auto"></div>'
].join('')
const scrollSpyEl = fixtureEl.querySelector('#wrapper')
window.dispatchEvent(createEvent('load'))
expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull()
})
})
describe('SmoothScroll', () => {
it('should not enable smoothScroll', () => {
fixtureEl.innerHTML = getDummyFixture()
const offSpy = spyOn(EventHandler, 'off').and.callThrough()
const onSpy = spyOn(EventHandler, 'on').and.callThrough()
const div = fixtureEl.querySelector('.content')
const target = fixtureEl.querySelector('#navBar')
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1
})
expect(offSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy')
expect(onSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy')
})
it('should enable smoothScroll', () => {
fixtureEl.innerHTML = getDummyFixture()
const offSpy = spyOn(EventHandler, 'off').and.callThrough()
const onSpy = spyOn(EventHandler, 'on').and.callThrough()
const div = fixtureEl.querySelector('.content')
const target = fixtureEl.querySelector('#navBar')
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1,
smoothScroll: true
})
expect(offSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy')
expect(onSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy', '[href]', jasmine.any(Function))
})
it('should not smoothScroll to element if it not handles a scrollspy section', () => {
fixtureEl.innerHTML = [
'<nav id="navBar" class="navbar">',
' <ul class="nav">',
' <a id="anchor-1" href="#div-jsm-1">div 1</a></li>',
' <a id="anchor-2" href="#foo">div 2</a></li>',
' </ul>',
'</nav>',
'<div class="content" data-bs-target="#navBar" style="overflow-y: auto">',
' <div id="div-jsm-1">div 1</div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('.content')
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1,
smoothScroll: true
})
const clickSpy = getElementScrollSpy(div)
fixtureEl.querySelector('#anchor-2').click()
expect(clickSpy).not.toHaveBeenCalled()
})
it('should call `scrollTop` if element doesn\'t not support `scrollTo`', () => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const link = fixtureEl.querySelector('[href="#div-jsm-1"]')
delete div.scrollTo
const clickSpy = getElementScrollSpy(div)
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1,
smoothScroll: true
})
link.click()
expect(clickSpy).toHaveBeenCalled()
})
it('should smoothScroll to the proper observable element on anchor click', done => {
fixtureEl.innerHTML = getDummyFixture()
const div = fixtureEl.querySelector('.content')
const link = fixtureEl.querySelector('[href="#div-jsm-1"]')
const observable = fixtureEl.querySelector('#div-jsm-1')
const clickSpy = getElementScrollSpy(div)
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1,
smoothScroll: true
})
setTimeout(() => {
if (div.scrollTo) {
expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' })
} else {
expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop)
}
done()
}, 100)
link.click()
})
it('should smoothscroll to observable with anchor link that contains a french word as id', done => {
fixtureEl.innerHTML = [
'<nav id="navBar" class="navbar">',
' <ul class="nav">',
' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#présentation">div 1</a></li>',
' </ul>',
'</nav>',
'<div class="content" data-bs-target="#navBar" style="overflow-y: auto">',
' <div id="présentation">div 1</div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('.content')
const link = fixtureEl.querySelector('[href="#présentation"]')
const observable = fixtureEl.querySelector('#présentation')
const clickSpy = getElementScrollSpy(div)
// eslint-disable-next-line no-new
new ScrollSpy(div, {
offset: 1,
smoothScroll: true
})
setTimeout(() => {
if (div.scrollTo) {
expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' })
} else {
expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop)
}
done()
}, 100)
link.click()
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,672 @@
import Toast from '../../src/toast.js'
import {
clearFixture, createEvent, getFixture, jQueryMock
} from '../helpers/fixture.js'
describe('Toast', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(Toast.VERSION).toEqual(jasmine.any(String))
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Toast.DATA_KEY).toEqual('bs.toast')
})
})
describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="toast"></div>'
const toastEl = fixtureEl.querySelector('.toast')
const toastBySelector = new Toast('.toast')
const toastByElement = new Toast(toastEl)
expect(toastBySelector._element).toEqual(toastEl)
expect(toastByElement._element).toEqual(toastEl)
})
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
})
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).toHaveClass('show')
resolve()
})
toast.show()
})
})
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)
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).toHaveClass('show')
const button = toastEl.querySelector('.btn-close')
button.click()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
})
})
})
describe('Default', () => {
it('should expose default setting to allow to override them', () => {
const defaultDelay = 1000
Toast.Default.delay = defaultDelay
fixtureEl.innerHTML = [
'<div class="toast" 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)
expect(toast._config.delay).toEqual(defaultDelay)
})
})
describe('DefaultType', () => {
it('should expose default setting types for read', () => {
expect(Toast.DefaultType).toEqual(jasmine.any(Object))
})
})
describe('show', () => {
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)
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
})
})
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)
toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).not.toHaveClass('fade')
resolve()
})
toast.show()
})
})
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 assertDone = () => {
setTimeout(() => {
expect(toastEl).not.toHaveClass('show')
resolve()
}, 20)
}
toastEl.addEventListener('show.bs.toast', event => {
event.preventDefault()
assertDone()
})
toastEl.addEventListener('shown.bs.toast', () => {
reject(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', () => {
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)
setTimeout(() => {
toast._config.autohide = false
toastEl.addEventListener('shown.bs.toast', () => {
expect(spy).toHaveBeenCalled()
expect(toast._timeout).toBeNull()
resolve()
})
toast.show()
}, toast._config.delay / 2)
const spy = 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()
})
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
}, toast._config.delay / 2)
toast.show()
})
})
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)
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
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()
resolve()
})
const mouseOverEvent = createEvent('mouseover')
toastEl.dispatchEvent(mouseOverEvent)
}, toast._config.delay / 2)
toast.show()
})
})
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)
setTimeout(() => {
toastEl.addEventListener('mouseover', () => {
const insideFocusable = toastEl.querySelector('button')
insideFocusable.focus()
})
toastEl.addEventListener('focusin', () => {
const outsideFocusable = document.getElementById('outside-focusable')
outsideFocusable.focus()
})
toastEl.addEventListener('focusout', () => {
expect(toast._timeout).toBeNull()
resolve()
})
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', () => {
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', () => {
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)
toastEl.addEventListener('shown.bs.toast', () => {
toast.hide()
})
toastEl.addEventListener('hidden.bs.toast', () => {
expect(toastEl).not.toHaveClass('show')
resolve()
})
toast.show()
})
})
it('should do nothing when we call hide on a non shown toast', () => {
fixtureEl.innerHTML = '<div></div>'
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
const spy = spyOn(toastEl.classList, 'contains')
toast.hide()
expect(spy).toHaveBeenCalled()
})
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 assertDone = () => {
setTimeout(() => {
expect(toastEl).toHaveClass('show')
resolve()
}, 20)
}
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()
})
})
})
describe('dispose', () => {
it('should allow to destroy toast', () => {
fixtureEl.innerHTML = '<div></div>'
const toastEl = fixtureEl.querySelector('div')
const toast = new Toast(toastEl)
expect(Toast.getInstance(toastEl)).not.toBeNull()
toast.dispose()
expect(Toast.getInstance(toastEl)).toBeNull()
})
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).toHaveClass('show')
expect(Toast.getInstance(toastEl)).not.toBeNull()
toast.dispose()
expect(Toast.getInstance(toastEl)).toBeNull()
expect(toastEl).not.toHaveClass('show')
resolve()
}
toastEl.addEventListener('shown.bs.toast', () => {
setTimeout(expected, 1)
})
toast.show()
})
})
})
describe('jQueryInterface', () => {
it('should create a toast', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
jQueryMock.fn.toast = Toast.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.toast.call(jQueryMock)
expect(Toast.getInstance(div)).not.toBeNull()
})
it('should not re create a toast', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const toast = new Toast(div)
jQueryMock.fn.toast = Toast.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.toast.call(jQueryMock)
expect(Toast.getInstance(div)).toEqual(toast)
})
it('should call a toast method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const toast = new Toast(div)
const spy = spyOn(toast, 'show')
jQueryMock.fn.toast = Toast.jQueryInterface
jQueryMock.elements = [div]
jQueryMock.fn.toast.call(jQueryMock, 'show')
expect(Toast.getInstance(div)).toEqual(toast)
expect(spy).toHaveBeenCalled()
})
it('should throw error on undefined method', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const action = 'undefinedMethod'
jQueryMock.fn.toast = Toast.jQueryInterface
jQueryMock.elements = [div]
expect(() => {
jQueryMock.fn.toast.call(jQueryMock, action)
}).toThrowError(TypeError, `No method named "${action}"`)
})
})
describe('getInstance', () => {
it('should return a toast instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const toast = new Toast(div)
expect(Toast.getInstance(div)).toEqual(toast)
expect(Toast.getInstance(div)).toBeInstanceOf(Toast)
})
it('should return null when there is no toast instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toBeNull()
})
})
describe('getOrCreateInstance', () => {
it('should return toast instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const toast = new Toast(div)
expect(Toast.getOrCreateInstance(div)).toEqual(toast)
expect(Toast.getInstance(div)).toEqual(Toast.getOrCreateInstance(div, {}))
expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast)
})
it('should return new instance when there is no toast instance', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toBeNull()
expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast)
})
it('should return new instance when there is no toast instance with given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Toast.getInstance(div)).toBeNull()
const toast = Toast.getOrCreateInstance(div, {
delay: 1
})
expect(toast).toBeInstanceOf(Toast)
expect(toast._config.delay).toEqual(1)
})
it('should return the instance when exists without given configuration', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const toast = new Toast(div, {
delay: 1
})
expect(Toast.getInstance(div)).toEqual(toast)
const toast2 = Toast.getOrCreateInstance(div, {
delay: 2
})
expect(toast).toBeInstanceOf(Toast)
expect(toast2).toEqual(toast)
expect(toast2._config.delay).toEqual(1)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,321 @@
import Backdrop from '../../../src/util/backdrop.js'
import { getTransitionDurationFromElement } from '../../../src/util/index.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
const CLASS_BACKDROP = '.modal-backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
describe('Backdrop', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
const list = document.querySelectorAll(CLASS_BACKDROP)
for (const el of list) {
el.remove()
}
})
describe('show', () => {
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()
})
})
})
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()).toHaveSize(0)
instance.show(() => {
expect(getElements()).toHaveSize(0)
resolve()
})
})
})
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()
})
})
})
})
describe('hide', () => {
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)
expect(getElements()).toHaveSize(0)
instance.show(() => {
expect(getElements()).toHaveSize(1)
instance.hide(() => {
expect(getElements()).toHaveSize(0)
resolve()
})
})
})
})
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).not.toHaveClass(CLASS_NAME_SHOW)
resolve()
})
})
})
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()
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()
})
})
})
})
it('should not error if the backdrop no longer has a parent', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div id="wrapper"></div>'
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('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()
resolve()
}, 10)
}
instance.show(() => {
const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true })
document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent)
endTest()
})
})
})
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()
})
})
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()
})
})
})
})
})
})
})

View File

@ -0,0 +1,106 @@
import BaseComponent from '../../../src/base-component.js'
import { enableDismissTrigger } from '../../../src/util/component-functions.js'
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js'
class DummyClass2 extends BaseComponent {
static get NAME() {
return 'test'
}
hide() {
return true
}
testMethod() {
return true
}
}
describe('Plugin functions', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('data-bs-dismiss functionality', () => {
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>',
'</div>'
].join('')
const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
const spyTest = spyOn(DummyClass2.prototype, 'testMethod')
const componentWrapper = fixtureEl.querySelector('#foo')
const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
const event = createEvent('click')
enableDismissTrigger(DummyClass2, 'testMethod')
btnClose.dispatchEvent(event)
expect(spyGet).toHaveBeenCalledWith(componentWrapper)
expect(spyTest).toHaveBeenCalled()
})
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>',
'</div>'
].join('')
const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
const spyHide = spyOn(DummyClass2.prototype, 'hide')
const componentWrapper = fixtureEl.querySelector('#foo')
const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
const event = createEvent('click')
enableDismissTrigger(DummyClass2)
btnClose.dispatchEvent(event)
expect(spyGet).toHaveBeenCalledWith(componentWrapper)
expect(spyHide).toHaveBeenCalled()
})
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>',
'</div>'
].join('')
const spy = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
const event = createEvent('click')
enableDismissTrigger(DummyClass2)
btnClose.dispatchEvent(event)
expect(spy).not.toHaveBeenCalled()
})
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>',
'</div>'
].join('')
const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
const event = createEvent('click')
enableDismissTrigger(DummyClass2)
const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
btnClose.dispatchEvent(event)
expect(spy).toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,166 @@
import Config from '../../../src/util/config.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
class DummyConfigClass extends Config {
static get NAME() {
return 'dummy'
}
}
describe('Config', () => {
let fixtureEl
const name = 'dummy'
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
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('mergeConfigObj', () => {
it('should parse element\'s data attributes and merge it with default config. Element\'s data attributes must excel Defaults', () => {
fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string1="bar"></div>'
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
testBool: true,
testString: 'foo',
testString1: 'foo',
testInt: 7
})
const instance = new DummyConfigClass()
const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test'))
expect(configResult.testBool).toEqual(false)
expect(configResult.testString).toEqual('foo')
expect(configResult.testString1).toEqual('bar')
expect(configResult.testInt).toEqual(8)
})
it('should parse element\'s data attributes and merge it with default config, plug these given during method call. The programmatically given should excel all', () => {
fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string-1="bar"></div>'
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
testBool: true,
testString: 'foo',
testString1: 'foo',
testInt: 7
})
const instance = new DummyConfigClass()
const configResult = instance._mergeConfigObj({
testString1: 'test',
testInt: 3
}, fixtureEl.querySelector('#test'))
expect(configResult.testBool).toEqual(false)
expect(configResult.testString).toEqual('foo')
expect(configResult.testString1).toEqual('test')
expect(configResult.testInt).toEqual(3)
})
it('should parse element\'s data attribute `config` and any rest attributes. The programmatically given should excel all. Data attribute `config` should excel only Defaults', () => {
fixtureEl.innerHTML = '<div id="test" data-bs-config=\'{"testBool":false,"testInt":50,"testInt2":100}\' data-bs-test-int="8" data-bs-test-string-1="bar"></div>'
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
testBool: true,
testString: 'foo',
testString1: 'foo',
testInt: 7,
testInt2: 600
})
const instance = new DummyConfigClass()
const configResult = instance._mergeConfigObj({
testString1: 'test'
}, fixtureEl.querySelector('#test'))
expect(configResult.testBool).toEqual(false)
expect(configResult.testString).toEqual('foo')
expect(configResult.testString1).toEqual('test')
expect(configResult.testInt).toEqual(8)
expect(configResult.testInt2).toEqual(100)
})
it('should omit element\'s data attribute `config` if is not an object', () => {
fixtureEl.innerHTML = '<div id="test" data-bs-config="foo" data-bs-test-int="8"></div>'
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
testInt: 7,
testInt2: 79
})
const instance = new DummyConfigClass()
const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test'))
expect(configResult.testInt).toEqual(8)
expect(configResult.testInt2).toEqual(79)
})
})
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()
})
})
})

View File

@ -0,0 +1,218 @@
import EventHandler from '../../../src/dom/event-handler.js'
import SelectorEngine from '../../../src/dom/selector-engine.js'
import FocusTrap from '../../../src/util/focustrap.js'
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js'
describe('FocusTrap', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('activate', () => {
it('should autofocus itself by default', () => {
fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
const trapElement = fixtureEl.querySelector('div')
const spy = spyOn(trapElement, 'focus')
const focustrap = new FocusTrap({ trapElement })
focustrap.activate()
expect(spy).toHaveBeenCalled()
})
it('if configured not to autofocus, should not autofocus itself', () => {
fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
const trapElement = fixtureEl.querySelector('div')
const spy = spyOn(trapElement, 'focus')
const focustrap = new FocusTrap({ trapElement, autofocus: false })
focustrap.activate()
expect(spy).not.toHaveBeenCalled()
})
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 inside = document.getElementById('inside')
const focusInListener = () => {
expect(spy).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
resolve()
}
const spy = spyOn(inside, 'focus')
spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside])
document.addEventListener('focusin', focusInListener)
const focusInEvent = createEvent('focusin', { bubbles: true })
Object.defineProperty(focusInEvent, 'target', {
value: document.getElementById('outside')
})
document.dispatchEvent(focusInEvent)
})
})
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 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])
const spy = spyOn(first, 'focus').and.callThrough()
const focusInListener = () => {
expect(spy).toHaveBeenCalled()
first.removeEventListener('focusin', focusInListener)
resolve()
}
first.addEventListener('focusin', focusInListener)
const keydown = createEvent('keydown')
keydown.key = 'Tab'
document.dispatchEvent(keydown)
outside.focus()
})
})
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])
const spy = spyOn(last, 'focus').and.callThrough()
const focusInListener = () => {
expect(spy).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(spy).toHaveBeenCalled()
document.removeEventListener('focusin', focusInListener)
resolve()
}
const spy = 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)
})
})
})
describe('deactivate', () => {
it('should flag itself as no longer active', () => {
const focustrap = new FocusTrap({ trapElement: fixtureEl })
focustrap.activate()
expect(focustrap._isActive).toBeTrue()
focustrap.deactivate()
expect(focustrap._isActive).toBeFalse()
})
it('should remove all event listeners', () => {
const focustrap = new FocusTrap({ trapElement: fixtureEl })
focustrap.activate()
const spy = spyOn(EventHandler, 'off')
focustrap.deactivate()
expect(spy).toHaveBeenCalled()
})
it('doesn\'t try removing event listeners unless it needs to (in case it hasn\'t been activated)', () => {
const focustrap = new FocusTrap({ trapElement: fixtureEl })
const spy = spyOn(EventHandler, 'off')
focustrap.deactivate()
expect(spy).not.toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,720 @@
import * as Util from '../../../src/util/index.js'
import { noop } from '../../../src/util/index.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'
describe('Util', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
})
describe('getUID', () => {
it('should generate uid', () => {
const uid = Util.getUID('bs')
const uid2 = Util.getUID('bs')
expect(uid).not.toEqual(uid2)
})
})
describe('getTransitionDurationFromElement', () => {
it('should get transition from element', () => {
fixtureEl.innerHTML = '<div style="transition: all 300ms ease-out;"></div>'
expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(300)
})
it('should return 0 if the element is undefined or null', () => {
expect(Util.getTransitionDurationFromElement(null)).toEqual(0)
expect(Util.getTransitionDurationFromElement(undefined)).toEqual(0)
})
it('should return 0 if the element do not possess transition', () => {
fixtureEl.innerHTML = '<div></div>'
expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(0)
})
})
describe('triggerTransitionEnd', () => {
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()
el.addEventListener('transitionend', () => {
expect(spy).toHaveBeenCalled()
resolve()
})
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('')
const el = fixtureEl.querySelector('#foo')
expect(Util.isElement(el)).toBeTrue()
expect(Util.isElement({})).toBeFalse()
expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toBeFalse()
})
it('should detect jQuery element', () => {
fixtureEl.innerHTML = '<div></div>'
const el = fixtureEl.querySelector('div')
const fakejQuery = {
0: el,
jquery: 'foo'
}
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('')
const el = fixtureEl.querySelector('div')
expect(Util.getElement(el)).toEqual(el)
expect(Util.getElement('#foo')).toEqual(el)
expect(Util.getElement('#fail')).toBeNull()
expect(Util.getElement({})).toBeNull()
expect(Util.getElement([])).toBeNull()
expect(Util.getElement()).toBeNull()
expect(Util.getElement(null)).toBeNull()
expect(Util.getElement(fixtureEl.querySelectorAll('.test'))).toBeNull()
const fakejQueryObject = {
0: el,
jquery: 'foo'
}
expect(Util.getElement(fakejQueryObject)).toEqual(el)
})
})
describe('isVisible', () => {
it('should return false if the element is not defined', () => {
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({})).toBeFalse()
})
it('should return false if the element is not visible with display none', () => {
fixtureEl.innerHTML = '<div style="display: none;"></div>'
const div = fixtureEl.querySelector('div')
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if the element is not visible with visibility hidden', () => {
fixtureEl.innerHTML = '<div style="visibility: hidden;"></div>'
const div = fixtureEl.querySelector('div')
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if an ancestor element is display none', () => {
fixtureEl.innerHTML = [
'<div style="display: none;">',
' <div>',
' <div>',
' <div class="content"></div>',
' </div>',
' </div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toBeFalse()
})
it('should return false if an ancestor element is visibility hidden', () => {
fixtureEl.innerHTML = [
'<div style="visibility: hidden;">',
' <div>',
' <div>',
' <div class="content"></div>',
' </div>',
' </div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toBeFalse()
})
it('should return true if an ancestor element is visibility hidden, but reverted', () => {
fixtureEl.innerHTML = [
'<div style="visibility: hidden;">',
' <div style="visibility: visible;">',
' <div>',
' <div class="content"></div>',
' </div>',
' </div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('.content')
expect(Util.isVisible(div)).toBeTrue()
})
it('should return true if the element is visible', () => {
fixtureEl.innerHTML = [
'<div>',
' <div id="element"></div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('#element')
expect(Util.isVisible(div)).toBeTrue()
})
it('should return false if the element is hidden, but not via display or visibility', () => {
fixtureEl.innerHTML = [
'<details>',
' <div id="element"></div>',
'</details>'
].join('')
const div = fixtureEl.querySelector('#element')
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)).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({})).toBeTrue()
expect(Util.isDisabled('test')).toBeTrue()
})
it('should return true if the element has disabled attribute', () => {
fixtureEl.innerHTML = [
'<div>',
' <div id="element" disabled="disabled"></div>',
' <div id="element1" disabled="true"></div>',
' <div id="element2" disabled></div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('#element')
const div1 = fixtureEl.querySelector('#element1')
const div2 = fixtureEl.querySelector('#element2')
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', () => {
fixtureEl.innerHTML = [
'<div>',
' <div id="element" disabled="false"></div>',
' <div id="element1" ></div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('#element')
const div1 = fixtureEl.querySelector('#element1')
expect(Util.isDisabled(div)).toBeFalse()
expect(Util.isDisabled(div1)).toBeFalse()
})
it('should return false if the element is not disabled ', () => {
fixtureEl.innerHTML = [
'<div>',
' <button id="button"></button>',
' <select id="select"></select>',
' <select id="input"></select>',
'</div>'
].join('')
const el = selector => fixtureEl.querySelector(selector)
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>',
' <input id="input" disabled="disabled">',
' <input id="input1" disabled="disabled">',
' <button id="button" disabled="true"></button>',
' <button id="button1" disabled="disabled"></button>',
' <button id="button2" disabled></button>',
' <select id="select" disabled></select>',
' <select id="input" disabled></select>',
'</div>'
].join('')
const el = selector => fixtureEl.querySelector(selector)
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"', () => {
fixtureEl.innerHTML = [
'<div>',
' <div id="element" class="disabled"></div>',
'</div>'
].join('')
const div = fixtureEl.querySelector('#element')
expect(Util.isDisabled(div)).toBeTrue()
})
it('should return true if the element has class "disabled" but disabled attribute is false', () => {
fixtureEl.innerHTML = [
'<div>',
' <input id="input" class="disabled" disabled="false">',
'</div>'
].join('')
const div = fixtureEl.querySelector('#input')
expect(Util.isDisabled(div)).toBeTrue()
})
})
describe('findShadowRoot', () => {
it('should return null if shadow dom is not available', () => {
// Only for newer browsers
if (!document.documentElement.attachShadow) {
expect().nothing()
return
}
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
spyOn(document.documentElement, 'attachShadow').and.returnValue(null)
expect(Util.findShadowRoot(div)).toBeNull()
})
it('should return null when we do not find a shadow root', () => {
// Only for newer browsers
if (!document.documentElement.attachShadow) {
expect().nothing()
return
}
spyOn(document, 'getRootNode').and.returnValue(undefined)
expect(Util.findShadowRoot(document)).toBeNull()
})
it('should return the shadow root when found', () => {
// Only for newer browsers
if (!document.documentElement.attachShadow) {
expect().nothing()
return
}
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const shadowRoot = div.attachShadow({
mode: 'open'
})
expect(Util.findShadowRoot(shadowRoot)).toEqual(shadowRoot)
shadowRoot.innerHTML = '<button>Shadow Button</button>'
expect(Util.findShadowRoot(shadowRoot.firstChild)).toEqual(shadowRoot)
})
})
describe('noop', () => {
it('should be a function', () => {
expect(Util.noop).toEqual(jasmine.any(Function))
})
})
describe('reflow', () => {
it('should return element offset height to force the reflow', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const spy = spyOnProperty(div, 'offsetHeight')
Util.reflow(div)
expect(spy).toHaveBeenCalled()
})
})
describe('getjQuery', () => {
const fakejQuery = { trigger() {} }
beforeEach(() => {
Object.defineProperty(window, 'jQuery', {
value: fakejQuery,
writable: true
})
})
afterEach(() => {
window.jQuery = undefined
})
it('should return jQuery object when present', () => {
expect(Util.getjQuery()).toEqual(fakejQuery)
})
it('should not return jQuery object when present if data-bs-no-jquery', () => {
document.body.setAttribute('data-bs-no-jquery', '')
expect(window.jQuery).toEqual(fakejQuery)
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()).toBeNull()
})
})
describe('onDOMContentLoaded', () => {
it('should execute callbacks when DOMContentLoaded is fired and should not add more than one listener', () => {
const spy = jasmine.createSpy()
const spy2 = jasmine.createSpy()
const spyAdd = spyOn(document, 'addEventListener').and.callThrough()
spyOnProperty(document, 'readyState').and.returnValue('loading')
Util.onDOMContentLoaded(spy)
Util.onDOMContentLoaded(spy2)
document.dispatchEvent(new Event('DOMContentLoaded', {
bubbles: true,
cancelable: true
}))
expect(spy).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
expect(spyAdd).toHaveBeenCalledTimes(1)
})
it('should execute callback if readyState is not "loading"', () => {
const spy = jasmine.createSpy()
Util.onDOMContentLoaded(spy)
expect(spy).toHaveBeenCalled()
})
})
describe('defineJQueryPlugin', () => {
const fakejQuery = { fn: {} }
beforeEach(() => {
Object.defineProperty(window, 'jQuery', {
value: fakejQuery,
writable: true
})
})
afterEach(() => {
window.jQuery = undefined
})
it('should define a plugin on the jQuery instance', () => {
const pluginMock = Util.noop
pluginMock.NAME = 'test'
pluginMock.jQueryInterface = Util.noop
Util.defineJQueryPlugin(pluginMock)
expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface)
expect(fakejQuery.fn.test.Constructor).toEqual(pluginMock)
expect(fakejQuery.fn.test.noConflict).toEqual(jasmine.any(Function))
})
})
describe('execute', () => {
it('should execute if arg is function', () => {
const spy = jasmine.createSpy('spy')
Util.execute(spy)
expect(spy).toHaveBeenCalled()
})
it('should execute if arg is function & return the result', () => {
const functionFoo = (num1, num2 = 10) => num1 + num2
const resultFoo = Util.execute(functionFoo, [4, 5])
expect(resultFoo).toBe(9)
const resultFoo1 = Util.execute(functionFoo, [4])
expect(resultFoo1).toBe(14)
const functionBar = () => 'foo'
const resultBar = Util.execute(functionBar)
expect(resultBar).toBe('foo')
})
it('should not execute if arg is not function & return default argument', () => {
const foo = 'bar'
expect(Util.execute(foo)).toBe('bar')
expect(Util.execute(foo, [], 4)).toBe(4)
})
})
describe('executeAfterTransition', () => {
it('should immediately execute a function when waitForTransition parameter is false', () => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
const eventListenerSpy = spyOn(el, 'addEventListener')
Util.executeAfterTransition(callbackSpy, el, false)
expect(callbackSpy).toHaveBeenCalled()
expect(eventListenerSpy).not.toHaveBeenCalled()
})
it('should execute a function when a transitionend event is dispatched', () => {
const el = document.createElement('div')
const callbackSpy = jasmine.createSpy('callback spy')
spyOn(window, 'getComputedStyle').and.returnValue({
transitionDuration: '0.05s',
transitionDelay: '0s'
})
Util.executeAfterTransition(callbackSpy, el)
el.dispatchEvent(new TransitionEvent('transitionend'))
expect(callbackSpy).toHaveBeenCalled()
})
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'
})
Util.executeAfterTransition(callbackSpy, el)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalled()
resolve()
}, 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', () => {
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'
})
Util.executeAfterTransition(callbackSpy, el)
setTimeout(() => {
el.dispatchEvent(new TransitionEvent('transitionend'))
}, 50)
setTimeout(() => {
expect(callbackSpy).toHaveBeenCalledTimes(1)
resolve()
}, 70)
})
})
it('should not trigger a transitionend event if another transitionend event had already happened', () => {
return new Promise(resolve => {
const el = document.createElement('div')
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'))
const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough()
setTimeout(() => {
// setTimeout should not have triggered another transitionend event.
expect(dispatchSpy).not.toHaveBeenCalled()
resolve()
}, 70)
})
})
it('should ignore transitionend events from nested elements', () => {
return new Promise(resolve => {
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()
resolve()
}, 70)
})
})
})
describe('getNextActiveElement', () => {
it('should return first element if active not exists or not given and shouldGetNext is either true, or false with cycling being disabled', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a')
expect(Util.getNextActiveElement(array, '', true, false)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', true, false)).toEqual('a')
expect(Util.getNextActiveElement(array, '', false, false)).toEqual('a')
expect(Util.getNextActiveElement(array, 'g', false, false)).toEqual('a')
})
it('should return last element if active not exists or not given and shouldGetNext is false but cycling is enabled', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, '', false, true)).toEqual('d')
expect(Util.getNextActiveElement(array, 'g', false, true)).toEqual('d')
})
it('should return next element or same if is last', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, 'a', true, true)).toEqual('b')
expect(Util.getNextActiveElement(array, 'b', true, true)).toEqual('c')
expect(Util.getNextActiveElement(array, 'd', true, false)).toEqual('d')
})
it('should return next element or first, if is last and "isCycleAllowed = true"', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, 'c', true, true)).toEqual('d')
expect(Util.getNextActiveElement(array, 'd', true, true)).toEqual('a')
})
it('should return previous element or same if is first', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, 'b', false, true)).toEqual('a')
expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c')
expect(Util.getNextActiveElement(array, 'a', false, false)).toEqual('a')
})
it('should return next element or first, if is last and "isCycleAllowed = true"', () => {
const array = ['a', 'b', 'c', 'd']
expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c')
expect(Util.getNextActiveElement(array, 'a', false, true)).toEqual('d')
})
})
})

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