diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 41fbd6a07d..0d370daed1 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -10,7 +10,7 @@ body: options: - label: I have searched for [duplicate or closed issues](https://github.com/materializecss/materialize/issues?utf8=%E2%9C%93&q=is%3Aissue). required: true - - label: I have read the [CONTRIBUTING document](https://github.com/materializecss/materialize/blob/master/CONTRIBUTING.md) and my issue is following the guidelines. + - label: I have read the [CONTRIBUTING document](https://github.com/materializecss/materialize/blob/main/docs/CONTRIBUTING.md) and my issue is following the guidelines. required: true - label: I have read the template completely before filling it in. required: true @@ -50,10 +50,10 @@ body: attributes: label: Steps to reproduce description: | - If relevant, give a set of steps to reproduce this bug. + If relevant, give a set of steps to reproduce this bug. We encourage you a live example that demonstrates the bug (you may want to use the [following jsfiddle template](https://jsfiddle.net/1es0zhaf/)) placeholder: | - 1. Open this link + 1. Open this link 2. Click there 3. This appears, but... validations: diff --git a/.github/ISSUE_TEMPLATE/improvement.yml b/.github/ISSUE_TEMPLATE/improvement.yml index 6f87ce4f9d..21f5b0a397 100644 --- a/.github/ISSUE_TEMPLATE/improvement.yml +++ b/.github/ISSUE_TEMPLATE/improvement.yml @@ -8,7 +8,7 @@ body: options: - label: I have searched for [duplicate or closed issues](https://github.com/materializecss/materialize/issues?utf8=%E2%9C%93&q=is%3Aissue). required: true - - label: I have read the [CONTRIBUTING document](https://github.com/materializecss/materialize/blob/master/CONTRIBUTING.md) and my issue is following the guidelines. + - label: I have read the [CONTRIBUTING document](https://github.com/materializecss/materialize/blob/main/docs/CONTRIBUTING.md) and my issue is following the guidelines. required: true - label: I have read the template completely before filling it in. required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 067f057beb..45fa972eac 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ ## Screenshots (if appropriate) or codepen: - + ## Types of changes @@ -14,8 +14,8 @@ ## Checklist: -- [ ] I have read the **[CONTRIBUTING document](https://github.com/materializecss/materialize/blob/main/CONTRIBUTING.md)**. -- [ ] My commit messages follows the [conventional commit format](https://github.com/materializecss/materialize/blob/main/CONTRIBUTING.md#submitting-your-pull-request) +- [ ] I have read the **[CONTRIBUTING document](https://github.com/materializecss/materialize/blob/main/docs/CONTRIBUTING.md)**. +- [ ] My commit messages follows the [conventional commit format](https://github.com/materializecss/materialize/blob/main/docs/CONTRIBUTING.md#submitting-your-pull-request) - [ ] My change requires a change to the documentation, and updated it accordingly. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0180aa7808..4e90b0f74e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,26 +7,24 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - - uses: actions/setup-node@v4 - with: - node-version: 23 + - uses: actions/checkout@master + - uses: actions/setup-node@v4 + with: + node-version: 23 - - name: Run tests - run: | - npm i - npm run build - npm run test + - name: Run tests + run: | + npm i + npm run build + npm test - - name: Nightly build - run: | - npm run release + - name: Nightly build + run: | + npm run release - - uses: actions/upload-artifact@v4 - with: - name: build - path: dist/ + - uses: actions/upload-artifact@v4 + with: + name: build + path: dist/ diff --git a/.gitignore b/.gitignore index b39fc3fbd4..e1abc63593 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ node_modules/ # Ignore lock yarn.lock -dist/*.zip +dist/**/* -*storybook.log -storybook-static +examples/html diff --git a/.storybook/main.ts b/.storybook/main.ts deleted file mode 100644 index 58dc67e833..0000000000 --- a/.storybook/main.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { StorybookConfig } from '@storybook/html-vite'; - -const config: StorybookConfig = { - stories: ['../components/**/*.mdx', '../components/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-themes', '@storybook/addon-docs'], - framework: { - name: '@storybook/html-vite', - options: {} - }, - docs: { - //👇 See the table below for the list of supported options - defaultName: 'Documentation' - } -}; -export default config; diff --git a/.storybook/manager.js b/.storybook/manager.js deleted file mode 100644 index d82a4cfcfa..0000000000 --- a/.storybook/manager.js +++ /dev/null @@ -1,6 +0,0 @@ -import { addons } from 'storybook/manager-api'; -import yourTheme from './my-theme'; - -addons.setConfig({ - theme: yourTheme -}); diff --git a/.storybook/material-icons.css b/.storybook/material-icons.css deleted file mode 100644 index 6f703d53bb..0000000000 --- a/.storybook/material-icons.css +++ /dev/null @@ -1,18 +0,0 @@ -@import '@fontsource/material-icons'; - -/* Font source only provides @font-face, so we need this class definition here */ -.material-icons { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-feature-settings: 'liga'; - -webkit-font-smoothing: antialiased; -} diff --git a/.storybook/my-theme.js b/.storybook/my-theme.js deleted file mode 100644 index 0c6a4eeca2..0000000000 --- a/.storybook/my-theme.js +++ /dev/null @@ -1,9 +0,0 @@ -import { create } from 'storybook/theming'; - -export default create({ - // base: 'light', - brandTitle: 'My custom Storybook', - brandUrl: 'https://example.com', - brandImage: 'https://storybook.js.org/images/placeholders/350x150.png', - brandTarget: '_self' -}); diff --git a/.storybook/preview.ts b/.storybook/preview.ts deleted file mode 100644 index 5697a49353..0000000000 --- a/.storybook/preview.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Preview } from '@storybook/html'; -import { withThemeByDataAttribute } from '@storybook/addon-themes'; -import './material-icons.css'; -import '../sass/materialize.scss'; - -const preview: Preview = { - decorators: [ - // Decorator to be able to switch between light and dark themes - withThemeByDataAttribute({ - themes: { - light: 'light', - dark: 'dark' - }, - defaultTheme: 'light', - attributeName: 'theme' - }) - ], - parameters: { - docs: { - toc: true // Enables the table of contents in auto generated docs - }, - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i - } - } - } -}; - -export default preview; diff --git a/README.md b/README.md index d155fe06fd..1f4d320097 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@

## Getting started + Read the [getting started guide](https://materializeweb.com/getting-started.html) for more information on how to use materialize. - [Download the latest release](https://github.com/materializecss/materialize/releases/latest) of materialize directly from GitHub. ([Beta](https://github.com/materializecss/materialize/releases/)) @@ -37,40 +38,27 @@ Read the [getting started guide](https://materializeweb.com/getting-started.html - Include the files via [jsDelivr](https://www.jsdelivr.com/package/npm/@materializecss/materialize). - Install with [npm](https://www.npmjs.com): `npm install @materializecss/materialize` (Beta: `npm install @materializecss/materialize@next`) -## Testing -To run tests locally you first have to build the files and then run the tests. This can be done with the command `npm test`; -To get more info and debug information you can do the following: -run `npx jasmine-browser-runner` then go to the url via browser `http://localhost:8888`. The tests will then run and show detailed feedback. +### Releases -## Development -The documentation can be found at . To run the documentation locally on your machine, you need [Node.js](https://nodejs.org) installed on your computer. -This is the core project with all the components. To see how they are used we recommend using an example project or go to the documentation. +Previous releases are available [here](https://github.com/materializecss/materialize/releases). You can also find the changelog under each release. -### Running documentation locally -See [Materialize Docs Repo](https://github.com/materializecss/materialize-docs) to see Materialize in Action. +## Examples -### Releases -Previous releases are available [here](https://github.com/materializecss/materialize/releases). You can also find the changelog under each release. +First install materializeweb locally. Then create the html files with `npm run build-examples`. After creating the files you can run `npm run demo` and browse to the url (normally [127.0.0.1:8080/](http://127.0.0.1:8080)) + +## Tests -## Supported Browsers: -Materialize is compatible with: +To run tests locally you first have to build the files and then run the tests. This can be done with the command `npm test`; +The tests will then run and show detailed feedback. -- Chrome 35+ -- Firefox 31+ -- Safari 9+ -- Opera -- Edge +## Develop -## Testing -We use Jasmine as our testing framework and we're trying to write a robust test suite for our components. If you want to help, [here's a starting guide on how to write tests in Jasmine](docs/CONTRIBUTING.md#jasmine-testing-guide). +This is the core project with all the components. To see how they are used we recommend to check out the examples or go to the documentation at . -## Quick Testing and Development -For quick testing install live-server node package globally and run `npm run demo` +## Contribute -## Contributing Check out the [CONTRIBUTING document](docs/CONTRIBUTING.md) in the root of the repository to learn how you can contribute. You can also browse the [help-wanted](https://github.com/materializecss/materialize/labels/help-wanted) tag in our issue tracker to find things to do. -## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/materialize#sponsor)] [![OC sponsor 0](https://opencollective.com/materialize/sponsor/0/avatar.svg)](https://opencollective.com/materialize/sponsor/0/website) @@ -84,11 +72,10 @@ Support this project by becoming a sponsor. Your logo will show up here with a l [![OC sponsor 8](https://opencollective.com/materialize/sponsor/8/avatar.svg)](https://opencollective.com/materialize/sponsor/8/website) [![OC sponsor 9](https://opencollective.com/materialize/sponsor/9/avatar.svg)](https://opencollective.com/materialize/sponsor/9/website) - -## Backers -Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/materialize#backer)] +Thank you to all our supporters! 🙏 [[Become a backer](https://opencollective.com/materialize#backer)] [![Backers](https://opencollective.com/materialize/backers.svg?width=890)](https://opencollective.com/materialize#backers) ## Copyright and license + Code Copyright 2026 Materialize. Code released under the MIT license. diff --git a/components/appbar/appbar.mjs b/components/appbar/appbar.mjs deleted file mode 100644 index 75feacd8ee..0000000000 --- a/components/appbar/appbar.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } from '../atomic/component.mjs'; - -// AppBar is the old *Navbar* - -class AppBar extends Component { - #title = ''; - - constructor(options) { - super(options); - this.setTagName('nav').addClassname('nav navbar'); - } - - setTitle(title) { - this.#title = title; - return this; - } - - setSearch() { - return this; - } - - setLogo() { - return this; - } - - addLeftIcon() { - return this; - } - - addRightIcon() { - return this; - } - - toHTML() { - const html = ``; - this.setChildren(html); - return super.toHTML(); - } -} - -export { AppBar }; diff --git a/components/atomic/component.mjs b/components/atomic/component.mjs deleted file mode 100644 index b79e33c316..0000000000 --- a/components/atomic/component.mjs +++ /dev/null @@ -1,67 +0,0 @@ -// function escapeHTML(str) { -// return str -// .replaceAll('&', '&') -// .replaceAll('<', '<') -// .replaceAll('>', '>') -// .replaceAll('"', '"') -// .replaceAll("'", '''); -// } - -class Component { - #tagname; - #children; - #classnames; - - constructor(options) { - this.#tagname = 'div'; - this.#children = []; - this.#classnames = []; - if (typeof options === 'object' && options.children) { - // Tree Nodes - options.children.forEach((c) => this.addChild(c)); - return; - } - // Text or numeric - this.#children = options; - } - - setChildren(children) { - this.#children = children; - return this; - } - - setTagName(tagname) { - if (typeof tagname !== 'string') throw new Error('tagname needs to be string'); - this.#tagname = tagname; - return this; - } - - addChild(child) { - if (!(child instanceof Component)) throw new Error('child is no valid component'); - this.#children.push(child); - return this; - } - - addClassname(name) { - this.#classnames.push(name); - return this; - } - - toHTML() { - const classAttr = - this.#classnames.length === 0 ? '' : ' class="' + this.#classnames.join(' ') + '"'; - if (typeof this.#children === 'object') - return `<${this.#tagname}${classAttr}>${this.#children.map((child) => child.toHTML()).join('')}`; - // string or number - return `<${this.#tagname}${classAttr}>${this.#children}`; - } - - // TODO: remove here - toDOM() { - const template = document.createElement('template'); - template.innerHTML = this.toHTML(); - return template.content.firstElementChild; - } -} - -export { Component }; diff --git a/components/badge/_badges.scss b/components/badge/_badges.scss deleted file mode 100644 index 03912bbf4f..0000000000 --- a/components/badge/_badges.scss +++ /dev/null @@ -1,75 +0,0 @@ -// TODO: Make better structure -:root { - --bagde-height: 22px; -} - -span.badge { - min-width: 3rem; - padding: 0 6px; - margin-left: 14px; - text-align: center; - font-size: 1rem; - line-height: var(--bagde-height); - height: var(--bagde-height); - color: var(--md-sys-color-on-surface-variant); - float: right; - box-sizing: border-box; - - &.new { - font-weight: 300; - font-size: 0.8rem; - color: var(--md-sys-color-on-primary); - background-color: var(--md-sys-color-primary); - border-radius: 2px; - } - &.new:after { - content: ' new'; - } - - &[data-badge-caption]::after { - content: ' ' attr(data-badge-caption); - } -} - -.active span.badge { - color: var(--md-sys-color-on-primary); -} - -// Special cases - -.navbar ul a span.badge { - display: inline-block; - float: none; - margin-left: 4px; - line-height: var(--bagde-height); - height: var(--bagde-height); - -webkit-font-smoothing: auto; -} - -// Line height centering -.collection-item span.badge { - margin-top: calc(calc(1.5rem * 0.5) - calc(var(--bagde-height) * 0.5)); -} - -.collapsible span.badge { - margin-left: auto; - - &.leading { - margin-right: 7px; - order: -1; - } -} - -.collapsible .active span.badge:not(.new) { - color: var(--md-sys-color-on-surface-variant); -} - -.sidenav span.badge { - margin-top: calc(calc(var(--sidenav-line-height) * 0.5) - calc(22px * 0.5)); -} - -table span.badge { - display: inline-block; - float: none; - margin-left: auto; -} diff --git a/components/breadcrumb/_breadcrumb.scss b/components/breadcrumb/_breadcrumb.scss deleted file mode 100644 index 7ef883c0a8..0000000000 --- a/components/breadcrumb/_breadcrumb.scss +++ /dev/null @@ -1,27 +0,0 @@ -.breadcrumb-wrapper { - display: flex; - align-items: center; -} - -.breadcrumb { - display: inline-flex; - align-items: center; - - i, - i.material-icons, i.material-symbols-outlined, - i.material-symbols-rounded, i.material-symbols-sharp, - &:before { - font-weight: normal; - font-style: normal; - font-size: 24px; - } - - &:before { - content: '\E5CC'; - font-family: 'Material Symbols Outlined', 'Material Symbols Rounded', 'Material Symbols Sharp', 'Material Icons'; - } - - &:first-child:before { - visibility: hidden; - } -} diff --git a/components/button/examples.iso.js b/components/button/examples.iso.js deleted file mode 100644 index 5c7f3a4110..0000000000 --- a/components/button/examples.iso.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Number, Text } from '../atomic/atomic.mjs'; -import { Card } from '../card/card.mjs'; -import { Button } from './button.mjs'; - -// Example App (isomorphic javascript) - -function createCounterApp() { - return new Card({ - children: [ - new Text('Counter Example').setTagName('div'), - new Number(9), - new Button('➕'), - new Button('➖') - ] - }).addClassname('p-3'); -} - -export { createCounterApp }; diff --git a/components/button/test-button.mjs b/components/button/test-button.mjs deleted file mode 100644 index 4f079113b7..0000000000 --- a/components/button/test-button.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { Number, Text } from '../atomic/atomic.mjs'; -import { Button, Card } from './button.mjs'; - -// Example App (isomorphic javascript) -const app = new Card({ - children: [ - new Text('Example App').setTagName('div'), // - new Number(9), - new Button('➕'), - new Button('➖') - ] -}).addClassname('p-3'); - -// Html -const html = app.toHTML(); -console.log(html); diff --git a/components/card/_cards.scss b/components/card/_cards.scss deleted file mode 100644 index c5eff946dc..0000000000 --- a/components/card/_cards.scss +++ /dev/null @@ -1,229 +0,0 @@ -@use '../../sass/variables' as *; -@use '../../sass/mixins.module.scss' as *; - -.card-panel { - transition: box-shadow .25s; - padding: 24px; - margin: $element-top-margin 0 $element-bottom-margin 0; - border-radius: 12px; - @extend .z-depth-1; - background-color: var(--md-sys-color-surface); -} - -.card { - --background-color: color-mix(in srgb, var(--md-sys-color-surface), var(--md-sys-color-surface-tint) 17%); - - position: relative; - //margin: $element-top-margin 0 $element-bottom-margin 0; - background-color: var(--background-color); - transition: box-shadow .25s; - border-radius: 12px; - @extend .z-depth-1; - - .card-title { - font-size: 24px; - font-weight: 300; - } - - // Card Sizes - &.small, &.medium, &.large { - position: relative; - - .card-image { - max-height: 60%; - overflow: hidden; - } - - .card-image + .card-content { - max-height: 40%; - } - - .card-content { - max-height: 100%; - overflow: hidden; - } - - .card-action { - position: absolute; - bottom: 0; - left: 0; - right: 0; - } - } - - &.small { - height: 300px; - } - - &.medium { - height: 400px; - } - - &.large { - height: 500px; - } - - // Horizontal Cards - &.horizontal { - display: flex; - - &.small, &.medium, &.large { - .card-image { - height: 100%; - max-height: none; - overflow: visible; - - img { - height: 100%; - } - } - } - - .card-image { - max-width: 50%; - - img { - border-radius: 2px 0 0 2px; - max-width: 100%; - width: auto; - } - } - - .card-stacked { - display: flex; - flex-direction: column; - flex: 1; - position: relative; - - .card-content { - flex-grow: 1; - } - } - } - - // Sticky Action Section - &.sticky-action { - .card-action { - z-index: 2; - } - - .card-reveal { - z-index: 1; - padding-bottom: 64px; - } - } - - .card-image { - position: relative; - - // Image background for content - img { - display: block; - border-radius: 2px 2px 0 0; - position: relative; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: 100%; - } - - .card-title { - color: var(--md-sys-color-surface); - position: absolute; - bottom: 0; - left: 0; - max-width: 100%; - padding: 24px; - } - - .activator { - position: absolute; - left: 0; - right: 0; - top:0; - bottom: 0; - cursor: pointer; - } - - img.activator { - position: relative; - } - } - - .card-content { - padding: 24px; - border-radius: 0 0 2px 2px; - - p { - margin: 0; - } - - .card-title { - display: block; - line-height: 32px; - margin-bottom: 8px; - - i { - line-height: 32px; - } - - &.activator { - cursor: pointer; - } - } - } - - .card-action { - padding: 0 1.6rem; - - &:last-child { - border-radius: 0 0 2px 2px; - } - // Replaced card links with buttons (Accessibility, @see https://github.com/materializecss/materialize/issues/565) - a { - @include btn( - var(--btn-height), - var(--btn-border-radius), - var(--btn-padding), - var(--btn-padding), - var(--btn-font-size) - ); - - &:first-child { - margin-left: -1.6rem; - } - - &:last-child { - margin-right: -1.6rem; - } - } - - .btn { - &.filled, - &.tonal, - &.elevated, - &.outlined { - margin: 0 .26rem 1.6rem 0; - } - } - } - - .card-reveal { - padding: 24px; - position: absolute; - background-color: var(--md-sys-color-surface); - width: 100%; - overflow-y: auto; - left: 0; - top: 100%; - height: 100%; - z-index: 3; - display: none; - - .card-title { - cursor: pointer; - display: block; - } - } -} diff --git a/components/carousel/_carousel.scss b/components/carousel/_carousel.scss deleted file mode 100644 index 4df4095f91..0000000000 --- a/components/carousel/_carousel.scss +++ /dev/null @@ -1,93 +0,0 @@ -.carousel { - --carousel-height: 400px; - - overflow: hidden; - position: relative; - width: 100%; - height: var(--carousel-height); - perspective: 500px; - transform-style: preserve-3d; - transform-origin: 0% 50%; - - &.carousel-slider { - top: 0; - left: 0; - - .carousel-fixed-item { - position: absolute; - left: 0; - right: 0; - bottom: 20px; - z-index: 1; - - &.with-indicators { - bottom: 68px; - } - } - - .carousel-item { - width: 100%; - height: 100%; - min-height: var(--carousel-height); - position: absolute; - top: 0; - left: 0; - - h2 { - font-size: 24px; - font-weight: 500; - line-height: 32px; - } - - p { - font-size: 15px; - } - } - } - - .carousel-item { - visibility: hidden; - width: calc(var(--carousel-height) * 0.5); - height: calc(var(--carousel-height) * 0.5); - position: absolute; - top: 0; - left: 0; - - & > img { - width: 100%; - } - } - - .indicators { - padding-left: 0; - list-style-type: none; - position: absolute; - text-align: center; - left: 0; - right: 0; - bottom: 0; - margin: 0; - - .indicator-item { - display: inline-block; - position: relative; - cursor: pointer; - height: 8px; - width: 8px; - margin: 24px 4px; - background-color: rgba(255, 255, 255, 0.45); - transition: background-color .3s; - border-radius: 50%; - - &.active { - background-color: var(--md-ref-palette-primary100); - } - } - } - - // Materialbox compatibility - &.scrolling .carousel-item .materialboxed, - .carousel-item:not(.active) .materialboxed { - pointer-events: none; - } -} diff --git a/components/carousel/slider.ts b/components/carousel/slider.ts deleted file mode 100644 index b4da9863da..0000000000 --- a/components/carousel/slider.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; - -export interface SliderOptions extends BaseOptions { - /** - * Set to false to hide slide indicators. - * @default true - */ - indicators: boolean; - /** - * Set height of slider. - * @default 400 - */ - height: number; - /** - * Set the duration of the transition animation in ms. - * @default 500 - */ - duration: number; - /** - * Set the duration between transitions in ms. - * @default 6000 - */ - interval: number; - /** - * If slider should pause when keyboard focus is received. - * @default true - */ - pauseOnFocus: boolean; - /** - * If slider should pause when is hovered by a pointer. - * @default true - */ - pauseOnHover: boolean; - /** - * Optional function used to generate ARIA label to indicators (for accessibility purposes). - * @param index Current index, starting from "1". - * @param current A which indicates whether it is the current element or not - * @returns a string to be used as label indicator. - * @default null - */ - indicatorLabelFunc: (index: number, current: boolean) => string; -} - -const _defaults: SliderOptions = { - indicators: true, - height: 400, - duration: 500, - interval: 6000, - pauseOnFocus: true, - pauseOnHover: true, - indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA) -}; - -export class Slider extends Component { - /** Index of current slide. */ - activeIndex: number; - interval: string | number | NodeJS.Timeout; - eventPause: boolean; - _slider: HTMLUListElement; - _slides: HTMLLIElement[]; - _activeSlide: HTMLLIElement; - _indicators: HTMLLIElement[]; - _hovered: boolean; - _focused: boolean; - _focusCurrent: boolean; - _sliderId: string; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, Slider); - this.el['M_Slider'] = this; - - this.options = { - ...Slider.defaults, - ...options - }; - - // init props - this.interval = null; - this.eventPause = false; - this._hovered = false; - this._focused = false; - this._focusCurrent = false; - - // setup - this._slider = this.el.querySelector('.slides'); - this._slides = Array.from(this._slider.querySelectorAll('li')); - this.activeIndex = this._slides.findIndex((li) => li.classList.contains('active')); - - if (this.activeIndex !== -1) { - this._activeSlide = this._slides[this.activeIndex]; - } - - this._setSliderHeight(); - - // Sets element id if it does not have one - if (this._slider.hasAttribute('id')) this._sliderId = this._slider.getAttribute('id'); - else { - this._sliderId = 'slider-' + Utils.guid(); - this._slider.setAttribute('id', this._sliderId); - } - - const placeholderBase64 = - 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; - // Set initial positions of captions - this._slides.forEach((slide) => { - // Caption - //const caption = slide.querySelector('.caption'); - //if (caption) this._animateCaptionIn(caption, 0); - // Set Images as Background Images - const img = slide.querySelector('img'); - if (img) { - if (img.src !== placeholderBase64) { - img.style.backgroundImage = 'url(' + img.src + ')'; - img.src = placeholderBase64; - } - } - // Sets slide as focusable by code - if (!slide.hasAttribute('tabindex')) slide.setAttribute('tabindex', '-1'); - // Removes initial visibility from "inactive" slides - slide.style.visibility = 'hidden'; - }); - - this._setupIndicators(); - - // Show active slide - if (this._activeSlide) { - this._activeSlide.style.display = 'block'; - this._activeSlide.style.visibility = 'visible'; - } else { - this.activeIndex = 0; - this._slides[0].classList.add('active'); - this._slides[0].style.visibility = 'visible'; - this._activeSlide = this._slides[0]; - this._animateSlide(this._slides[0], true); - // Update indicators - if (this.options.indicators) { - this._indicators[this.activeIndex].children[0].classList.add('active'); - } - } - this._setupEventHandlers(); - // auto scroll - this.start(); - } - - static get defaults() { - return _defaults; - } - - /** - * Initializes instance of Slider. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Slider; - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Slider[]; - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): Slider | Slider[] { - return super.init(els, options, Slider); - } - - static getInstance(el: HTMLElement): Slider { - return el['M_Slider']; - } - - destroy() { - this.pause(); - this._removeIndicators(); - this._removeEventHandlers(); - this.el['M_Slider'] = undefined; - } - - private _setupEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.addEventListener('focusin', this._handleAutoPauseFocus); - this.el.addEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.addEventListener('mouseenter', this._handleAutoPauseHover); - this.el.addEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.addEventListener('click', this._handleIndicatorClick); - }); - } - } - - private _removeEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.removeEventListener('focusin', this._handleAutoPauseFocus); - this.el.removeEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.removeEventListener('mouseenter', this._handleAutoPauseHover); - this.el.removeEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.removeEventListener('click', this._handleIndicatorClick); - }); - } - } - - private _handleIndicatorClick = (e: MouseEvent) => { - const el = (e.target).parentElement; - const currIndex = [...el.parentNode.children].indexOf(el); - this._focusCurrent = true; - this.set(currIndex); - }; - - private _handleAutoPauseHover = () => { - this._hovered = true; - if (this.interval != null) { - this._pause(true); - } - }; - - private _handleAutoPauseFocus = () => { - this._focused = true; - if (this.interval != null) { - this._pause(true); - } - }; - - private _handleAutoStartHover = () => { - this._hovered = false; - if (!(this.options.pauseOnFocus && this._focused) && this.eventPause) { - this.start(); - } - }; - - private _handleAutoStartFocus = () => { - this._focused = false; - if (!(this.options.pauseOnHover && this._hovered) && this.eventPause) { - this.start(); - } - }; - - private _handleInterval = () => { - const activeElem = this._slider.querySelector('.active'); - let newActiveIndex = [...activeElem.parentNode.children].indexOf(activeElem); - if (this._slides.length === newActiveIndex + 1) newActiveIndex = 0; // loop to start - else newActiveIndex += 1; - this.set(newActiveIndex); - }; - - private _animateSlide(slide: HTMLElement, isDirectionIn: boolean): void { - let dx = 0, - dy = 0; - // from - slide.style.opacity = isDirectionIn ? '0' : '1'; - setTimeout(() => { - slide.style.transition = `opacity ${this.options.duration}ms ease`; - // to - slide.style.opacity = isDirectionIn ? '1' : '0'; - }, 1); - // Caption - const caption: HTMLElement = slide.querySelector('.caption'); - if (!caption) return; - if (caption.classList.contains('center-align')) dy = -100; - else if (caption.classList.contains('right-align')) dx = 100; - else if (caption.classList.contains('left-align')) dx = -100; - // from - caption.style.opacity = isDirectionIn ? '0' : '1'; - caption.style.transform = isDirectionIn ? `translate(${dx}px, ${dy}px)` : `translate(0, 0)`; - setTimeout(() => { - caption.style.transition = `opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`; - // to - caption.style.opacity = isDirectionIn ? '1' : '0'; - caption.style.transform = isDirectionIn ? `translate(0, 0)` : `translate(${dx}px, ${dy}px)`; - }, this.options.duration); // delay - } - - private _setSliderHeight() { - // If fullscreen, do nothing - if (!this.el.classList.contains('fullscreen')) { - if (this.options.indicators) { - // Add height if indicators are present - this.el.style.height = this.options.height + 40 + 'px'; //.css('height', this.options.height + 40 + 'px'); - } else { - this.el.style.height = this.options.height + 'px'; - } - this._slider.style.height = this.options.height + 'px'; - } - } - - private _setupIndicators() { - if (this.options.indicators) { - const ul = document.createElement('ul'); - ul.classList.add('indicators'); - - const arrLi = []; - this._slides.forEach((el, i) => { - const label = this.options.indicatorLabelFunc - ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) - : `${i + 1}`; - const li = document.createElement('li'); - li.classList.add('indicator-item'); - li.innerHTML = ``; - arrLi.push(li); - ul.append(li); - }); - - this.el.append(ul); - this._indicators = arrLi; - } - } - - private _removeIndicators() { - this.el.querySelector('ul.indicators').remove(); //find('ul.indicators').remove(); - } - - set(index: number) { - // Wrap around indices. - if (index >= this._slides.length) index = 0; - else if (index < 0) index = this._slides.length - 1; - - // Only do if index changes - if (this.activeIndex === index) return; - - this._activeSlide = this._slides[this.activeIndex]; - const _caption = this._activeSlide.querySelector('.caption'); - - this._activeSlide.classList.remove('active'); - // Enables every slide - this._slides.forEach((slide) => (slide.style.visibility = 'visible')); - - //--- Hide active Slide + Caption - this._activeSlide.style.opacity = '0'; - setTimeout(() => { - this._slides.forEach((slide) => { - if (slide.classList.contains('active')) return; - slide.style.opacity = '0'; - slide.style.transform = 'translate(0, 0)'; - // Disables invisible slides (for assistive technologies) - slide.style.visibility = 'hidden'; - }); - }, this.options.duration); - - // Hide active Caption - //this._animateCaptionIn(_caption, this.options.duration); - _caption.style.opacity = '0'; - - // Update indicators - if (this.options.indicators) { - const activeIndicator = this._indicators[this.activeIndex].children[0]; - const nextIndicator = this._indicators[index].children[0]; - activeIndicator.classList.remove('active'); - nextIndicator.classList.add('active'); - if (typeof this.options.indicatorLabelFunc === 'function') { - activeIndicator.ariaLabel = this.options.indicatorLabelFunc.call( - this, - this.activeIndex, - false - ); - nextIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, index, true); - } - } - - //--- Show new Slide + Caption - this._animateSlide(this._slides[index], true); - this._slides[index].classList.add('active'); - this.activeIndex = index; - - // Reset interval, if allowed. This check prevents autostart - // when slider is paused, since it can be changed though indicators. - if (this.interval != null) { - this.start(); - } - } - - _pause(fromEvent: boolean) { - clearInterval(this.interval); - this.eventPause = fromEvent; - this.interval = null; - } - - /** - * Pause slider autoslide. - */ - pause = () => { - this._pause(false); - }; - - /** - * Start slider autoslide. - */ - start = () => { - clearInterval(this.interval); - this.interval = setInterval( - this._handleInterval, - this.options.duration + this.options.interval - ); - this.eventPause = false; - }; - - /** - * Move to next slider. - */ - next = () => { - let newIndex = this.activeIndex + 1; - // Wrap around indices. - if (newIndex >= this._slides.length) newIndex = 0; - else if (newIndex < 0) newIndex = this._slides.length - 1; - this.set(newIndex); - }; - - /** - * Move to prev slider. - */ - prev = () => { - let newIndex = this.activeIndex - 1; - // Wrap around indices. - if (newIndex >= this._slides.length) newIndex = 0; - else if (newIndex < 0) newIndex = this._slides.length - 1; - this.set(newIndex); - }; -} diff --git a/components/checkbox/_checkboxes.scss b/components/checkbox/_checkboxes.scss deleted file mode 100644 index 2c57d2dedb..0000000000 --- a/components/checkbox/_checkboxes.scss +++ /dev/null @@ -1,281 +0,0 @@ -/* Checkboxes */ - -@at-root { - :root { - --checkbox-size: 18px; // 18px - --checkbox-shape: 2px; - } -} - -input[type="checkbox"] { - /* Add if not using autoprefixer */ - -webkit-appearance: none; - appearance: none; - /* For iOS < 15 to remove gradient background */ - // background-color: #fff; - margin: 0; - position: relative; - cursor: pointer; -} - -input[type="checkbox"]:focus { - outline: 3px solid var(--md-sys-color-secondary); - border-radius: 50%; - outline-offset: 0.5rem; -} - -input[type="checkbox"]:before { - opacity: 1; - position: absolute; - display: block; - width: var(--checkbox-size); - height: var(--checkbox-size); - border: 2px solid var(--md-sys-color-on-surface-variant); - border-radius: var(--checkbox-shape); - content: ""; - // background: #fff; -} - -input[type="checkbox"]:after { - position: relative; - display: block; - // left: 2px; - // top: -11px; - width: var(--checkbox-size); - height: var(--checkbox-size); - // border-width: 2px; - // border-style: solid; - // border-color: var(--md-sys-color-on-surface-variant); - content: ""; - // background-image: linear-gradient(135deg, #B1B6BE 0%, #FFF 100%); - border-radius: var(--checkbox-shape); - background-repeat: no-repeat; - background-position: center; -} - -input[type="checkbox"]:checked:after { - background-color: var(--md-sys-color-primary); - clip-path: polygon(0 48%, 0 0, 56% 0, 74% 17%, 40% 64%, 23% 50%, 14% 61%, 42% 82%, 81% 31%, 32% 0, 100% 0, 100% 100%, 0 100%, 0 49%); -} - -[type="checkbox"]:indeterminate:after { - background-color: var(--md-sys-color-primary); - clip-path: polygon(0% 0%, 0% 100%, 18% 100%, 16% 43%, 83% 42%, 83% 63%, 16% 63%, 16% 100%, 100% 100%, 100% 0%); -} - -// Disabled -input[type="checkbox"]:disabled:before { - filter: opacity(0.2); -} -input[type="checkbox"]:disabled:after { - filter: opacity(0.2); -} - -// Hover -input[type="checkbox"]:not(:disabled):checked:hover:after{ - // background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAQAAABuW59YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAB2SURBVHjaAGkAlv8A3QDyAP0A/QD+Dam3W+kCAAD8APYAAgTVZaZCGwwA5wr0AvcA+Dh+7UX/x24AqK3Wg/8nt6w4/5q71wAAVP9g/7rTXf9n/+9N+AAAtpJa/zf/S//DhP8H/wAA4gzWj2P4lsf0JP0A/wADAHB0Ngka6UmKAAAAAElFTkSuQmCC'), linear-gradient(135deg, #8BB0C2 0%,#FFF 100%); - background-color: var(--md-sys-color-primary); -} - -input[type="checkbox"]:not(:disabled):hover:after{ - // background-image: linear-gradient(135deg, #8BB0C2 0%,#FFF 100%); - border-color: #85A9BB #92C2DA #92C2DA #85A9BB; -} - -input[type="checkbox"]:not(:disabled):hover:before{ - border-color: #3D7591; -} - -/* -// Checkbox Styles -[type="checkbox"] { - // Text Label Style - + span:not(.lever) { - position: relative; - cursor: pointer; - display: inline-block; - height: var(--checkbox-size); - line-height: var(--checkbox-size); - padding-left: var(--checkbox-size); - // background-color: red; - font-size: 1rem; - user-select: none; - } - - // checkbox aspect - + span:not(.lever):before, - &:not(.filled-in) + span:not(.lever):after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: var(--checkbox-size); - height: var(--checkbox-size); - z-index: 0; - border: 2px solid var(--md-sys-color-on-surface-variant); - border-radius: 1px; - transition: .2s; - } - - &:not(.filled-in) + span:not(.lever):after { - border: 0; - transform: scale(0); - } - - &:not(:checked):disabled + span:not(.lever):before { - border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - } - - &:disabled + span:not(.lever) { - color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - } - - // Focused styles - &.tabbed:focus + span:not(.lever):after { - transform: scale(1); - border: 0; - border-radius: 50%; - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.12); - background-color: rgba(0, 0, 0, 0.12); - } -} - -[type="checkbox"]:checked { - + span:not(.lever):before { - top: -4px; - left: -5px; - width: 12px; - height: 22px; - border-top: 2px solid transparent; - border-left: 2px solid transparent; - border-right: 2px solid var(--md-sys-color-primary); - border-bottom: 2px solid var(--md-sys-color-primary); - transform: rotate(40deg); - backface-visibility: hidden; - transform-origin: 100% 100%; - } - - &:disabled + span:before { - border-right: 2px solid color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - border-bottom: 2px solid color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - } -} - -// Indeterminate checkbox -[type="checkbox"]:indeterminate { - + span:not(.lever):before { - top: -11px; - left: -12px; - width: 10px; - height: 22px; - border-top: none; - border-left: none; - border-right: 2px solid var(--md-sys-color-primary); - border-bottom: none; - transform: rotate(90deg); - backface-visibility: hidden; - transform-origin: 100% 100%; - } - - // Disabled indeterminate - &:disabled + span:not(.lever):before { - border-right: 2px solid color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - background-color: transparent; - } -} - -// Filled in Style -[type="checkbox"].filled-in { - // General - + span:not(.lever):after { - border-radius: 2px; - } - - + span:not(.lever):before, - + span:not(.lever):after { - content: ''; - left: 0; - position: absolute; - // .1s delay is for check animation - transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s; - z-index: 1; - } - - // Unchecked style - &:not(:checked) + span:not(.lever):before { - width: 0; - height: 0; - border: 3px solid transparent; - left: 6px; - top: 10px; - transform: rotateZ(37deg); - transform-origin: 100% 100%; - } - - &:not(:checked) + span:not(.lever):after { - height: 20px; - width: 20px; - background-color: transparent; - border: 2px solid var(--md-sys-color-on-surface-variant); - top: 0px; - z-index: 0; - } - - // Checked style - &:checked { - + span:not(.lever):before { - top: 0; - left: 1px; - width: 8px; - height: 13px; - border-top: 2px solid transparent; - border-left: 2px solid transparent; - border-right: 2px solid var(--md-sys-color-on-primary); - border-bottom: 2px solid var(--md-sys-color-on-primary); - transform: rotateZ(37deg); - transform-origin: 100% 100%; - } - - + span:not(.lever):after { - top: 0; - width: 20px; - height: 20px; - border: 2px solid var(--md-sys-color-primary); - background-color: var(--md-sys-color-primary); - z-index: 0; - } - } - - // Focused styles - &.tabbed:focus + span:not(.lever):after { - border-radius: 2px; - border-color: var(--md-sys-color-on-surface-variant)r; - background-color: rgba(0, 0, 0, 0.12); - } - - &.tabbed:checked:focus + span:not(.lever):after { - border-radius: 2px; - background-color: var(--md-sys-color-primary); - border-color: var(--md-sys-color-primary); - } - - // Disabled style - &:disabled:not(:checked) + span:not(.lever):before { - background-color: transparent; - border: 2px solid transparent; - } - - &:disabled:not(:checked) + span:not(.lever):after { - border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - } - - &:disabled:checked + span:not(.lever):before { - background-color: transparent; - } - - &:disabled:checked + span:not(.lever):after { - background-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); - border: none; - } -} -*/ \ No newline at end of file diff --git a/components/checkbox/checkbox.mdx b/components/checkbox/checkbox.mdx deleted file mode 100644 index 0dba9587ea..0000000000 --- a/components/checkbox/checkbox.mdx +++ /dev/null @@ -1,24 +0,0 @@ -import {Story, Meta, Source, Controls, Markdown, Primary} from '@storybook/addon-docs/blocks'; -import * as stories from './checkbox.stories'; - - - -# Checkboxes - -Use checkboxes when looking for yes or no answers. The `for` attribute is necessary to bind our custom checkbox with the input. -Add the input's `id` as the value of the `for` attribute of the label. - -## Default - - - - -## Filled-in Checkbox - - - - -## Indeterminate Checkbox - - - diff --git a/components/checkbox/checkbox.stories.ts b/components/checkbox/checkbox.stories.ts deleted file mode 100644 index 13fd7fd40b..0000000000 --- a/components/checkbox/checkbox.stories.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/html-vite'; - -export default { - title: 'Components/Checkbox' -} satisfies Meta; - -export const Default: StoryObj = { - render() { - return ` -
- -

Normal basic checkbox input

- - - - - - -

Compound Checkbox Elements

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

-
- `; - } -}; - -export const FilledIn: StoryObj = { - render() { - return ` -
-

- -

-

- -

-

- -

-

- -

-
- `; - } -}; - -export const Indeterminate: StoryObj = { - render() { - return ` -
-

- -

-

- -

-
- - - `; - } -}; diff --git a/components/chip/chip.test.mjs b/components/chip/chip.test.mjs deleted file mode 100644 index 89013d0456..0000000000 --- a/components/chip/chip.test.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { Chip } from './chip.mjs'; - -describe('chip', () => { - test('create html', () => { - const chip = new Chip().setText('John Doe'); - //console.log(list.toHTML()); - expect(chip.toHTML()).toContain('John'); - }); -}); diff --git a/components/chip/chips.mdx b/components/chip/chips.mdx deleted file mode 100644 index ae0829be73..0000000000 --- a/components/chip/chips.mdx +++ /dev/null @@ -1,33 +0,0 @@ -# Chips - -To add tags, just enter your tag text and press enter. You can delete them by clicking on the close icon or by using your delete button. - - -ATTENTION: Data-Format has changed from version 1.X.X to 2.0.0! Please update option 'data'. - -Normal: - -
- -Set initial tags: - -
- -Use placeholders and override hint texts: - -
- -Use autocomplete with chips: - -
- -```html - -
-
-
-
- -
- -
\ No newline at end of file diff --git a/components/chip/chipsSpec.js b/components/chip/chipsSpec.js deleted file mode 100644 index d4a0cd6980..0000000000 --- a/components/chip/chipsSpec.js +++ /dev/null @@ -1,165 +0,0 @@ -describe('Chips', () => { - const fixture = `
-
-
-
-
-
`; - - beforeEach(() => { - XloadHtml(fixture); - M.Chips.init(document.querySelector('.chips')); - M.Chips.init(document.querySelector('.chips-initial'), { - data: [ - { id: 12, text: 'Apple' }, - { id: 13, text: 'Microsoft' }, - { - id: 42, - text: 'Google', - image: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' - } - ] - }); - M.Chips.init(document.querySelector('.chips.input-field'), { - allowUserInput: true - }); - M.Chips.init(document.querySelector('.chips-initial.input-field'), { - allowUserInput: true, - data: [ - { id: 12, text: 'Apple' }, - { id: 13, text: 'Microsoft' }, - { - id: 42, - text: 'Google', - image: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' - } - ] - }); - M.Chips.init(document.querySelector('.chips-placeholder.input-field'), { - allowUserInput: true, - placeholder: 'Enter a tag', - secondaryPlaceholder: '+Tag' - }); - M.Chips.init(document.querySelector('.chips-autocomplete.input-field'), { - allowUserInput: true, - autocompleteOptions: { - data: [ - { id: 12, text: 'Apple' }, - { id: 13, text: 'Microsoft' }, - { id: 42, text: 'Google' } - ] - } - }); - }); - afterEach(() => XunloadFixtures()); - - describe('chips plugin', () => { - let chips, chipsUserInput, input; - - it('should work with multiple initializations', () => { - chips = document.querySelector('.chips'); - M.Chips.init(chips); - M.Chips.init(chips); - M.Chips.init(chips); - chipsUserInput = document.querySelector('.chips.input-field'); - M.Chips.init(chips, { allowUserInput: true }); - input = chipsUserInput.querySelectorAll('input'); - expect(input.length).toEqual(1, 'Should dynamically generate chips structure.'); - }); - - it('should be able to add chip', (done) => { - chips = document.querySelector('.chips.input-field'); - input = chips.querySelector('input'); - input.value = 'one'; - keydown(input, 13); - setTimeout(() => { - const numChips = chips.querySelectorAll('.chip').length; - const oneChip = chips.querySelector('.chip'); - expect(numChips).toEqual(1, 'one chip should have been added'); - for (let i = oneChip.children.length - 1; i >= 0; i--) { - oneChip.children[i].remove(); - } - expect(oneChip.innerText).toEqual('one', 'the chip should have value "one"'); - done(); - }, 100); // 400 - }); - - it('should be able to delete chip', (done) => { - chips = document.querySelector('.chips.chips-initial.input-field'); - let numChips = chips.querySelectorAll('.chip').length; - expect(numChips).toEqual(3, '3 initial chips should have been added'); - const chipCloseButton = chips.querySelectorAll('.chip .close'); - expect(chipCloseButton.length).toEqual(3, 'expected all chips to have close button'); - click(chipCloseButton[0]); - setTimeout(() => { - numChips = chips.querySelectorAll('.chip').length; - expect(numChips).toEqual(2, 'one chip should have been deleted'); - done(); - }, 100); // 400 - }); - - it('should have working callbacks', (done) => { - chips = document.querySelector('.chips.input-field'); - let chipWasAdded = false; - let chipAddedElem = null; - let chipSelect = false; - let chipSelected = null; - let chipDelete = false; - let chipDeleted = null; - - M.Chips.init(chips, { - allowUserInput: true, - data: [{ id: 'One' }, { id: 'Two' }, { id: 'Three' }], - onChipAdd: (chipsEl, chipEl) => { - chipAddedElem = chipEl; - chipWasAdded = true; - }, - onChipSelect: (chipsEl, chipEl) => { - chipSelected = chipEl; - chipSelect = true; - }, - onChipDelete: (chipsEl, chipEl) => { - chipDeleted = chipEl; - chipDelete = true; - } - }); - - input = chips.querySelector('input'); - input.value = 'Four'; - expect(chipWasAdded).toEqual(false, 'callback not yet fired'); - expect(chipSelect).toEqual(false, 'callback not yet fired'); - expect(chipDelete).toEqual(false, 'callback not yet fired'); - keydown(input, 13); - - setTimeout(() => { - expect(chipWasAdded).toEqual(true, 'add callback fired'); - expect(chipAddedElem.childNodes[0].nodeValue).toEqual( - 'Four', - 'add callback provides correct chip element' - ); - click(chips.querySelectorAll('.chip')[1]); - setTimeout(() => { - expect(chipSelect).toEqual(true, 'select callback fired'); - expect(chipSelected.childNodes[0].nodeValue).toEqual( - 'Two', - 'select callback provides correct chip element' - ); - click(chips.querySelectorAll('.close')[2]); - setTimeout(() => { - expect(chipDelete).toEqual(true, 'delete callback fired'); - expect(chipDeleted.childNodes[0].nodeValue).toEqual( - 'Three', - 'add callback provides correct chip element' - ); - done(); - }, 100); - }, 100); - }, 100); - }); - }); - - // describe("Chips autocomplete", () => { - // }); -}); diff --git a/components/collapsible/collapsibleSpec.js b/components/collapsible/collapsibleSpec.js deleted file mode 100644 index bc2e26d01b..0000000000 --- a/components/collapsible/collapsibleSpec.js +++ /dev/null @@ -1,240 +0,0 @@ -describe('Collapsible Plugin:', () => { - let collapsible, accordion, popout, expandable, expandablePreselect; - - const fixture = ` -
    -
  • -
    filter_dramaFirst
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    placeSecond
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    whatshotThird
    -

    Lorem ipsum dolor sit amet.

    -
  • -
- - -
    -
  • -
    filter_dramaFirst
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    placeSecond
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    whatshotThird
    -

    Lorem ipsum dolor sit amet.

    -
  • -
- -
    -
  • -
    filter_dramaFirst
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    placeSecond
    -

    Lorem ipsum dolor sit amet.

    -
  • -
  • -
    whatshotThird
    -

    Lorem ipsum dolor sit amet.

    -
  • -
`; - - beforeEach(() => { - XloadHtml(fixture); - collapsible = document.querySelectorAll('.collapsible'); - expandable = document.querySelector('.expandable'); - expandablePreselect = document.querySelector('.expandable-preselected'); - accordion = document.querySelector('.accordion'); - popout = document.querySelector('.popout'); - M.Collapsible.init(collapsible, { inDuration: 0, outDuration: 0 }); - M.Collapsible.init(expandable, { accordion: false, inDuration: 0, outDuration: 0 }); - M.Collapsible.init(expandablePreselect, { accordion: false, inDuration: 0, outDuration: 0 }); - }); - - afterEach(() => XunloadFixtures()); - - describe('collapsible', () => { - it('should open all items, keeping all open', (done) => { - // Collapsible body height should be 0 on start when hidden. - const headers = expandable.querySelectorAll('.collapsible-header'); - const bodies = expandable.querySelectorAll('.collapsible-body'); - for (let i = 0; i < bodies.length; i++) { - expect(bodies[i]).hasMaxHeightZero( - 'because collapsible bodies should be hidden initially.' - ); - //TODO replace with alternative for deprecated jasmine-jquery - } - // Collapsible body height should be > 0 after being opened. - for (let i = 0; i < headers.length; i++) { - click(headers[i]); - } - setTimeout(() => { - for (let i = 0; i < bodies.length; i++) { - expect(bodies[i]).notHasMaxHeightZero( - 'because collapsible bodies not visible after being opened.' - ); //TODO replace with alternative for deprecated jasmine-jquery - } - done(); - }, 10); - }); - - it('should allow preopened sections', () => { - const bodies = expandablePreselect.querySelectorAll('.collapsible-body'); - for (let i = 0; i < bodies.length; i++) { - const headerLi = bodies[i].parentNode; - if (i === 1) { - expect(headerLi).toHaveClass( - 'active', - 'because collapsible header should have active class to be preselected.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(bodies[i]).notHasMaxHeightZero( - 'because collapsible bodies should be visible if preselected.' - ); //TODO replace with alternative for deprecated jasmine-jquery - } else { - expect(bodies[i]).hasMaxHeightZero( - 'because collapsible bodies should be hidden initially.' - ); //TODO replace with alternative for deprecated jasmine-jquery - } - } - }); - - it('should open and close programmatically with callbacks', (done) => { - let openCallback = false; - let closeCallback = false; - M.Collapsible.init(expandable, { - accordion: false, - onOpenStart: () => { - openCallback = true; - }, - onCloseStart: () => { - closeCallback = true; - }, - inDuration: 0, - outDuration: 0 - }); - const bodies = expandable.querySelectorAll('.collapsible-body'); - expect(openCallback).toEqual(false, 'because open callback not yet fired'); - expect(closeCallback).toEqual(false, 'because close callback not yet fired'); - for (let i = 0; i < bodies.length; i++) { - //TODO replace with alternative for deprecated jasmine-jquery - expect(bodies[i]).hasMaxHeightZero( - 'because collapsible bodies should be hidden initially.' - ); - const collapsibleInstance = M.Collapsible.getInstance(bodies[i].parentNode.parentNode); - collapsibleInstance.open(i); - } - expect(openCallback).toEqual(true, 'because open callback fired'); - setTimeout(() => { - for (let i = 0; i < bodies.length; i++) { - expect(bodies[i]).notHasMaxHeightZero( - 'because collapsible bodies should be visible after being opened.' - ); //TODO replace with alternative for deprecated jasmine-jquery - M.Collapsible.getInstance(bodies[i].parentNode.parentNode).close(i); - } - expect(closeCallback).toEqual(true, 'because close callback fired'); - setTimeout(() => { - for (let i = 0; i < bodies.length; i++) { - expect(bodies[i]).hasMaxHeightZero( - 'because collapsible bodies should be hidden after close.' - ); //TODO replace with alternative for deprecated jasmine-jquery - } - done(); - }, 10); - }, 10); - }); - }); - - describe('accordion', () => { - it('should open first and second items, keeping only second open', (done) => { - // Collapsible body height should be 0 on start when hidden. - const firstHeader = accordion.querySelector('.collapsible-header'); - const firstBody = accordion.querySelector('.collapsible-body'); - const secondHeader = accordion.querySelectorAll('.collapsible-header')[1]; - const secondBody = accordion.querySelectorAll('.collapsible-body')[1]; - expect(firstBody).hasMaxHeightZero('because accordion bodies should be hidden initially.'); - expect(secondBody).hasMaxHeightZero('because accordion bodies should be hidden initially.'); - // Collapsible body height should be > 0 after being opened. - firstHeader.click(); - setTimeout(() => { - expect(firstBody).notHasMaxHeightZero( - 'because accordion bodies not visible after being opened.' - ); - click(secondHeader); - setTimeout(() => { - expect(firstBody).hasMaxHeightZero( - 'because accordion bodies should be hidden when another item is opened.' - ); - expect(secondBody).notHasMaxHeightZero( - 'because accordion bodies not visible after being opened.' - ); - done(); - }, 10); - }, 10); - }); - }); - - describe('popout', () => { - it('should open first and popout', (done) => { - // Collapsible body height should be 0 on start when hidden. - const listItems = popout.querySelectorAll('li'); - const firstHeader = popout.querySelector('.collapsible-header'); - const firstBody = popout.querySelector('.collapsible-body'); - expect(firstBody).hasMaxHeightZero('because accordion bodies should be hidden initially.'); - // Expect margin to be > 0 because not popped out. - for (let i = 0; i < listItems.length; i++) { - const listItemStyles = getComputedStyle(listItems[i]); - const marginLeft = parseInt(listItemStyles.getPropertyValue('margin-left')); - const marginRight = parseInt(listItemStyles.getPropertyValue('margin-right')); - expect(marginLeft).toBeGreaterThan( - 0, - 'because closed popout items should have horizontal margins.' - ); - expect(marginRight).toBeGreaterThan( - 0, - 'because closed popout items should have horizontal margins.' - ); - } - // expect margin to be 0 because popped out. - click(firstHeader); - setTimeout(() => { - const firstStyles = getComputedStyle(listItems[0]); - const firstMarginLeft = parseInt(firstStyles.getPropertyValue('margin-left')); - const firstMarginRight = parseInt(firstStyles.getPropertyValue('margin-right')); - expect(firstMarginLeft).toEqual( - 0, - 'because opened popout items should have no horizontal margins.' - ); - expect(firstMarginRight).toEqual( - 0, - 'because opened popout items should have no horizontal margins.' - ); - expect(firstBody).notHasMaxHeightZero( - 'because accordion bodies not visible after being opened.' - ); - done(); - }, 300); - }); - }); -}); diff --git a/components/dialog/_materialbox.scss b/components/dialog/_materialbox.scss deleted file mode 100644 index 15f8454160..0000000000 --- a/components/dialog/_materialbox.scss +++ /dev/null @@ -1,42 +0,0 @@ -.materialboxed { - display: block; - cursor: zoom-in; - position: relative; - transition: opacity .4s; - -webkit-backface-visibility: hidden; - - &:hover { - &:not(.active) { - opacity: .8; - } - } - &.active { - cursor: zoom-out; - } -} - -#materialbox-overlay { - position:fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: var(--md-sys-color-background); - z-index: 1000; - will-change: opacity; -} - -.materialbox-caption { - position: fixed; - display: none; - color: var(--md-sys-color-on-background); - line-height: 50px; - bottom: 0; - left: 0; - width: 100%; - text-align: center; - padding: 0% 15%; - height: 50px; - z-index: 1000; - -webkit-font-smoothing: antialiased; -} \ No newline at end of file diff --git a/components/dialog/materialbox.ts b/components/dialog/materialbox.ts deleted file mode 100644 index aed6235ebd..0000000000 --- a/components/dialog/materialbox.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { Utils } from '../../src/utils'; -import { BaseOptions, Component, InitElements, MElement } from '../../src/component'; - -export interface MaterialboxOptions extends BaseOptions { - /** - * Transition in duration in milliseconds. - * @default 275 - */ - inDuration: number; - /** - * Transition out duration in milliseconds. - * @default 200 - */ - outDuration: number; - /** - * Callback function called before materialbox is opened. - * @default null - */ - onOpenStart: (el: Element) => void; - /** - * Callback function called after materialbox is opened. - * @default null - */ - onOpenEnd: (el: Element) => void; - /** - * Callback function called before materialbox is closed. - * @default null - */ - onCloseStart: (el: Element) => void; - /** - * Callback function called after materialbox is closed. - * @default null - */ - onCloseEnd: (el: Element) => void; -} - -const _defaults: MaterialboxOptions = { - inDuration: 275, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null -}; - -export class Materialbox extends Component { - /** If the materialbox overlay is showing. */ - overlayActive: boolean; - /** If the materialbox is no longer being animated. */ - doneAnimating: boolean; - /** Caption, if specified. */ - caption: string; - /** Original width of image. */ - originalWidth: number; - /** Original height of image. */ - originalHeight: number; - private originInlineStyles: string; - private placeholder: HTMLElement; - private _changedAncestorList: HTMLElement[]; - private newHeight: number; - private newWidth: number; - private windowWidth: number; - private windowHeight: number; - private attrWidth: string; - private attrHeight: string; - private _overlay: HTMLElement; - private _photoCaption: HTMLElement; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, Materialbox); - this.el['M_Materialbox'] = this; - - this.options = { - ...Materialbox.defaults, - ...options - }; - - this.overlayActive = false; - this.doneAnimating = true; - this.placeholder = document.createElement('div'); - this.placeholder.classList.add('material-placeholder'); - this.originalWidth = 0; - this.originalHeight = 0; - this.originInlineStyles = this.el.getAttribute('style'); - this.caption = this.el.getAttribute('data-caption') || ''; - this.el.tabIndex = 0; - // Wrap - this.el.before(this.placeholder); - this.placeholder.append(this.el); - this._setupEventHandlers(); - } - - static get defaults(): MaterialboxOptions { - return _defaults; - } - - /** - * Initializes instance of MaterialBox. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Materialbox; - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Materialbox[]; - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): Materialbox | Materialbox[] { - return super.init(els, options, Materialbox); - } - - static getInstance(el: HTMLElement): Materialbox { - return el['M_Materialbox']; - } - - destroy() { - this._removeEventHandlers(); - this.el['M_Materialbox'] = undefined; - // Unwrap image - //this.placeholder.after(this.el).remove(); - this.placeholder.remove(); - this.el.removeAttribute('style'); - } - - private _setupEventHandlers() { - this.el.addEventListener('click', this._handleMaterialboxClick); - this.el.addEventListener('keypress', this._handleMaterialboxKeypress); - } - - private _removeEventHandlers() { - this.el.removeEventListener('click', this._handleMaterialboxClick); - this.el.removeEventListener('keypress', this._handleMaterialboxKeypress); - } - - private _handleMaterialboxClick = () => { - this._handleMaterialboxToggle(); - }; - - private _handleMaterialboxKeypress = (e: KeyboardEvent) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleMaterialboxToggle(); - } - }; - - private _handleMaterialboxToggle = () => { - // If already modal, return to original - if (this.doneAnimating === false || (this.overlayActive && this.doneAnimating)) this.close(); - else this.open(); - }; - - private _handleWindowScroll = () => { - if (this.overlayActive) this.close(); - }; - - private _handleWindowResize = () => { - if (this.overlayActive) this.close(); - }; - - private _handleWindowEscape = (e: KeyboardEvent) => { - if (Utils.keys.ESC.includes(e.key) && this.doneAnimating && this.overlayActive) this.close(); - }; - - private _makeAncestorsOverflowVisible() { - this._changedAncestorList = []; - let ancestor = this.placeholder.parentNode; - while (ancestor !== null && ancestor !== document) { - const curr = ancestor; - if (curr.style.overflow !== 'visible') { - curr.style.overflow = 'visible'; - this._changedAncestorList.push(curr); - } - ancestor = ancestor.parentNode; - } - } - - private _offset(el: HTMLElement) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - private _updateVars(): void { - this.windowWidth = window.innerWidth; - this.windowHeight = window.innerHeight; - this.caption = this.el.getAttribute('data-caption') || ''; - } - - // Image - private _animateImageIn(): void { - this.el.style.maxHeight = this.newHeight.toString() + 'px'; - this.el.style.maxWidth = this.newWidth.toString() + 'px'; - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.height = this.originalHeight + 'px'; - this.el.style.width = this.originalWidth + 'px'; - setTimeout(() => { - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.newHeight + 'px'; - this.el.style.width = this.newWidth + 'px'; - this.el.style.left = - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2 + - 'px'; - - this.el.style.top = - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2 + - 'px'; - }, 1); - - setTimeout(() => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') this.options.onOpenEnd.call(this, this.el); - }, duration); - - /* - anim({ - targets: this.el, // image - height: [this.originalHeight, this.newHeight], - width: [this.originalWidth, this.newWidth], - left: - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2, - top: - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2, - - duration: this.options.inDuration, - easing: 'easeOutQuad', - complete: () => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') this.options.onOpenEnd.call(this, this.el); - } - }); - */ - } - private _animateImageOut(): void { - const duration = this.options.outDuration; - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.originalWidth + 'px'; - this.el.style.width = this.originalWidth + 'px'; - this.el.style.left = '0'; - this.el.style.top = '0'; - setTimeout(() => { - this.placeholder.style.height = ''; - this.placeholder.style.width = ''; - this.placeholder.style.position = ''; - this.placeholder.style.top = ''; - this.placeholder.style.left = ''; - // Revert to width or height attribute - if (this.attrWidth) this.el.setAttribute('width', this.attrWidth.toString()); - if (this.attrHeight) this.el.setAttribute('height', this.attrHeight.toString()); - this.el.removeAttribute('style'); - if (this.originInlineStyles) this.el.setAttribute('style', this.originInlineStyles); - // Remove class - this.el.classList.remove('active'); - this.doneAnimating = true; - // Remove overflow overrides on ancestors - this._changedAncestorList.forEach((anchestor) => (anchestor.style.overflow = '')); - // onCloseEnd callback - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - - // Caption - private _addCaption(): void { - this._photoCaption = document.createElement('div'); - this._photoCaption.classList.add('materialbox-caption'); - this._photoCaption.innerText = this.caption; - document.body.append(this._photoCaption); - this._photoCaption.style.display = 'inline'; - // Animate - this._photoCaption.style.transition = 'none'; - this._photoCaption.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '1'; - }, 1); - } - private _removeCaption(): void { - const duration = this.options.outDuration; - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '0'; - setTimeout(() => { - this._photoCaption.remove(); - }, duration); - } - - // Overlay - private _addOverlay(): void { - this._overlay = document.createElement('div'); - this._overlay.id = 'materialbox-overlay'; - this._overlay.addEventListener( - 'click', - () => { - if (this.doneAnimating) this.close(); - }, - { once: true } - ); - - // Put before in origin image to preserve z-index layering. - this.el.before(this._overlay); - - // Set dimensions if needed - const overlayOffset = this._overlay.getBoundingClientRect(); - this._overlay.style.width = this.windowWidth + 'px'; - this._overlay.style.height = this.windowHeight + 'px'; - this._overlay.style.left = -1 * overlayOffset.left + 'px'; - this._overlay.style.top = -1 * overlayOffset.top + 'px'; - - // Animate - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '1'; - }, 1); - } - private _removeOverlay(): void { - const duration = this.options.outDuration; - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '0'; - setTimeout(() => { - this.overlayActive = false; - this._overlay.remove(); - }, duration); - } - - /** - * Open materialbox. - */ - open = () => { - this._updateVars(); - this.originalWidth = this.el.getBoundingClientRect().width; - this.originalHeight = this.el.getBoundingClientRect().height; - // Set states - this.doneAnimating = false; - this.el.classList.add('active'); - this.overlayActive = true; - // onOpenStart callback - if (typeof this.options.onOpenStart === 'function') - this.options.onOpenStart.call(this, this.el); - // Set positioning for placeholder - this.placeholder.style.width = this.placeholder.getBoundingClientRect().width + 'px'; - this.placeholder.style.height = this.placeholder.getBoundingClientRect().height + 'px'; - this.placeholder.style.position = 'relative'; - this.placeholder.style.top = '0'; - this.placeholder.style.left = '0'; - this._makeAncestorsOverflowVisible(); - // Set css on origin - this.el.style.position = 'absolute'; - this.el.style.zIndex = '1000'; - this.el.style.willChange = 'left, top, width, height'; - // Change from width or height attribute to css - this.attrWidth = this.el.getAttribute('width'); - this.attrHeight = this.el.getAttribute('height'); - if (this.attrWidth) { - this.el.style.width = this.attrWidth + 'px'; - this.el.removeAttribute('width'); - } - if (this.attrHeight) { - this.el.style.width = this.attrHeight + 'px'; - this.el.removeAttribute('height'); - } - this._addOverlay(); - // Add and animate caption if it exists - if (this.caption !== '') this._addCaption(); - // Resize Image - const widthPercent = this.originalWidth / this.windowWidth; - const heightPercent = this.originalHeight / this.windowHeight; - this.newWidth = 0; - this.newHeight = 0; - if (widthPercent > heightPercent) { - // Width first - const ratio = this.originalHeight / this.originalWidth; - this.newWidth = this.windowWidth * 0.9; - this.newHeight = this.windowWidth * 0.9 * ratio; - } else { - // Height first - const ratio = this.originalWidth / this.originalHeight; - this.newWidth = this.windowHeight * 0.9 * ratio; - this.newHeight = this.windowHeight * 0.9; - } - this._animateImageIn(); - // Handle Exit triggers - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleWindowResize); - window.addEventListener('keyup', this._handleWindowEscape); - }; - - /** - * Close materialbox. - */ - close = () => { - this._updateVars(); - this.doneAnimating = false; - // onCloseStart callback - if (typeof this.options.onCloseStart === 'function') - this.options.onCloseStart.call(this, this.el); - //anim.remove(this.el); - //anim.remove(this._overlay); - //if (this.caption !== '') anim.remove(this._photoCaption); - // disable exit handlers - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleWindowResize); - window.removeEventListener('keyup', this._handleWindowEscape); - this._removeOverlay(); - this._animateImageOut(); - if (this.caption !== '') this._removeCaption(); - }; -} diff --git a/components/dialog/materialboxSpec.js b/components/dialog/materialboxSpec.js deleted file mode 100644 index 3c544d14ed..0000000000 --- a/components/dialog/materialboxSpec.js +++ /dev/null @@ -1,37 +0,0 @@ -describe('Materialbox:', () => { - const fixture = `
- -
`; - - beforeEach(() => XloadHtml(fixture)); - afterEach(() => XunloadFixtures()); - - describe('Materialbox opens correctly with transformed ancestor', () => { - it('Opens a correctly placed overlay when clicked', (done) => { - const transformMaterialbox = document.querySelector('#transformTest'); - M.Materialbox.init(document.querySelector('.materialboxed'), { - inDuration: 0, - outDuration: 0 - }); - // Mouse click - click(transformMaterialbox.querySelector('.materialboxed')); - setTimeout(() => { - // Check overlay is attached - const overlay = transformMaterialbox.querySelector('#materialbox-overlay'); - const overlayRect = overlay.getBoundingClientRect(); - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - expect(overlay).toExist('because it is generated on init'); - expect(overlay).toBeVisible('because materialbox was clicked'); - expect(overlayRect.top).toEqual(0); - expect(overlayRect.left).toEqual(0); - expect(overlayRect.width).toEqual(windowWidth); - expect(overlayRect.height).toEqual(windowHeight); - done(); - }, 10); - }); - }); -}); diff --git a/components/dialog/readme.md b/components/dialog/readme.md deleted file mode 100644 index 0f8badda10..0000000000 --- a/components/dialog/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -# Dialog - -*THIS SECTION IS FOR DEMONSTRATION ONLY* - -Also called Modal. \ No newline at end of file diff --git a/components/dropdown/dropdown.ts b/components/dropdown/dropdown.ts deleted file mode 100644 index b40e82eb7a..0000000000 --- a/components/dropdown/dropdown.ts +++ /dev/null @@ -1,653 +0,0 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement, Openable } from '../../src/component'; - -export interface DropdownOptions extends BaseOptions { - /** - * Defines the edge the menu is aligned to. - * @default 'left' - */ - alignment: 'left' | 'right'; - /** - * If true, automatically focus dropdown el for keyboard. - * @default true - */ - autoFocus: boolean; - /** - * If true, constrainWidth to the size of the dropdown activator. - * @default true - */ - constrainWidth: boolean; - /** - * Provide an element that will be the bounding container of the dropdown. - * @default null - */ - container: Element; - /** - * If false, the dropdown will show below the trigger. - * @default true - */ - coverTrigger: boolean; - /** - * If true, close dropdown on item click. - * @default true - */ - closeOnClick: boolean; - /** - * If true, the dropdown will open on hover. - * @default false - */ - hover: boolean; - /** - * The duration of the transition enter in milliseconds. - * @default 150 - */ - inDuration: number; - /** - * The duration of the transition out in milliseconds. - * @default 250 - */ - outDuration: number; - /** - * Function called when dropdown starts entering. - * @default null - */ - onOpenStart: (el: HTMLElement) => void; - /** - * Function called when dropdown finishes entering. - * @default null - */ - onOpenEnd: (el: HTMLElement) => void; - /** - * Function called when dropdown starts exiting. - * @default null - */ - onCloseStart: (el: HTMLElement) => void; - /** - * Function called when dropdown finishes exiting. - * @default null - */ - onCloseEnd: (el: HTMLElement) => void; - /** - * Function called when item is clicked. - * @default null - */ - onItemClick: (el: HTMLLIElement) => void; -} - -const _defaults: DropdownOptions = { - alignment: 'left', - autoFocus: true, - constrainWidth: true, - container: null, - coverTrigger: true, - closeOnClick: true, - hover: false, - inDuration: 150, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - onItemClick: null -}; - -export class Dropdown extends Component implements Openable { - static _dropdowns: Dropdown[] = []; - /** ID of the dropdown element. */ - id: string; - /** The DOM element of the dropdown. */ - dropdownEl: HTMLElement; - /** If the dropdown is open. */ - isOpen: boolean; - /** If the dropdown content is scrollable. */ - isScrollable: boolean; - isTouchMoving: boolean; - /** The index of the item focused. */ - focusedIndex: number; - filterQuery: string[]; - filterTimeout: NodeJS.Timeout | number; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, Dropdown); - this.el['M_Dropdown'] = this; - - Dropdown._dropdowns.push(this); - this.id = Utils.getIdFromTrigger(el); - this.dropdownEl = document.getElementById(this.id); - - this.options = { - ...Dropdown.defaults, - ...options - }; - - this.isOpen = false; - this.isScrollable = false; - this.isTouchMoving = false; - this.focusedIndex = -1; - this.filterQuery = []; - this.el.ariaExpanded = 'false'; - - // Move dropdown-content after dropdown-trigger - this._moveDropdownToElement(); - this._makeDropdownFocusable(); - this._setupEventHandlers(); - } - - static get defaults(): DropdownOptions { - return _defaults; - } - - /** - * Initializes instance of Dropdown. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Dropdown; - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Dropdown[]; - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): Dropdown | Dropdown[] { - return super.init(els, options, Dropdown); - } - - static getInstance(el: HTMLElement): Dropdown { - return el['M_Dropdown']; - } - - destroy() { - this._resetDropdownStyles(); - this._removeEventHandlers(); - Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); - this.el['M_Dropdown'] = undefined; - } - - _setupEventHandlers() { - // Trigger keydown handler - this.el.addEventListener('keydown', this._handleTriggerKeydown); - // Item click handler - this.dropdownEl?.addEventListener('click', this._handleDropdownClick); - // Hover event handlers - if (this.options.hover) { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave); - // Click event handlers - } else { - this.el.addEventListener('click', this._handleClick); - } - } - - _removeEventHandlers() { - this.el.removeEventListener('keydown', this._handleTriggerKeydown); - this.dropdownEl.removeEventListener('click', this._handleDropdownClick); - if (this.options.hover) { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave); - } else { - this.el.removeEventListener('click', this._handleClick); - } - } - - _setupTemporaryEventHandlers() { - document.body.addEventListener('click', this._handleDocumentClick); - document.body.addEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); - window.addEventListener('resize', this._handleWindowResize); - } - - _removeTemporaryEventHandlers() { - document.body.removeEventListener('click', this._handleDocumentClick); - document.body.removeEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); - window.removeEventListener('resize', this._handleWindowResize); - } - - _handleClick = (e: MouseEvent) => { - e.preventDefault(); - //this._moveDropdown((e.target).closest('li')); - if (this.isOpen) { - this.close(); - } else { - this.open(); - } - }; - - _handleMouseEnter = () => { - //this._moveDropdown((e.target).closest('li')); - this.open(); - }; - - _handleMouseLeave = (e: MouseEvent) => { - const toEl = e.relatedTarget as HTMLElement; - const leaveToDropdownContent = !!toEl.closest('.dropdown-content'); - let leaveToActiveDropdownTrigger = false; - const closestTrigger = toEl.closest('.dropdown-trigger'); - if (closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen) { - leaveToActiveDropdownTrigger = true; - } - // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content - if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { - this.close(); - } - }; - - _handleDocumentClick = (e: MouseEvent) => { - const target = e.target; - if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) { - // isTouchMoving to check if scrolling on mobile. - this.close(); - } else if (!target.closest('.dropdown-content')) { - // Do this one frame later so that if the element clicked also triggers _handleClick - // For example, if a label for a select was clicked, that we don't close/open the dropdown - setTimeout(() => { - if (this.isOpen) { - this.close(); - } - }, 0); - } - this.isTouchMoving = false; - }; - - _handleTriggerKeydown = (e: KeyboardEvent) => { - // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown - const arrowDownOrEnter = - Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key); - if (arrowDownOrEnter && !this.isOpen) { - e.preventDefault(); - this.open(); - } - }; - - _handleDocumentTouchmove = (e: TouchEvent) => { - const target = e.target; - if (target.closest('.dropdown-content')) { - this.isTouchMoving = true; - } - }; - - _handleDropdownClick = (e: MouseEvent) => { - // onItemClick callback - if (typeof this.options.onItemClick === 'function') { - const itemEl = (e.target).closest('li'); - this.options.onItemClick.call(this, itemEl); - } - }; - - _handleDropdownKeydown = (e: KeyboardEvent) => { - const arrowUpOrDown = - Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key); - if (Utils.keys.TAB.includes(e.key)) { - e.preventDefault(); - this.close(); - } - // Navigate down dropdown list - else if (arrowUpOrDown && this.isOpen) { - e.preventDefault(); - const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1; - let newFocusedIndex = this.focusedIndex; - let hasFoundNewIndex = false; - do { - newFocusedIndex = newFocusedIndex + direction; - if ( - !!this.dropdownEl.children[newFocusedIndex] && - (this.dropdownEl.children[newFocusedIndex]).tabIndex !== -1 - ) { - hasFoundNewIndex = true; - break; - } - } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); - - if (hasFoundNewIndex) { - // Remove active class from old element - if (this.focusedIndex >= 0) - this.dropdownEl.children[this.focusedIndex].classList.remove('active'); - this.focusedIndex = newFocusedIndex; - this._focusFocusedItem(); - } - } - // ENTER selects choice on focused item - else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) { - // Search for and ` + - ''); - } - renderRow(days, isRTL, isRowSelected) { - return ('' + - (isRTL ? days.reverse() : days).join('') + - ''); - } - renderTable(opts, data, randId) { - return ('
' + - this.renderHead(opts) + - this.renderBody(data) + - '
'); - } - renderHead(opts) { - const arr = []; - let i; - for (i = 0; i < 7; i++) { - arr.push(`${this.renderDayName(opts, i, true)}`); - } - return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; - } - renderBody(rows) { - return '' + rows.join('') + ''; - } - renderTitle(instance, c, year, month, refYear, randId) { - const opts = this.options, isMinYear = year === opts.minYear, isMaxYear = year === opts.maxYear; - let i, j, arr = [], html = '
', prev = true, next = true; - for (i = 0; i < 12; i++) { - arr.push(''); - } - const monthHtml = ''; - if (Array.isArray(opts.yearRange)) { - i = opts.yearRange[0]; - j = opts.yearRange[1] + 1; - } - else { - i = year - opts.yearRange; - j = 1 + year + opts.yearRange; - } - for (arr = []; i < j && i <= opts.maxYear; i++) { - if (i >= opts.minYear) { - arr.push(``); - } - } - if (opts.yearRangeReverse) - arr.reverse(); - const yearHtml = ``; - const leftArrow = ''; - html += ``; - html += '
'; - if (opts.showMonthAfterYear) { - html += yearHtml + monthHtml; - } - else { - html += monthHtml + yearHtml; - } - html += '
'; - if (isMinYear && (month === 0 || opts.minMonth >= month)) { - prev = false; - } - if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { - next = false; - } - const rightArrow = ''; - html += ``; - return (html += '
'); - } - // refresh HTML - draw() { - const opts = this.options, minYear = opts.minYear, maxYear = opts.maxYear, minMonth = opts.minMonth, maxMonth = opts.maxMonth; - let html = ''; - if (this._y <= minYear) { - this._y = minYear; - if (!isNaN(minMonth) && this._m < minMonth) { - this._m = minMonth; - } - } - if (this._y >= maxYear) { - this._y = maxYear; - if (!isNaN(maxMonth) && this._m > maxMonth) { - this._m = maxMonth; - } - } - const randId = 'datepicker-title-' + - Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .substr(0, 2); - for (let c = 0; c < 1; c++) { - if (!this.options.isDateRange) { - this._renderDateDisplay(this.date); - } - else { - this._renderDateDisplay(this.date, this.endDate); - } - html += - this.renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId); - } - this.destroySelects(); - this.calendarEl.innerHTML = html; - // Init Materialize Select - const yearSelect = this.calendarEl.querySelector('.orig-select-year'); - const monthSelect = this.calendarEl.querySelector('.orig-select-month'); - // @todo fix accessibility @see https://github.com/materializecss/materialize/issues/522 - FormSelect.init(yearSelect, { - classes: 'select-year', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - FormSelect.init(monthSelect, { - classes: 'select-month', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - // Add change handlers for select - yearSelect.addEventListener('change', this._handleYearChange); - monthSelect.addEventListener('change', this._handleMonthChange); - if (typeof this.options.onDraw === 'function') { - this.options.onDraw.call(this); - } - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - this.el.addEventListener('keydown', this._handleInputKeydown); - this.el.addEventListener('change', this._handleInputChange); - this.calendarEl.addEventListener('click', this._handleCalendarClick); - } - _setupVariables() { - const template = document.createElement('template'); - template.innerHTML = Datepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.calendarEl = this.containerEl.querySelector('.datepicker-calendar'); - this.yearTextEl = this.containerEl.querySelector('.year-text'); - this.dateTextEl = this.containerEl.querySelector('.date-text'); - this.footer = this.containerEl.querySelector('.datepicker-footer'); - this.formats = { - d: (date) => { - return date.getDate(); - }, - dd: (date) => { - const d = date.getDate(); - return (d < 10 ? '0' : '') + d; - }, - ddd: (date) => { - return this.options.i18n.weekdaysShort[date.getDay()]; - }, - dddd: (date) => { - return this.options.i18n.weekdays[date.getDay()]; - }, - m: (date) => { - return date.getMonth() + 1; - }, - mm: (date) => { - const m = date.getMonth() + 1; - return (m < 10 ? '0' : '') + m; - }, - mmm: (date) => { - return this.options.i18n.monthsShort[date.getMonth()]; - }, - mmmm: (date) => { - return this.options.i18n.months[date.getMonth()]; - }, - yy: (date) => { - return ('' + date.getFullYear()).slice(2); - }, - yyyy: (date) => { - return date.getFullYear(); - } - }; - } - _pickerSetup() { - if (!this.options.defaultDate) { - this.options.defaultDate = new Date(Date.parse(this.el.value)); - } - const defDate = this.options.defaultDate; - if (Datepicker._isDate(defDate)) { - if (this.options.setDefaultDate) { - this.setDate(defDate, true); - this.setInputValue(this.el, defDate); - } - else { - this.gotoDate(defDate); - } - } - else { - this.gotoDate(new Date()); - } - if (this.options.isDateRange) { - this.multiple = true; - const defEndDate = this.options.defaultEndDate; - if (Datepicker._isDate(defEndDate)) { - if (this.options.setDefaultEndDate) { - this.setDate(defEndDate, true, true); - this.setInputValue(this.endDateEl, defEndDate); - } - } - } - if (this.options.isMultipleSelection) { - this.multiple = true; - this.dates = []; - this.dateEls = []; - this.dateEls.push(this.el); - } - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - this.el.removeEventListener('change', this._handleInputChange); - this.calendarEl.removeEventListener('click', this._handleCalendarClick); - if (this.options.isDateRange) { - this.endDateEl.removeEventListener('click', this._handleInputClick); - this.endDateEl.removeEventListener('keypress', this._handleInputKeydown); - this.endDateEl.removeEventListener('change', this._handleInputChange); - } - } - _handleInputClick = (e) => { - // Prevents default browser datepicker modal rendering - if (e.type == 'date') { - e.preventDefault(); - } - this.setDateFromInput(e.target); - this.draw(); - this.gotoDate(e.target === this.el ? this.date : this.endDate); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.setDateFromInput(e.target); - this.draw(); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - } - }; - _handleCalendarClick = (e) => { - const target = e.target; - if (!target.classList.contains('is-disabled')) { - if (target.classList.contains('datepicker-day-button') && - !target.classList.contains('is-empty') && - !target.parentElement.classList.contains('is-disabled')) { - const selectedDate = new Date(e.target.getAttribute('data-year'), e.target.getAttribute('data-month'), e.target.getAttribute('data-day')); - if (!this.multiple || (this.multiple && this.options.isMultipleSelection)) { - this.setDate(selectedDate); - } - if (this.options.isDateRange) { - const confirmAfterSelection = Datepicker._isDate(this.date) && this.options.autoSubmit; - this._handleDateRangeCalendarClick(selectedDate); - if (confirmAfterSelection) { - this._confirm(); - } - } - if (!this.options.isDateRange && this.options.autoSubmit) - this._confirm(); - } - else if (target.closest('.month-prev')) { - this.prevMonth(); - } - else if (target.closest('.month-next')) { - this.nextMonth(); - } - } - }; - _handleDateRangeCalendarClick = (date) => { - if (this.endDate == null || !Datepicker._compareDates(date, this.endDate)) { - if (Datepicker._isDate(this.date) && Datepicker._comparePastDate(date, this.date)) { - return; - } - this.setDate(date, true, Datepicker._isDate(this.date)); - return; - } - this._clearDates(); - this.draw(); - }; - _handleClearClick = () => { - this._clearDates(); - this.setInputValues(); - }; - _clearDates = () => { - this.date = null; - this.endDate = null; - this.draw(); - }; - _handleMonthChange = (e) => { - this.gotoMonth(e.target.value); - }; - _handleYearChange = (e) => { - this.gotoYear(e.target.value); - }; - // change view to a specific month (zero-index, e.g. 0: January) - gotoMonth(month) { - if (!isNaN(month)) { - this.calendars[0].month = parseInt(month, 10); - this.adjustCalendars(); - } - } - // change view to a specific full year (e.g. "2012") - gotoYear(year) { - if (!isNaN(year)) { - this.calendars[0].year = parseInt(year, 10); - this.adjustCalendars(); - } - } - _handleInputChange = (e) => { - let date; - const el = e.target; - // Prevent change event from being fired when triggered by the plugin - if (e['detail']?.firedBy === this) - return; - // Prevent change event from being fired if an end date is set without a start date - if (el == this.endDateEl && !this.date) - return; - if (this.options.parse) { - date = this.options.parse(e.target.value, typeof this.options.format === 'function' - ? this.options.format(new Date(this.el.value)) - : this.options.format); - } - else { - date = new Date(Date.parse(e.target.value)); - } - if (Datepicker._isDate(date)) { - this.setDate(date, false, el == this.endDateEl, true); - if (e.type == 'date') { - this.setDataDate(e, date); - this.setInputValues(); - } - } - }; - renderDayName(opts, day, abbr = false) { - day += opts.firstDay; - while (day >= 7) { - day -= 7; - } - return abbr ? opts.i18n.weekdaysAbbrev[day] : opts.i18n.weekdays[day]; - } - createDateInput() { - const dateInput = this.el.cloneNode(true); - dateInput.addEventListener('click', this._handleInputClick); - dateInput.addEventListener('keypress', this._handleInputKeydown); - dateInput.addEventListener('change', this._handleInputChange); - this.el.parentElement.appendChild(dateInput); - return dateInput; - } - // Set input value to the selected date and close Datepicker - _finishSelection = () => { - this.setInputValues(); - // Commented out because of function deprecations - // this.close(); - }; - _confirm = () => { - this._finishSelection(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onConfirm === 'function') - this.options.onConfirm.call(this); - }; - _cancel = () => { - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - // deprecated - open() { - console.warn('Datepicker.open() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Datepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Datepicker._template = ` -
-
- - -
-
-
- -
-
`; - } -} - -class Forms { - /** - * Checks if the label has validation and apply - * the correct class and styles - * @param textfield - */ - static validateField(textfield) { - if (!textfield) { - console.error('No text field element found'); - return; - } - const hasLength = textfield.getAttribute('data-length') !== null; - const lenAttr = parseInt(textfield.getAttribute('data-length')); - const len = textfield.value.length; - if (len === 0 && - textfield.validity.badInput === false && - !textfield.required && - textfield.classList.contains('validate')) { - textfield.classList.remove('invalid'); - } - else if (textfield.classList.contains('validate')) { - // Check for character counter attributes - if ((textfield.validity.valid && hasLength && len <= lenAttr) || - (textfield.validity.valid && !hasLength)) { - textfield.classList.remove('invalid'); - } - else { - textfield.classList.add('invalid'); - } - } - } - /** - * Resizes the given TextArea after updating the - * value content dynamically. - * @param e EventTarget - */ - static textareaAutoResize(e) { - const textarea = e; - // if (!textarea) { - // console.error('No textarea element found'); - // return; - // } - // Textarea Auto Resize - let hiddenDiv = document.querySelector('.hiddendiv'); - if (!hiddenDiv) { - hiddenDiv = document.createElement('div'); - hiddenDiv.classList.add('hiddendiv', 'common'); - document.body.append(hiddenDiv); - } - const style = getComputedStyle(textarea); - // Set font properties of hiddenDiv - const fontFamily = style.fontFamily; //textarea.css('font-family'); - const fontSize = style.fontSize; //textarea.css('font-size'); - const lineHeight = style.lineHeight; //textarea.css('line-height'); - // Firefox can't handle padding shorthand. - const paddingTop = style.paddingTop; //getComputedStyle(textarea).css('padding-top'); - const paddingRight = style.paddingRight; //textarea.css('padding-right'); - const paddingBottom = style.paddingBottom; //textarea.css('padding-bottom'); - const paddingLeft = style.paddingLeft; //textarea.css('padding-left'); - if (fontSize) - hiddenDiv.style.fontSize = fontSize; //('font-size', fontSize); - if (fontFamily) - hiddenDiv.style.fontFamily = fontFamily; //css('font-family', fontFamily); - if (lineHeight) - hiddenDiv.style.lineHeight = lineHeight; //css('line-height', lineHeight); - if (paddingTop) - hiddenDiv.style.paddingTop = paddingTop; //ss('padding-top', paddingTop); - if (paddingRight) - hiddenDiv.style.paddingRight = paddingRight; //css('padding-right', paddingRight); - if (paddingBottom) - hiddenDiv.style.paddingBottom = paddingBottom; //css('padding-bottom', paddingBottom); - if (paddingLeft) - hiddenDiv.style.paddingLeft = paddingLeft; //css('padding-left', paddingLeft); - // Set original-height, if none - if (!textarea.hasAttribute('original-height')) - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - if (textarea.getAttribute('wrap') === 'off') { - hiddenDiv.style.overflowWrap = 'normal'; // ('overflow-wrap', 'normal') - hiddenDiv.style.whiteSpace = 'pre'; //.css('white-space', 'pre'); - } - hiddenDiv.innerText = textarea.value + '\n'; - hiddenDiv.innerHTML = hiddenDiv.innerHTML.replace(/\n/g, '
'); - // When textarea is hidden, width goes crazy. - // Approximate with half of window size - if (textarea.offsetWidth > 0 && textarea.offsetHeight > 0) { - hiddenDiv.style.width = textarea.getBoundingClientRect().width + 'px'; // ('width', textarea.width() + 'px'); - } - else { - hiddenDiv.style.width = window.innerWidth / 2 + 'px'; //css('width', window.innerWidth / 2 + 'px'); - } - // Resize if the new height is greater than the - // original height of the textarea - const originalHeight = parseInt(textarea.getAttribute('original-height')); - const prevLength = parseInt(textarea.getAttribute('previous-length')); - if (isNaN(originalHeight)) - return; - if (originalHeight <= hiddenDiv.clientHeight) { - textarea.style.height = hiddenDiv.clientHeight + 'px'; //css('height', hiddenDiv.innerHeight() + 'px'); - } - else if (textarea.value.length < prevLength) { - // In case the new height is less than original height, it - // means the textarea has less text than before - // So we set the height to the original one - textarea.style.height = originalHeight + 'px'; - } - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - } - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.addEventListener('change', (e) => { - const target = e.target; - if (target instanceof HTMLInputElement) { - if (target.value.length !== 0 || target.getAttribute('placeholder') !== null) { - for (const child of target.parentNode.children) { - if (child.tagName == 'label') { - child.classList.add('active'); - } - } - } - Forms.validateField(target); - } - }); - document.addEventListener('keyup', (e) => { - const target = e.target; - // Radio and Checkbox focus class - if (target instanceof HTMLInputElement && ['radio', 'checkbox'].includes(target.type)) { - // TAB, check if tabbing to radio or checkbox. - if (Utils.keys.TAB.includes(e.key)) { - target.classList.add('tabbed'); - target.addEventListener('blur', () => target.classList.remove('tabbed'), { - once: true - }); - } - } - }); - document - .querySelectorAll('.materialize-textarea') - .forEach((textArea) => { - Forms.InitTextarea(textArea); - }); - // File Input Path - document - .querySelectorAll('.file-field input[type="file"]') - .forEach((fileInput) => { - Forms.InitFileInputPath(fileInput); - }); - }); - } - static InitTextarea(textarea) { - // Save Data in Element - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - Forms.textareaAutoResize(textarea); - textarea.addEventListener('keyup', (e) => Forms.textareaAutoResize(e.target)); - textarea.addEventListener('keydown', (e) => Forms.textareaAutoResize(e.target)); - } - static InitFileInputPath(fileInput) { - fileInput.addEventListener('change', () => { - const fileField = fileInput.closest('.file-field'); - const pathInput = fileField.querySelector('input.file-path'); - const files = fileInput.files; - const filenames = []; - for (let i = 0; i < files.length; i++) { - filenames.push(files[i].name); - } - pathInput.value = filenames.join(', '); - pathInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - }); - } -} - -const _defaults$d = { - inDuration: 275, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null -}; -class Materialbox extends Component { - /** If the materialbox overlay is showing. */ - overlayActive; - /** If the materialbox is no longer being animated. */ - doneAnimating; - /** Caption, if specified. */ - caption; - /** Original width of image. */ - originalWidth; - /** Original height of image. */ - originalHeight; - originInlineStyles; - placeholder; - _changedAncestorList; - newHeight; - newWidth; - windowWidth; - windowHeight; - attrWidth; - attrHeight; - _overlay; - _photoCaption; - constructor(el, options) { - super(el, options, Materialbox); - this.el['M_Materialbox'] = this; - this.options = { - ...Materialbox.defaults, - ...options - }; - this.overlayActive = false; - this.doneAnimating = true; - this.placeholder = document.createElement('div'); - this.placeholder.classList.add('material-placeholder'); - this.originalWidth = 0; - this.originalHeight = 0; - this.originInlineStyles = this.el.getAttribute('style'); - this.caption = this.el.getAttribute('data-caption') || ''; - this.el.tabIndex = 0; - // Wrap - this.el.before(this.placeholder); - this.placeholder.append(this.el); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$d; - } - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Materialbox); - } - static getInstance(el) { - return el['M_Materialbox']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_Materialbox'] = undefined; - // Unwrap image - //this.placeholder.after(this.el).remove(); - this.placeholder.remove(); - this.el.removeAttribute('style'); - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleMaterialboxClick); - this.el.addEventListener('keypress', this._handleMaterialboxKeypress); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleMaterialboxClick); - this.el.removeEventListener('keypress', this._handleMaterialboxKeypress); - } - _handleMaterialboxClick = () => { - this._handleMaterialboxToggle(); - }; - _handleMaterialboxKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleMaterialboxToggle(); - } - }; - _handleMaterialboxToggle = () => { - // If already modal, return to original - if (this.doneAnimating === false || (this.overlayActive && this.doneAnimating)) - this.close(); - else - this.open(); - }; - _handleWindowScroll = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowResize = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowEscape = (e) => { - if (Utils.keys.ESC.includes(e.key) && this.doneAnimating && this.overlayActive) - this.close(); - }; - _makeAncestorsOverflowVisible() { - this._changedAncestorList = []; - let ancestor = this.placeholder.parentNode; - while (ancestor !== null && ancestor !== document) { - const curr = ancestor; - if (curr.style.overflow !== 'visible') { - curr.style.overflow = 'visible'; - this._changedAncestorList.push(curr); - } - ancestor = ancestor.parentNode; - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateVars() { - this.windowWidth = window.innerWidth; - this.windowHeight = window.innerHeight; - this.caption = this.el.getAttribute('data-caption') || ''; - } - // Image - _animateImageIn() { - this.el.style.maxHeight = this.newHeight.toString() + 'px'; - this.el.style.maxWidth = this.newWidth.toString() + 'px'; - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.height = this.originalHeight + 'px'; - this.el.style.width = this.originalWidth + 'px'; - setTimeout(() => { - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.newHeight + 'px'; - this.el.style.width = this.newWidth + 'px'; - this.el.style.left = - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2 + - 'px'; - this.el.style.top = - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2 + - 'px'; - }, 1); - setTimeout(() => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - /* - anim({ - targets: this.el, // image - height: [this.originalHeight, this.newHeight], - width: [this.originalWidth, this.newWidth], - left: - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2, - top: - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2, - - duration: this.options.inDuration, - easing: 'easeOutQuad', - complete: () => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') this.options.onOpenEnd.call(this, this.el); - } - }); - */ - } - _animateImageOut() { - const duration = this.options.outDuration; - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.originalWidth + 'px'; - this.el.style.width = this.originalWidth + 'px'; - this.el.style.left = '0'; - this.el.style.top = '0'; - setTimeout(() => { - this.placeholder.style.height = ''; - this.placeholder.style.width = ''; - this.placeholder.style.position = ''; - this.placeholder.style.top = ''; - this.placeholder.style.left = ''; - // Revert to width or height attribute - if (this.attrWidth) - this.el.setAttribute('width', this.attrWidth.toString()); - if (this.attrHeight) - this.el.setAttribute('height', this.attrHeight.toString()); - this.el.removeAttribute('style'); - if (this.originInlineStyles) - this.el.setAttribute('style', this.originInlineStyles); - // Remove class - this.el.classList.remove('active'); - this.doneAnimating = true; - // Remove overflow overrides on ancestors - this._changedAncestorList.forEach((anchestor) => (anchestor.style.overflow = '')); - // onCloseEnd callback - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - // Caption - _addCaption() { - this._photoCaption = document.createElement('div'); - this._photoCaption.classList.add('materialbox-caption'); - this._photoCaption.innerText = this.caption; - document.body.append(this._photoCaption); - this._photoCaption.style.display = 'inline'; - // Animate - this._photoCaption.style.transition = 'none'; - this._photoCaption.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '1'; - }, 1); - } - _removeCaption() { - const duration = this.options.outDuration; - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '0'; - setTimeout(() => { - this._photoCaption.remove(); - }, duration); - } - // Overlay - _addOverlay() { - this._overlay = document.createElement('div'); - this._overlay.id = 'materialbox-overlay'; - this._overlay.addEventListener('click', () => { - if (this.doneAnimating) - this.close(); - }, { once: true }); - // Put before in origin image to preserve z-index layering. - this.el.before(this._overlay); - // Set dimensions if needed - const overlayOffset = this._overlay.getBoundingClientRect(); - this._overlay.style.width = this.windowWidth + 'px'; - this._overlay.style.height = this.windowHeight + 'px'; - this._overlay.style.left = -1 * overlayOffset.left + 'px'; - this._overlay.style.top = -1 * overlayOffset.top + 'px'; - // Animate - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '1'; - }, 1); - } - _removeOverlay() { - const duration = this.options.outDuration; - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '0'; - setTimeout(() => { - this.overlayActive = false; - this._overlay.remove(); - }, duration); - } - /** - * Open materialbox. - */ - open = () => { - this._updateVars(); - this.originalWidth = this.el.getBoundingClientRect().width; - this.originalHeight = this.el.getBoundingClientRect().height; - // Set states - this.doneAnimating = false; - this.el.classList.add('active'); - this.overlayActive = true; - // onOpenStart callback - if (typeof this.options.onOpenStart === 'function') - this.options.onOpenStart.call(this, this.el); - // Set positioning for placeholder - this.placeholder.style.width = this.placeholder.getBoundingClientRect().width + 'px'; - this.placeholder.style.height = this.placeholder.getBoundingClientRect().height + 'px'; - this.placeholder.style.position = 'relative'; - this.placeholder.style.top = '0'; - this.placeholder.style.left = '0'; - this._makeAncestorsOverflowVisible(); - // Set css on origin - this.el.style.position = 'absolute'; - this.el.style.zIndex = '1000'; - this.el.style.willChange = 'left, top, width, height'; - // Change from width or height attribute to css - this.attrWidth = this.el.getAttribute('width'); - this.attrHeight = this.el.getAttribute('height'); - if (this.attrWidth) { - this.el.style.width = this.attrWidth + 'px'; - this.el.removeAttribute('width'); - } - if (this.attrHeight) { - this.el.style.width = this.attrHeight + 'px'; - this.el.removeAttribute('height'); - } - this._addOverlay(); - // Add and animate caption if it exists - if (this.caption !== '') - this._addCaption(); - // Resize Image - const widthPercent = this.originalWidth / this.windowWidth; - const heightPercent = this.originalHeight / this.windowHeight; - this.newWidth = 0; - this.newHeight = 0; - if (widthPercent > heightPercent) { - // Width first - const ratio = this.originalHeight / this.originalWidth; - this.newWidth = this.windowWidth * 0.9; - this.newHeight = this.windowWidth * 0.9 * ratio; - } - else { - // Height first - const ratio = this.originalWidth / this.originalHeight; - this.newWidth = this.windowHeight * 0.9 * ratio; - this.newHeight = this.windowHeight * 0.9; - } - this._animateImageIn(); - // Handle Exit triggers - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleWindowResize); - window.addEventListener('keyup', this._handleWindowEscape); - }; - /** - * Close materialbox. - */ - close = () => { - this._updateVars(); - this.doneAnimating = false; - // onCloseStart callback - if (typeof this.options.onCloseStart === 'function') - this.options.onCloseStart.call(this, this.el); - //anim.remove(this.el); - //anim.remove(this._overlay); - //if (this.caption !== '') anim.remove(this._photoCaption); - // disable exit handlers - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleWindowResize); - window.removeEventListener('keyup', this._handleWindowEscape); - this._removeOverlay(); - this._animateImageOut(); - if (this.caption !== '') - this._removeCaption(); - }; -} - -const _defaults$c = { - opacity: 0.5, - inDuration: 250, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true, - dismissible: true, - startingTop: '4%', - endingTop: '10%' -}; -class Modal extends Component { - constructor(el, options) { - super(el, options, Modal); - this.el['M_Modal'] = this; - this.options = { - ...Modal.defaults, - ...options - }; - this.el.tabIndex = 0; - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$c; - } - static init(els, options = {}) { - return super.init(els, options, Modal); - } - static getInstance(el) { - return el['M_Modal']; - } - destroy() { } - _setupEventHandlers() { } - _removeEventHandlers() { } - _handleTriggerClick() { } - _handleOverlayClick() { } - _handleModalCloseClick() { } - _handleKeydown() { } - _handleFocus() { } - open() { - return this; - } - close() { - return this; - } - // Experimental! - static #createHtml(config) { - return ` - ${config.header ? '' : ''} - - ${config.header ? '' : ''} - `; - } - static #createHtmlElement(config) { - const dialog = document.createElement('dialog'); - dialog.id = config.id; - return dialog; - } - static create(config) { - return this.#createHtmlElement(config); - } - static { } -} - -const _defaults$b = { - edge: 'left', - draggable: true, - dragTargetWidth: '10px', - inDuration: 250, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true -}; -class Sidenav extends Component { - id; - /** Describes open/close state of Sidenav. */ - isOpen; - /** Describes if sidenav is fixed. */ - isFixed; - /** Describes if Sidenav is being dragged. */ - isDragged; - lastWindowWidth; - lastWindowHeight; - static _sidenavs; - _overlay; - dragTarget; - _startingXpos; - _xPos; - _time; - _width; - _initialScrollTop; - _verticallyScrolling; - deltaX; - velocityX; - percentOpen; - constructor(el, options) { - super(el, options, Sidenav); - this.el['M_Sidenav'] = this; - this.options = { - ...Sidenav.defaults, - ...options - }; - this.id = this.el.id; - this.isOpen = false; - this.isFixed = this.el.classList.contains('sidenav-fixed'); - this.isDragged = false; - // Window size variables for window resize checks - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - this._createOverlay(); - this._createDragTarget(); - this._setupEventHandlers(); - this._setupClasses(); - this._setupFixed(); - Sidenav._sidenavs.push(this); - } - static get defaults() { - return _defaults$b; - } - /** - * Initializes instances of Sidenav. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Sidenav); - } - static getInstance(el) { - return el['M_Sidenav']; - } - destroy() { - this._removeEventHandlers(); - this._enableBodyScrolling(); - this._overlay.parentNode.removeChild(this._overlay); - this.dragTarget.parentNode.removeChild(this.dragTarget); - this.el['M_Sidenav'] = undefined; - this.el.style.transform = ''; - const index = Sidenav._sidenavs.indexOf(this); - if (index >= 0) { - Sidenav._sidenavs.splice(index, 1); - } - } - _createOverlay() { - this._overlay = document.createElement('div'); - this._overlay.classList.add('sidenav-overlay'); - this._overlay.addEventListener('click', this.close); - document.body.appendChild(this._overlay); - } - _setupEventHandlers() { - if (Sidenav._sidenavs.length === 0) { - document.body.addEventListener('click', this._handleTriggerClick); - } - const passiveIfSupported = null; - this.dragTarget.addEventListener('touchmove', this._handleDragTargetDrag, passiveIfSupported); - this.dragTarget.addEventListener('touchend', this._handleDragTargetRelease); - this._overlay.addEventListener('touchmove', this._handleCloseDrag, passiveIfSupported); - this._overlay.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('touchmove', this._handleCloseDrag); // , passiveIfSupported); - this.el.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('click', this._handleCloseTriggerClick); - // Add resize for side nav fixed - if (this.isFixed) { - window.addEventListener('resize', this._handleWindowResize); - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - _removeEventHandlers() { - if (Sidenav._sidenavs.length === 1) { - document.body.removeEventListener('click', this._handleTriggerClick); - } - this.dragTarget.removeEventListener('touchmove', this._handleDragTargetDrag); - this.dragTarget.removeEventListener('touchend', this._handleDragTargetRelease); - this._overlay.removeEventListener('touchmove', this._handleCloseDrag); - this._overlay.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('touchmove', this._handleCloseDrag); - this.el.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('click', this._handleCloseTriggerClick); - // Remove resize for side nav fixed - if (this.isFixed) { - window.removeEventListener('resize', this._handleWindowResize); - } - } - _handleTriggerClick(e) { - const trigger = e.target.closest('.sidenav-trigger'); - if (e.target && trigger) { - const sidenavId = Utils.getIdFromTrigger(trigger); - const sidenavInstance = document.getElementById(sidenavId)['M_Sidenav']; - if (sidenavInstance) { - sidenavInstance.open(); - } - e.preventDefault(); - } - } - // Set variables needed at the beginning of drag and stop any current transition. - _startDrag(e) { - const clientX = e.targetTouches[0].clientX; - this.isDragged = true; - this._startingXpos = clientX; - this._xPos = this._startingXpos; - this._time = Date.now(); - this._width = this.el.getBoundingClientRect().width; - this._overlay.style.display = 'block'; - this._initialScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this._verticallyScrolling = false; - } - //Set variables needed at each drag move update tick - _dragMoveUpdate(e) { - const clientX = e.targetTouches[0].clientX; - const currentScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this.deltaX = Math.abs(this._xPos - clientX); - this._xPos = clientX; - this.velocityX = this.deltaX / (Date.now() - this._time); - this._time = Date.now(); - if (this._initialScrollTop !== currentScrollTop) { - this._verticallyScrolling = true; - } - } - _handleDragTargetDrag = (e) => { - // Check if draggable - if (!this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge === dragDirection) { - totalDeltaX = 0; - } - /** - * transformX is the drag displacement - * transformPrefix is the initial transform placement - * Invert values if Sidenav is right edge - */ - let transformX = totalDeltaX; - let transformPrefix = 'translateX(-100%)'; - if (this.options.edge === 'right') { - transformPrefix = 'translateX(100%)'; - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `${transformPrefix} translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _handleDragTargetRelease = () => { - if (this.isDragged) { - if (this.percentOpen > 0.2) { - this.open(); - } - else { - this._animateOut(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - _handleCloseDrag = (e) => { - // Check if open and draggable - if (!this.isOpen || !this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - // dragDirection is the attempted user drag direction - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge !== dragDirection) { - totalDeltaX = 0; - } - let transformX = -totalDeltaX; - if (this.options.edge === 'right') { - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, 1 - totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _calculateDelta = (e) => { - // If not being dragged, set initial drag start variables - if (!this.isDragged) { - this._startDrag(e); - } - // Run touchmove updates - this._dragMoveUpdate(e); - // Calculate raw deltaX - return this._xPos - this._startingXpos; - }; - _handleCloseRelease = () => { - if (this.isOpen && this.isDragged) { - if (this.percentOpen > 0.8) { - this._animateIn(); - } - else { - this.close(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - // Handles closing of Sidenav when element with class .sidenav-close - _handleCloseTriggerClick = (e) => { - const closeTrigger = e.target.closest('.sidenav-close'); - if (closeTrigger && !this._isCurrentlyFixed()) { - this.close(); - } - }; - _handleWindowResize = () => { - // Only handle horizontal resizes - if (this.lastWindowWidth !== window.innerWidth) { - if (window.innerWidth > 992) { - this.open(); - } - else { - this.close(); - } - } - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - }; - _setupClasses() { - if (this.options.edge === 'right') { - this.el.classList.add('right-aligned'); - this.dragTarget.classList.add('right-aligned'); - } - } - _removeClasses() { - this.el.classList.remove('right-aligned'); - this.dragTarget.classList.remove('right-aligned'); - } - _setupFixed() { - if (this._isCurrentlyFixed()) - this.open(); - } - _isDraggable() { - return this.options.draggable && !this._isCurrentlyFixed() && !this._verticallyScrolling; - } - _isCurrentlyFixed() { - return this.isFixed && window.innerWidth > 992; - } - _createDragTarget() { - const dragTarget = document.createElement('div'); - dragTarget.classList.add('drag-target'); - dragTarget.style.width = this.options.dragTargetWidth; - document.body.appendChild(dragTarget); - this.dragTarget = dragTarget; - } - _preventBodyScrolling() { - document.body.style.overflow = 'hidden'; - } - _enableBodyScrolling() { - document.body.style.overflow = ''; - } - /** - * Opens Sidenav. - */ - open = () => { - if (this.isOpen === true) - return; - this.isOpen = true; - // Run onOpenStart callback - if (typeof this.options.onOpenStart === 'function') { - this.options.onOpenStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - // Show if fixed - this.el.style.transform = 'translateX(0)'; - this._enableBodyScrolling(); - this._overlay.style.display = 'none'; - } - // Handle non-fixed Sidenav - else { - if (this.options.preventScrolling) - this._preventBodyScrolling(); - if (!this.isDragged || this.percentOpen != 1) - this._animateIn(); - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - /** - * Closes Sidenav. - */ - close = () => { - if (this.isOpen === false) - return; - this.isOpen = false; - // Run onCloseStart callback - if (typeof this.options.onCloseStart === 'function') { - this.options.onCloseStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - const transformX = this.options.edge === 'left' ? '-105%' : '105%'; - this.el.style.transform = `translateX(${transformX})`; - } - // Handle non-fixed Sidenav - else { - this._enableBodyScrolling(); - if (!this.isDragged || this.percentOpen != 0) { - this._animateOut(); - } - else { - this._overlay.style.display = 'none'; - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - _animateIn() { - this._animateSidenavIn(); - this._animateOverlayIn(); - } - _animateOut() { - this._animateSidenavOut(); - this._animateOverlayOut(); - } - _animateSidenavIn() { - let slideOutPercent = this.options.edge === 'left' ? -1 : 1; - if (this.isDragged) { - slideOutPercent = - this.options.edge === 'left' - ? slideOutPercent + this.percentOpen - : slideOutPercent - this.percentOpen; - } - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.transform = 'translateX(' + slideOutPercent * 100 + '%)'; - setTimeout(() => { - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(0)'; - }, 1); - setTimeout(() => { - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - } - _animateSidenavOut() { - const endPercent = this.options.edge === 'left' ? -1 : 1; - // let slideOutPercent = 0; - // if (this.isDragged) { - // // @todo unused variable - // slideOutPercent = - // this.options.edge === 'left' - // ? endPercent + this.percentOpen - // : endPercent - this.percentOpen; - // } - const duration = this.options.outDuration; - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(' + endPercent * 100 + '%)'; - setTimeout(() => { - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - _animateOverlayIn() { - let start = 0; - if (this.isDragged) - start = this.percentOpen; - else - this._overlay.style.display = 'block'; - // Animation - const duration = this.options.inDuration; - // from - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = start.toString(); - // easeOutQuad - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '1'; - }, 1); - } - _animateOverlayOut() { - const duration = this.options.outDuration; - // easeOutQuad - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '0'; - setTimeout(() => { - this._overlay.style.display = 'none'; - }, duration); - } - _setAriaHidden = () => { - this.el.ariaHidden = this.isOpen ? 'false' : 'true'; - const navWrapper = document.querySelector('.nav-wrapper ul'); - if (navWrapper) - navWrapper.ariaHidden = this.isOpen.toString(); - }; - _setTabIndex = () => { - const navLinks = document.querySelectorAll('.nav-wrapper ul li a'); - const sideNavLinks = document.querySelectorAll('.sidenav li a'); - if (navLinks) - navLinks.forEach((navLink) => { - navLink.tabIndex = this.isOpen ? -1 : 0; - }); - if (sideNavLinks) - sideNavLinks.forEach((sideNavLink) => { - sideNavLink.tabIndex = this.isOpen ? 0 : -1; - }); - }; - static { - Sidenav._sidenavs = []; - } -} - -const _defaults$a = { - indicators: true, - height: 400, - duration: 500, - interval: 6000, - pauseOnFocus: true, - pauseOnHover: true, - indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA) -}; -class Slider extends Component { - /** Index of current slide. */ - activeIndex; - interval; - eventPause; - _slider; - _slides; - _activeSlide; - _indicators; - _hovered; - _focused; - _focusCurrent; - _sliderId; - constructor(el, options) { - super(el, options, Slider); - this.el['M_Slider'] = this; - this.options = { - ...Slider.defaults, - ...options - }; - // init props - this.interval = null; - this.eventPause = false; - this._hovered = false; - this._focused = false; - this._focusCurrent = false; - // setup - this._slider = this.el.querySelector('.slides'); - this._slides = Array.from(this._slider.querySelectorAll('li')); - this.activeIndex = this._slides.findIndex((li) => li.classList.contains('active')); - if (this.activeIndex !== -1) { - this._activeSlide = this._slides[this.activeIndex]; - } - this._setSliderHeight(); - // Sets element id if it does not have one - if (this._slider.hasAttribute('id')) - this._sliderId = this._slider.getAttribute('id'); - else { - this._sliderId = 'slider-' + Utils.guid(); - this._slider.setAttribute('id', this._sliderId); - } - const placeholderBase64 = 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; - // Set initial positions of captions - this._slides.forEach((slide) => { - // Caption - //const caption = slide.querySelector('.caption'); - //if (caption) this._animateCaptionIn(caption, 0); - // Set Images as Background Images - const img = slide.querySelector('img'); - if (img) { - if (img.src !== placeholderBase64) { - img.style.backgroundImage = 'url(' + img.src + ')'; - img.src = placeholderBase64; - } - } - // Sets slide as focusable by code - if (!slide.hasAttribute('tabindex')) - slide.setAttribute('tabindex', '-1'); - // Removes initial visibility from "inactive" slides - slide.style.visibility = 'hidden'; - }); - this._setupIndicators(); - // Show active slide - if (this._activeSlide) { - this._activeSlide.style.display = 'block'; - this._activeSlide.style.visibility = 'visible'; - } - else { - this.activeIndex = 0; - this._slides[0].classList.add('active'); - this._slides[0].style.visibility = 'visible'; - this._activeSlide = this._slides[0]; - this._animateSlide(this._slides[0], true); - // Update indicators - if (this.options.indicators) { - this._indicators[this.activeIndex].children[0].classList.add('active'); - } - } - this._setupEventHandlers(); - // auto scroll - this.start(); - } - static get defaults() { - return _defaults$a; - } - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Slider); - } - static getInstance(el) { - return el['M_Slider']; - } - destroy() { - this.pause(); - this._removeIndicators(); - this._removeEventHandlers(); - this.el['M_Slider'] = undefined; - } - _setupEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.addEventListener('focusin', this._handleAutoPauseFocus); - this.el.addEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.addEventListener('mouseenter', this._handleAutoPauseHover); - this.el.addEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.addEventListener('click', this._handleIndicatorClick); - }); - } - } - _removeEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.removeEventListener('focusin', this._handleAutoPauseFocus); - this.el.removeEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.removeEventListener('mouseenter', this._handleAutoPauseHover); - this.el.removeEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.removeEventListener('click', this._handleIndicatorClick); - }); - } - } - _handleIndicatorClick = (e) => { - const el = e.target.parentElement; - const currIndex = [...el.parentNode.children].indexOf(el); - this._focusCurrent = true; - this.set(currIndex); - }; - _handleAutoPauseHover = () => { - this._hovered = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoPauseFocus = () => { - this._focused = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoStartHover = () => { - this._hovered = false; - if (!(this.options.pauseOnFocus && this._focused) && this.eventPause) { - this.start(); - } - }; - _handleAutoStartFocus = () => { - this._focused = false; - if (!(this.options.pauseOnHover && this._hovered) && this.eventPause) { - this.start(); - } - }; - _handleInterval = () => { - const activeElem = this._slider.querySelector('.active'); - let newActiveIndex = [...activeElem.parentNode.children].indexOf(activeElem); - if (this._slides.length === newActiveIndex + 1) - newActiveIndex = 0; // loop to start - else - newActiveIndex += 1; - this.set(newActiveIndex); - }; - _animateSlide(slide, isDirectionIn) { - let dx = 0, dy = 0; - // from - slide.style.opacity = isDirectionIn ? '0' : '1'; - setTimeout(() => { - slide.style.transition = `opacity ${this.options.duration}ms ease`; - // to - slide.style.opacity = isDirectionIn ? '1' : '0'; - }, 1); - // Caption - const caption = slide.querySelector('.caption'); - if (!caption) - return; - if (caption.classList.contains('center-align')) - dy = -100; - else if (caption.classList.contains('right-align')) - dx = 100; - else if (caption.classList.contains('left-align')) - dx = -100; - // from - caption.style.opacity = isDirectionIn ? '0' : '1'; - caption.style.transform = isDirectionIn ? `translate(${dx}px, ${dy}px)` : `translate(0, 0)`; - setTimeout(() => { - caption.style.transition = `opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`; - // to - caption.style.opacity = isDirectionIn ? '1' : '0'; - caption.style.transform = isDirectionIn ? `translate(0, 0)` : `translate(${dx}px, ${dy}px)`; - }, this.options.duration); // delay - } - _setSliderHeight() { - // If fullscreen, do nothing - if (!this.el.classList.contains('fullscreen')) { - if (this.options.indicators) { - // Add height if indicators are present - this.el.style.height = this.options.height + 40 + 'px'; //.css('height', this.options.height + 40 + 'px'); - } - else { - this.el.style.height = this.options.height + 'px'; - } - this._slider.style.height = this.options.height + 'px'; - } - } - _setupIndicators() { - if (this.options.indicators) { - const ul = document.createElement('ul'); - ul.classList.add('indicators'); - const arrLi = []; - this._slides.forEach((el, i) => { - const label = this.options.indicatorLabelFunc - ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) - : `${i + 1}`; - const li = document.createElement('li'); - li.classList.add('indicator-item'); - li.innerHTML = ``; - arrLi.push(li); - ul.append(li); - }); - this.el.append(ul); - this._indicators = arrLi; - } - } - _removeIndicators() { - this.el.querySelector('ul.indicators').remove(); //find('ul.indicators').remove(); - } - set(index) { - // Wrap around indices. - if (index >= this._slides.length) - index = 0; - else if (index < 0) - index = this._slides.length - 1; - // Only do if index changes - if (this.activeIndex === index) - return; - this._activeSlide = this._slides[this.activeIndex]; - const _caption = this._activeSlide.querySelector('.caption'); - this._activeSlide.classList.remove('active'); - // Enables every slide - this._slides.forEach((slide) => (slide.style.visibility = 'visible')); - //--- Hide active Slide + Caption - this._activeSlide.style.opacity = '0'; - setTimeout(() => { - this._slides.forEach((slide) => { - if (slide.classList.contains('active')) - return; - slide.style.opacity = '0'; - slide.style.transform = 'translate(0, 0)'; - // Disables invisible slides (for assistive technologies) - slide.style.visibility = 'hidden'; - }); - }, this.options.duration); - // Hide active Caption - //this._animateCaptionIn(_caption, this.options.duration); - _caption.style.opacity = '0'; - // Update indicators - if (this.options.indicators) { - const activeIndicator = this._indicators[this.activeIndex].children[0]; - const nextIndicator = this._indicators[index].children[0]; - activeIndicator.classList.remove('active'); - nextIndicator.classList.add('active'); - if (typeof this.options.indicatorLabelFunc === 'function') { - activeIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, this.activeIndex, false); - nextIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, index, true); - } - } - //--- Show new Slide + Caption - this._animateSlide(this._slides[index], true); - this._slides[index].classList.add('active'); - this.activeIndex = index; - // Reset interval, if allowed. This check prevents autostart - // when slider is paused, since it can be changed though indicators. - if (this.interval != null) { - this.start(); - } - } - _pause(fromEvent) { - clearInterval(this.interval); - this.eventPause = fromEvent; - this.interval = null; - } - /** - * Pause slider autoslide. - */ - pause = () => { - this._pause(false); - }; - /** - * Start slider autoslide. - */ - start = () => { - clearInterval(this.interval); - this.interval = setInterval(this._handleInterval, this.options.duration + this.options.interval); - this.eventPause = false; - }; - /** - * Move to next slider. - */ - next = () => { - let newIndex = this.activeIndex + 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; - /** - * Move to prev slider. - */ - prev = () => { - let newIndex = this.activeIndex - 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; -} - -const _defaults$9 = { - duration: 300, - onShow: null, - swipeable: false, - responsiveThreshold: Infinity // breakpoint for swipeable -}; -class Tabs extends Component { - _tabLinks; - _index; - _indicator; - _tabWidth; - _tabsWidth; - _tabsCarousel; - _activeTabLink; - _content; - constructor(el, options) { - super(el, options, Tabs); - this.el['M_Tabs'] = this; - this.options = { - ...Tabs.defaults, - ...options - }; - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - this._index = 0; - this._setupActiveTabLink(); - if (this.options.swipeable) { - this._setupSwipeableTabs(); - } - else { - this._setupNormalTabs(); - } - // Setup tabs indicator after content to ensure accurate widths - this._setTabsAndTabWidth(); - this._createIndicator(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$9; - } - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tabs); - } - static getInstance(el) { - return el['M_Tabs']; - } - destroy() { - this._removeEventHandlers(); - this._indicator.parentNode.removeChild(this._indicator); - if (this.options.swipeable) { - this._teardownSwipeableTabs(); - } - else { - this._teardownNormalTabs(); - } - this.el['M_Tabs'] = undefined; - } - /** - * The index of tab that is currently shown. - */ - get index() { - return this._index; - } - _setupEventHandlers() { - window.addEventListener('resize', this._handleWindowResize); - this.el.addEventListener('click', this._handleTabClick); - // map vertical scrolling to horizontal scrolling because no scrollbar is shown on desktop - this.el.addEventListener('wheel', (event) => { - if (event.deltaY !== 0) { - event.preventDefault(); - this.el.scrollLeft += event.deltaY * 1; - } - }); - } - _removeEventHandlers() { - window.removeEventListener('resize', this._handleWindowResize); - this.el.removeEventListener('click', this._handleTabClick); - } - _handleWindowResize = () => { - this._setTabsAndTabWidth(); - if (this._tabWidth !== 0 && this._tabsWidth !== 0) { - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - }; - _handleTabClick = (e) => { - let tabLink = e.target; - if (!tabLink) - return; - let tab = tabLink.parentElement; - while (tab && !tab.classList.contains('tab')) { - tabLink = tabLink.parentElement; - tab = tab.parentElement; - } - // Handle click on tab link only - if (!tabLink || !tab.classList.contains('tab')) - return; - // is disabled? - if (tab.classList.contains('disabled')) { - e.preventDefault(); - return; - } - // Act as regular link if target attribute is specified. - if (tabLink.hasAttribute('target')) - return; - // Make the old tab inactive. - this._activeTabLink.classList.remove('active'); - const _oldContent = this._content; - // Update the variables with the new link and content - this._activeTabLink = tabLink; - if (tabLink.hash) - this._content = document.querySelector(tabLink.hash); - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - // Make the tab active - this._activeTabLink.classList.add('active'); - const prevIndex = this._index; - this._index = Math.max(Array.from(this._tabLinks).indexOf(tabLink), 0); - // Swap content - if (this.options.swipeable) { - if (this._tabsCarousel) { - this._tabsCarousel.set(this._index, () => { - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - }); - } - } - else { - if (this._content) { - this._content.style.display = 'block'; - this._content.classList.add('active'); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - if (_oldContent && _oldContent !== this._content) { - _oldContent.style.display = 'none'; - _oldContent.classList.remove('active'); - } - } - } - // Update widths after content is swapped (scrollbar bugfix) - this._setTabsAndTabWidth(); - this._animateIndicator(prevIndex); - e.preventDefault(); - }; - _createIndicator() { - const indicator = document.createElement('li'); - indicator.classList.add('indicator'); - this.el.appendChild(indicator); - this._indicator = indicator; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - _setupActiveTabLink() { - // If the location.hash matches one of the links, use that as the active tab. - this._activeTabLink = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === location.hash); - // If no match is found, use the first link or any with class 'active' as the initial active tab. - if (!this._activeTabLink) { - let activeTabLink = this.el.querySelector('li.tab a.active'); - if (!activeTabLink) { - activeTabLink = this.el.querySelector('li.tab a'); - } - this._activeTabLink = activeTabLink; - } - Array.from(this._tabLinks).forEach((a) => a.classList.remove('active')); - this._activeTabLink.classList.add('active'); - this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); - if (this._activeTabLink && this._activeTabLink.hash) { - this._content = document.querySelector(this._activeTabLink.hash); - if (this._content) - this._content.classList.add('active'); - } - } - _setupSwipeableTabs() { - // Change swipeable according to responsive threshold - if (window.innerWidth > this.options.responsiveThreshold) - this.options.swipeable = false; - const tabsContent = []; - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - currContent.classList.add('carousel-item'); - tabsContent.push(currContent); - } - }); - // Create Carousel-Wrapper around Tab-Contents - const tabsWrapper = document.createElement('div'); - tabsWrapper.classList.add('tabs-content', 'carousel', 'carousel-slider'); - // Wrap around - tabsContent[0].parentElement.insertBefore(tabsWrapper, tabsContent[0]); - tabsContent.forEach((tabContent) => { - tabsWrapper.appendChild(tabContent); - tabContent.style.display = ''; - }); - // Keep active tab index to set initial carousel slide - const tab = this._activeTabLink.parentElement; - const activeTabIndex = Array.from(tab.parentNode.children).indexOf(tab); - this._tabsCarousel = Carousel.init(tabsWrapper, { - fullWidth: true, - noWrap: true, - onCycleTo: (item) => { - const prevIndex = this._index; - this._index = Array.from(item.parentNode.children).indexOf(item); - this._activeTabLink.classList.remove('active'); - this._activeTabLink = Array.from(this._tabLinks)[this._index]; - this._activeTabLink.classList.add('active'); - this._animateIndicator(prevIndex); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - } - }); - // Set initial carousel slide to active tab - this._tabsCarousel.set(activeTabIndex); - } - _teardownSwipeableTabs() { - const tabsWrapper = this._tabsCarousel.el; - this._tabsCarousel.destroy(); - // Unwrap - tabsWrapper.append(tabsWrapper.parentElement); - tabsWrapper.remove(); - } - _setupNormalTabs() { - // Hide Tabs Content - Array.from(this._tabLinks).forEach((a) => { - if (a === this._activeTabLink) - return; - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = 'none'; - } - }); - } - _teardownNormalTabs() { - // show Tabs Content - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = ''; - } - }); - } - _setTabsAndTabWidth() { - this._tabsWidth = this.el.getBoundingClientRect().width; - this._tabWidth = Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length; - } - _calcRightPos(el) { - return Math.ceil(this._tabsWidth - el.offsetLeft - el.getBoundingClientRect().width); - } - _calcLeftPos(el) { - return Math.floor(el.offsetLeft); - } - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ - updateTabIndicator() { - this._setTabsAndTabWidth(); - this._animateIndicator(this._index); - } - _animateIndicator(prevIndex) { - let leftDelay = 0, rightDelay = 0; - const isMovingLeftOrStaying = this._index - prevIndex >= 0; - if (isMovingLeftOrStaying) - leftDelay = 90; - else - rightDelay = 90; - // in v1: easeOutQuad - this._indicator.style.transition = ` - left ${this.options.duration}ms ease-out ${leftDelay}ms, - right ${this.options.duration}ms ease-out ${rightDelay}ms`; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ - select(tabId) { - const tab = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === '#' + tabId); - if (tab) - tab.click(); - } -} - -const _defaults$8 = { - dialRadius: 135, - outerRadius: 105, - innerRadius: 70, - tickRadius: 20, - duration: 350, - container: null, - defaultTime: 'now', // default time, 'now' or '13:14' e.g. - fromNow: 0, // Millisecond offset from the defaultTime - showClearBtn: false, - autoSubmit: true, - // internationalization - i18n: { - cancel: 'Cancel', - clear: 'Clear', - done: 'Ok' - }, - twelveHour: true, // change to 12 hour AM/PM clock from 24 hour - vibrate: true, // vibrate the device when dragging clock hand - // Callbacks - onSelect: null, - onInputInteraction: null, - onDone: null, - onCancel: null, - displayPlugin: null, - displayPluginOptions: null -}; -class Timepicker extends Component { - id; - containerEl; - plate; - digitalClock; - inputHours; - inputMinutes; - x0; - y0; - moved; - dx; - dy; - /** - * Current view on the timepicker. - * @default 'hours' - */ - currentView; - hand; - minutesView; - hours; - minutes; - /** The selected time. */ - time; - /** - * If the time is AM or PM on twelve-hour clock. - * @default 'PM' - */ - amOrPm; - static _template; - /** Vibrate device when dragging clock hand. */ - vibrate; - _canvas; - hoursView; - spanAmPm; - footer; - _amBtn; - _pmBtn; - bg; - bearing; - g; - toggleViewTimer; - vibrateTimer; - displayPlugin; - constructor(el, options) { - super(el, options, Timepicker); - this.el['M_Timepicker'] = this; - this.options = { - ...Timepicker.defaults, - ...options - }; - this.id = Utils.guid(); - this._insertHTMLIntoDOM(); - this._setupVariables(); - this._setupEventHandlers(); - this._clockSetup(); - if (this.options.displayPlugin) - this._setupDisplayPlugin(); - this._pickerSetup(); - } - static get defaults() { - return _defaults$8; - } - /** - * Initializes instances of Timepicker. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Timepicker); - } - static _addLeadingZero(num) { - return (num < 10 ? '0' : '') + num; - } - static _createSVGEl(name) { - const svgNS = 'http://www.w3.org/2000/svg'; - return document.createElementNS(svgNS, name); - } - static _Pos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return { - x: e.targetTouches[0].clientX, - y: e.targetTouches[0].clientY - }; - } - // mouse event - return { x: e.clientX, y: e.clientY }; - } - static getInstance(el) { - return el['M_Timepicker']; - } - destroy() { - this._removeEventHandlers(); - this.containerEl.remove(); - this.el['M_Timepicker'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - // @todo allow input field to fill values from input field when container/modal opens - this.el.addEventListener('keydown', this._handleInputKeydown); - this.plate.addEventListener('mousedown', this._handleClockClickStart); - this.plate.addEventListener('touchstart', this._handleClockClickStart); - this.digitalClock.addEventListener('keyup', this._inputFromTextField); - this.inputHours.addEventListener('focus', () => this.showView('hours')); - this.inputHours.addEventListener('focusout', () => this.formatHours()); - this.inputMinutes.addEventListener('focus', () => this.showView('minutes')); - this.inputMinutes.addEventListener('focusout', () => this.formatMinutes()); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - } - _handleInputClick = () => { - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - } - }; - _handleTimeInputEnterKey = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this._inputFromTextField(); - } - }; - _handleClockClickStart = (e) => { - e.preventDefault(); - const clockPlateBR = this.plate.getBoundingClientRect(); - const offset = { x: clockPlateBR.left, y: clockPlateBR.top }; - this.x0 = offset.x + this.options.dialRadius; - this.y0 = offset.y + this.options.dialRadius; - this.moved = false; - const clickPos = Timepicker._Pos(e); - this.dx = clickPos.x - this.x0; - this.dy = clickPos.y - this.y0; - // Set clock hands - this.setHand(this.dx, this.dy, false); - // Mousemove on document - document.addEventListener('mousemove', this._handleDocumentClickMove); - document.addEventListener('touchmove', this._handleDocumentClickMove); - // Mouseup on document - document.addEventListener('mouseup', this._handleDocumentClickEnd); - document.addEventListener('touchend', this._handleDocumentClickEnd); - }; - _handleDocumentClickMove = (e) => { - e.preventDefault(); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - this.moved = true; - this.setHand(x, y, false); - }; - _handleDocumentClickEnd = (e) => { - e.preventDefault(); - document.removeEventListener('mouseup', this._handleDocumentClickEnd); - document.removeEventListener('touchend', this._handleDocumentClickEnd); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - if (this.moved && x === this.dx && y === this.dy) { - this.setHand(x, y); - } - if (this.currentView === 'hours') { - this.inputMinutes.focus(); - this.showView('minutes', this.options.duration / 2); - } - else { - // this.minutesView.classList.add('timepicker-dial-out'); - setTimeout(() => { - if (this.options.autoSubmit) - this.done(); - }, this.options.duration / 2); - } - if (typeof this.options.onSelect === 'function') { - this.options.onSelect.call(this, this.hours, this.minutes); - } - // Unbind mousemove event - document.removeEventListener('mousemove', this._handleDocumentClickMove); - document.removeEventListener('touchmove', this._handleDocumentClickMove); - }; - _insertHTMLIntoDOM() { - const template = document.createElement('template'); - template.innerHTML = Timepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.containerEl.id = 'container-' + this.id; - // Append popover to input by default - const optEl = this.options.container; - const containerEl = optEl instanceof HTMLElement ? optEl : document.querySelector(optEl); - if (this.options.container && !!containerEl) { - containerEl.append(this.containerEl); - } - else { - this.el.parentElement.appendChild(this.containerEl); - } - } - _setupVariables() { - this.currentView = 'hours'; - this.vibrate = navigator.vibrate - ? 'vibrate' - : navigator['webkitVibrate'] - ? 'webkitVibrate' - : null; - this._canvas = this.containerEl.querySelector('.timepicker-canvas'); - this.plate = this.containerEl.querySelector('.timepicker-plate'); - this.digitalClock = this.containerEl.querySelector('.timepicker-display-column'); - this.hoursView = this.containerEl.querySelector('.timepicker-hours'); - this.minutesView = this.containerEl.querySelector('.timepicker-minutes'); - this.inputHours = this.containerEl.querySelector('.timepicker-input-hours'); - this.inputMinutes = this.containerEl.querySelector('.timepicker-input-minutes'); - this.spanAmPm = this.containerEl.querySelector('.timepicker-span-am-pm'); - this.footer = this.containerEl.querySelector('.timepicker-footer'); - this.amOrPm = 'PM'; - } - /*private _createButton(text: string, visibility: string): HTMLButtonElement { - const button = document.createElement('button'); - button.classList.add('btn', 'waves-effect', 'text'); - button.style.visibility = visibility; - button.type = 'button'; - button.tabIndex = -1; - button.innerText = text; - return button; - }*/ - _pickerSetup() { - // clearButton.classList.add('timepicker-clear'); - // clearButton.addEventListener('click', this.clear); - // this.footer.appendChild(clearButton); - if (this.options.showClearBtn) { - Utils.createButton(this.footer, this.options.i18n.clear, ['timepicker-clear'], true, this.clear); - } - if (!this.options.autoSubmit) { - /*const confirmationBtnsContainer = document.createElement('div'); - confirmationBtnsContainer.classList.add('confirmation-btns'); - this.footer.append(confirmationBtnsContainer); - - const cancelButton = this._createButton(this.options.i18n.cancel, ''); - cancelButton.classList.add('timepicker-close'); - cancelButton.addEventListener('click', this.close); - confirmationBtnsContainer.appendChild(cancelButton); - - const doneButton = this._createButton(this.options.i18n.done, ''); - doneButton.classList.add('timepicker-close'); - //doneButton.addEventListener('click', this._finishSelection); - confirmationBtnsContainer.appendChild(doneButton);*/ - Utils.createConfirmationContainer(this.footer, this.options.i18n.done, this.options.i18n.cancel, this.confirm, this.cancel); - } - this._updateTimeFromInput(); - this.showView('hours'); - } - _setupDisplayPlugin() { - if (this.options.displayPlugin === 'docked') - this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); - if (this.options.displayPlugin === 'modal') { - this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { - ...this.options.displayPluginOptions, - ...{ classList: ['timepicker-modal'] } - }); - this.footer.remove(); - this.footer = this.displayPlugin.footer; - } - } - _clockSetup() { - if (this.options.twelveHour) { - // AM Button - this._amBtn = document.createElement('div'); - this._amBtn.classList.add('am-btn', 'btn'); - this._amBtn.innerText = 'AM'; - this._amBtn.tabIndex = 0; - this._amBtn.addEventListener('click', this._handleAmPmClick); - this._amBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._amBtn); - // PM Button - this._pmBtn = document.createElement('div'); - this._pmBtn.classList.add('pm-btn', 'btn'); - this._pmBtn.innerText = 'PM'; - this._pmBtn.tabIndex = 0; - this._pmBtn.addEventListener('click', this._handleAmPmClick); - this._pmBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._pmBtn); - } - this._buildHoursView(); - this._buildMinutesView(); - this._buildSVGClock(); - } - _buildSVGClock() { - // Draw clock hands and others - const dialRadius = this.options.dialRadius; - const tickRadius = this.options.tickRadius; - const diameter = dialRadius * 2; - const svg = Timepicker._createSVGEl('svg'); - svg.setAttribute('class', 'timepicker-svg'); - svg.setAttribute('width', diameter.toString()); - svg.setAttribute('height', diameter.toString()); - const g = Timepicker._createSVGEl('g'); - g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')'); - const bearing = Timepicker._createSVGEl('circle'); - bearing.setAttribute('class', 'timepicker-canvas-bearing'); - bearing.setAttribute('cx', '0'); - bearing.setAttribute('cy', '0'); - bearing.setAttribute('r', '4'); - const hand = Timepicker._createSVGEl('line'); - hand.setAttribute('x1', '0'); - hand.setAttribute('y1', '0'); - const bg = Timepicker._createSVGEl('circle'); - bg.setAttribute('class', 'timepicker-canvas-bg'); - bg.setAttribute('r', tickRadius.toString()); - g.appendChild(hand); - g.appendChild(bg); - g.appendChild(bearing); - svg.appendChild(g); - this._canvas.appendChild(svg); - this.hand = hand; - this.bg = bg; - this.bearing = bearing; - this.g = g; - } - _buildHoursView() { - // const $tick = document.createElement('div'); - // $tick.classList.add('timepicker-tick'); - // Hours view - if (this.options.twelveHour) { - for (let i = 1; i < 13; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const radius = this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - else { - for (let i = 0; i < 24; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const inner = i > 0 && i < 13; - const radius = inner ? this.options.innerRadius : this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - } - _buildHoursTick(i, radian, radius) { - const tick = document.createElement('div'); - tick.classList.add('timepicker-tick'); - tick.style.left = - this.options.dialRadius + Math.sin(radian) * radius - this.options.tickRadius + 'px'; - tick.style.top = - this.options.dialRadius - Math.cos(radian) * radius - this.options.tickRadius + 'px'; - tick.innerHTML = i === 0 ? '00' : i.toString(); - this.hoursView.appendChild(tick); - } - _buildMinutesView() { - const _tick = document.createElement('div'); - _tick.classList.add('timepicker-tick'); - // Minutes view - for (let i = 0; i < 60; i += 5) { - const tick = _tick.cloneNode(true); - const radian = (i / 30) * Math.PI; - tick.style.left = - this.options.dialRadius + - Math.sin(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.style.top = - this.options.dialRadius - - Math.cos(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.innerHTML = Timepicker._addLeadingZero(i); - this.minutesView.appendChild(tick); - } - } - _handleAmPmClick = (e) => { - this._handleAmPmInteraction(e.target); - }; - _handleAmPmKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleAmPmInteraction(e.target); - } - }; - _handleAmPmInteraction = (e) => { - this.amOrPm = e.classList.contains('am-btn') ? 'AM' : 'PM'; - this._updateAmPmView(); - }; - _updateAmPmView() { - if (this.options.twelveHour) { - if (this.amOrPm === 'PM') { - this._amBtn.classList.remove('filled'); - this._pmBtn.classList.add('filled'); - } - else if (this.amOrPm === 'AM') { - this._amBtn.classList.add('filled'); - this._pmBtn.classList.remove('filled'); - } - } - } - _updateTimeFromInput() { - // Get the time - let value = ((this.el.value || this.options.defaultTime || '') + '').split(':'); - if (this.options.twelveHour && !(typeof value[1] === 'undefined')) { - if (value[1].toUpperCase().indexOf('AM') > 0) { - this.amOrPm = 'AM'; - } - else { - this.amOrPm = 'PM'; - } - value[1] = value[1].replace('AM', '').replace('PM', ''); - } - if (value[0] === 'now') { - const now = new Date(+new Date() + this.options.fromNow); - value = [now.getHours().toString(), now.getMinutes().toString()]; - if (this.options.twelveHour) { - this.amOrPm = parseInt(value[0]) >= 12 && parseInt(value[0]) < 24 ? 'PM' : 'AM'; - } - } - this.hours = +value[0] || 0; - this.minutes = +value[1] || 0; - this.inputHours.value = Timepicker._addLeadingZero(this.hours); - this.inputMinutes.value = Timepicker._addLeadingZero(this.minutes); - this._updateAmPmView(); - } - /** - * Show hours or minutes view on timepicker. - * @param view The name of the view you want to switch to, 'hours' or 'minutes'. - * @param delay - */ - showView = (view, delay = null) => { - if (view === 'minutes' && getComputedStyle(this.hoursView).visibility === 'visible') ; - const isHours = view === 'hours', nextView = isHours ? this.hoursView : this.minutesView, hideView = isHours ? this.minutesView : this.hoursView; - this.currentView = view; - /*if (isHours) { - this.inputHours.classList.add('text-primary'); - this.inputMinutes.classList.remove('text-primary'); - } else { - this.inputHours.classList.remove('text-primary'); - this.inputMinutes.classList.add('text-primary'); - }*/ - // Transition view - hideView.classList.add('timepicker-dial-out'); - nextView.style.visibility = 'visible'; - nextView.classList.remove('timepicker-dial-out'); - // Reset clock hand - this.resetClock(delay); - // After transitions ended - clearTimeout(this.toggleViewTimer); - this.toggleViewTimer = setTimeout(() => { - hideView.style.visibility = 'hidden'; - }, this.options.duration); - }; - resetClock(delay) { - const view = this.currentView, value = this[view], isHours = view === 'hours', unit = Math.PI / (isHours ? 6 : 30), radian = value * unit, radius = isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius, x = Math.sin(radian) * radius, y = -Math.cos(radian) * radius; - if (delay) { - this._canvas?.classList.add('timepicker-canvas-out'); - setTimeout(() => { - this._canvas?.classList.remove('timepicker-canvas-out'); - this.setHand(x, y); - }, delay); - } - else { - this.setHand(x, y); - } - } - _inputFromTextField = () => { - const isHours = this.currentView === 'hours'; - if (isHours && this.inputHours.value !== '') { - const value = parseInt(this.inputHours.value); - if (value > 0 && value < (this.options.twelveHour ? 13 : 24)) { - this.hours = value; - } - else { - this.setHoursDefault(); - } - this.drawClockFromTimeInput(this.hours, isHours); - } - else if (!isHours && this.inputMinutes.value !== '') { - const value = parseInt(this.inputMinutes.value); - if (value >= 0 && value < 60) { - this.minutes = value; - } - else { - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = this.minutes.toString(); - } - this.drawClockFromTimeInput(this.minutes, isHours); - } - }; - drawClockFromTimeInput(value, isHours) { - const unit = Math.PI / (isHours ? 6 : 30); - const radian = value * unit; - let radius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - else { - radius = - isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius; - } - this.setClockAttributes(radian, radius); - } - setHand(x, y, roundBy5 = false) { - const isHours = this.currentView === 'hours', unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), inner = isHours && z < (this.options.outerRadius + this.options.innerRadius) / 2; - let radian = Math.atan2(x, -y), radius = inner ? this.options.innerRadius : this.options.outerRadius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - // Radian should in range [0, 2PI] - if (radian < 0) { - radian = Math.PI * 2 + radian; - } - // Get the round value - let value = Math.round(radian / unit); - // Get the round radian - radian = value * unit; - // Correct the hours or minutes - if (this.options.twelveHour) { - if (isHours) { - if (value === 0) - value = 12; - } - else { - if (roundBy5) - value *= 5; - if (value === 60) - value = 0; - } - } - else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } - else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } - } - } - // Once hours or minutes changed, vibrate the device - if (this[this.currentView] !== value) { - if (this.vibrate && this.options.vibrate) { - // Do not vibrate too frequently - if (!this.vibrateTimer) { - navigator[this.vibrate](10); - this.vibrateTimer = setTimeout(() => { - this.vibrateTimer = null; - }, 100); - } - } - } - this[this.currentView] = value; - if (isHours) { - this.inputHours.value = Timepicker._addLeadingZero(value); - } - else { - this.inputMinutes.value = Timepicker._addLeadingZero(value); - } - // Set clock hand and others' position - this.setClockAttributes(radian, radius); - } - setClockAttributes(radian, radius) { - const cx1 = Math.sin(radian) * (radius - this.options.tickRadius), cy1 = -Math.cos(radian) * (radius - this.options.tickRadius), cx2 = Math.sin(radian) * radius, cy2 = -Math.cos(radian) * radius; - this.hand.setAttribute('x2', cx1.toString()); - this.hand.setAttribute('y2', cy1.toString()); - this.bg.setAttribute('cx', cx2.toString()); - this.bg.setAttribute('cy', cy2.toString()); - } - formatHours() { - if (this.inputHours.value == '') - this.setHoursDefault(); - this.inputHours.value = Timepicker._addLeadingZero(Number(this.inputHours.value)); - } - formatMinutes() { - if (this.inputMinutes.value == '') - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = Timepicker._addLeadingZero(Number(this.inputMinutes.value)); - } - setHoursDefault() { - this.hours = new Date().getHours(); - this.inputHours.value = (this.hours % (this.options.twelveHour ? 12 : 24)).toString(); - } - done = (clearValue = null) => { - // Set input value - const last = this.el.value; - let value = clearValue - ? '' - : Timepicker._addLeadingZero(this.hours) + ':' + Timepicker._addLeadingZero(this.minutes); - this.time = value; - if (!clearValue && this.options.twelveHour) { - value = `${value} ${this.amOrPm}`; - } - this.el.value = value; - // Trigger change event - if (value !== last) { - this.el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - } - }; - confirm = () => { - this.done(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onDone === 'function') - this.options.onDone.call(this); - }; - cancel = () => { - // not logical clearing the input field on cancel, since the end user might want to make use of the previously submitted value - // this.clear(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - clear = () => { - // @todo should clear timepicker hour/minute input elems and reset analog clock, currently clears input el - this.done(true); - }; - // deprecated - open() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Timepicker._template = `
-
-
-
-
- -
-
- : -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
`; - } -} - -const _defaults$7 = { - text: '', - displayLength: 4000, - inDuration: 300, - outDuration: 375, - classes: '', - completeCallback: null, - activationPercent: 0.8 -}; -class Toast { - /** The toast element. */ - el; - /** - * The remaining amount of time in ms that the toast - * will stay before dismissal. - */ - timeRemaining; - /** - * Describes the current pan state of the Toast. - */ - panning; - options; - message; - counterInterval; - wasSwiped; - startingXPos; - xPos; - time; - deltaX; - velocityX; - static _toasts; - static _container; - static _draggedToast; - constructor(options) { - this.options = { - ...Toast.defaults, - ...options - }; - this.message = this.options.text; - this.panning = false; - this.timeRemaining = this.options.displayLength; - if (Toast._toasts.length === 0) { - Toast._createContainer(); - } - // Create new toast - Toast._toasts.push(this); - const toastElement = this._createToast(); - toastElement['M_Toast'] = this; - this.el = toastElement; - this._animateIn(); - this._setTimer(); - } - static get defaults() { - return _defaults$7; - } - static getInstance(el) { - return el['M_Toast']; - } - static _createContainer() { - const container = document.createElement('div'); - container.setAttribute('id', 'toast-container'); - // Add event handler - container.addEventListener('touchstart', Toast._onDragStart); - container.addEventListener('touchmove', Toast._onDragMove); - container.addEventListener('touchend', Toast._onDragEnd); - container.addEventListener('mousedown', Toast._onDragStart); - document.addEventListener('mousemove', Toast._onDragMove); - document.addEventListener('mouseup', Toast._onDragEnd); - document.body.appendChild(container); - Toast._container = container; - } - static _removeContainer() { - document.removeEventListener('mousemove', Toast._onDragMove); - document.removeEventListener('mouseup', Toast._onDragEnd); - if (Toast._container) { - Toast._container.remove(); - Toast._container = null; - } - } - static _onDragStart(e) { - if (e.target && e.target.closest('.toast')) { - const toastElem = e.target.closest('.toast'); - const toast = toastElem['M_Toast']; - toast.panning = true; - Toast._draggedToast = toast; - toast.el.classList.add('panning'); - toast.el.style.transition = ''; - toast.startingXPos = Toast._xPos(e); - toast.time = Date.now(); - toast.xPos = Toast._xPos(e); - } - } - static _onDragMove(e) { - if (!!Toast._draggedToast) { - e.preventDefault(); - const toast = Toast._draggedToast; - toast.deltaX = Math.abs(toast.xPos - Toast._xPos(e)); - toast.xPos = Toast._xPos(e); - toast.velocityX = toast.deltaX / (Date.now() - toast.time); - toast.time = Date.now(); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - toast.el.style.transform = `translateX(${totalDeltaX}px)`; - toast.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString(); - } - } - static _onDragEnd() { - if (!!Toast._draggedToast) { - const toast = Toast._draggedToast; - toast.panning = false; - toast.el.classList.remove('panning'); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || toast.velocityX > 1; - // Remove toast - if (shouldBeDismissed) { - toast.wasSwiped = true; - toast.dismiss(); - // Animate toast back to original position - } - else { - toast.el.style.transition = 'transform .2s, opacity .2s'; - toast.el.style.transform = ''; - toast.el.style.opacity = ''; - } - Toast._draggedToast = null; - } - } - static _xPos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return e.targetTouches[0].clientX; - } - // mouse event - return e.clientX; - } - /** - * dismiss all toasts. - */ - static dismissAll() { - for (const toastIndex in Toast._toasts) { - Toast._toasts[toastIndex].dismiss(); - } - } - _createToast() { - let toast = this.options.toastId - ? document.getElementById(this.options.toastId) - : document.createElement('div'); - if (toast instanceof HTMLTemplateElement) { - const node = toast.content.cloneNode(true); - toast = node.firstElementChild; - } - toast.classList.add('toast'); - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - toast.setAttribute('aria-atomic', 'true'); - // Add custom classes onto toast - if (this.options.classes.length > 0) { - toast.classList.add(...this.options.classes.split(' ')); - } - if (this.message) - toast.innerText = this.message; - if (Toast._container) { - Toast._container.appendChild(toast); - } - return toast; - } - _animateIn() { - // Animate toast in - this.el.style.display = ''; - this.el.style.opacity = '0'; - // easeOutCubic - this.el.style.transition = ` - top ${this.options.inDuration}ms ease, - opacity ${this.options.inDuration}ms ease - `; - setTimeout(() => { - this.el.style.top = '0'; - this.el.style.opacity = '1'; - }, 1); - } - /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. - */ - _setTimer() { - if (this.timeRemaining !== Infinity) { - this.counterInterval = setInterval(() => { - // If toast is not being dragged, decrease its time remaining - if (!this.panning) { - this.timeRemaining -= 20; - } - // Animate toast out - if (this.timeRemaining <= 0) { - this.dismiss(); - } - }, 20); - } - } - /** - * Dismiss toast with animation. - */ - dismiss() { - clearInterval(this.counterInterval); - const activationDistance = this.el.offsetWidth * this.options.activationPercent; - if (this.wasSwiped) { - this.el.style.transition = 'transform .05s, opacity .05s'; - this.el.style.transform = `translateX(${activationDistance}px)`; - this.el.style.opacity = '0'; - } - // easeOutExpo - this.el.style.transition = ` - margin ${this.options.outDuration}ms ease, - opacity ${this.options.outDuration}ms ease`; - setTimeout(() => { - this.el.style.opacity = '0'; - this.el.style.marginTop = '-40px'; - }, 1); - setTimeout(() => { - // Call the optional callback - if (typeof this.options.completeCallback === 'function') { - this.options.completeCallback(); - } - // Remove toast from DOM - if (this.el.id != this.options.toastId) { - this.el.remove(); - Toast._toasts.splice(Toast._toasts.indexOf(this), 1); - if (Toast._toasts.length === 0) { - Toast._removeContainer(); - } - } - }, this.options.outDuration); - } - static { - Toast._toasts = []; - Toast._container = null; - Toast._draggedToast = null; - } -} - -const _defaults$6 = { - exitDelay: 200, - enterDelay: 0, - text: '', - margin: 5, - inDuration: 250, - outDuration: 200, - position: 'bottom', - transitionMovement: 10, - opacity: 1 -}; -class Tooltip extends Component { - /** - * If tooltip is open. - */ - isOpen; - /** - * If tooltip is hovered. - */ - isHovered; - /** - * If tooltip is focused. - */ - isFocused; - tooltipEl; - _exitDelayTimeout; - _enterDelayTimeout; - xMovement; - yMovement; - constructor(el, options) { - super(el, options, Tooltip); - this.el['M_Tooltip'] = this; - this.options = { - ...Tooltip.defaults, - ...this._getAttributeOptions(), - ...options - }; - this.isOpen = false; - this.isHovered = false; - this.isFocused = false; - this._appendTooltipEl(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$6; - } - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tooltip); - } - static getInstance(el) { - return el['M_Tooltip']; - } - destroy() { - this.tooltipEl.remove(); - this._removeEventHandlers(); - this.el['M_Tooltip'] = undefined; - } - _appendTooltipEl() { - this.tooltipEl = document.createElement('div'); - this.tooltipEl.classList.add('material-tooltip'); - const tooltipContentEl = this.options.tooltipId - ? document.getElementById(this.options.tooltipId) - : document.createElement('div'); - this.tooltipEl.append(tooltipContentEl); - tooltipContentEl.style.display = ''; - tooltipContentEl.classList.add('tooltip-content'); - this._setTooltipContent(tooltipContentEl); - this.tooltipEl.appendChild(tooltipContentEl); - document.body.appendChild(this.tooltipEl); - } - _setTooltipContent(tooltipContentEl) { - if (this.options.tooltipId) - return; - tooltipContentEl.innerText = this.options.text; - } - _updateTooltipContent() { - this._setTooltipContent(this.tooltipEl.querySelector('.tooltip-content')); - } - _setupEventHandlers() { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.el.addEventListener('focus', this._handleFocus, true); - this.el.addEventListener('blur', this._handleBlur, true); - } - _removeEventHandlers() { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.el.removeEventListener('focus', this._handleFocus, true); - this.el.removeEventListener('blur', this._handleBlur, true); - } - /** - * Show tooltip. - */ - open = (isManual) => { - if (this.isOpen) - return; - isManual = isManual === undefined ? true : undefined; // Default value true - this.isOpen = true; - // Update tooltip content with HTML attribute options - this.options = { ...this.options, ...this._getAttributeOptions() }; - this._updateTooltipContent(); - this._setEnterDelayTimeout(isManual); - }; - /** - * Hide tooltip. - */ - close = () => { - if (!this.isOpen) - return; - this.isHovered = false; - this.isFocused = false; - this.isOpen = false; - this._setExitDelayTimeout(); - }; - _setExitDelayTimeout() { - clearTimeout(this._exitDelayTimeout); - this._exitDelayTimeout = setTimeout(() => { - if (this.isHovered || this.isFocused) - return; - this._animateOut(); - }, this.options.exitDelay); - } - _setEnterDelayTimeout(isManual) { - clearTimeout(this._enterDelayTimeout); - this._enterDelayTimeout = setTimeout(() => { - if (!this.isHovered && !this.isFocused && !isManual) - return; - this._animateIn(); - }, this.options.enterDelay); - } - _positionTooltip() { - const tooltip = this.tooltipEl; - const origin = this.el, originHeight = origin.offsetHeight, originWidth = origin.offsetWidth, tooltipHeight = tooltip.offsetHeight, tooltipWidth = tooltip.offsetWidth, margin = this.options.margin; - this.xMovement = 0; - this.yMovement = 0; - let targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(); - let targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (this.options.position === 'top') { - targetTop += -tooltipHeight - margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = -this.options.transitionMovement; - } - else if (this.options.position === 'right') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += originWidth + margin; - this.xMovement = this.options.transitionMovement; - } - else if (this.options.position === 'left') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += -tooltipWidth - margin; - this.xMovement = -this.options.transitionMovement; - } - else { - targetTop += originHeight + margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = this.options.transitionMovement; - } - const newCoordinates = this._repositionWithinScreen(targetLeft, targetTop, tooltipWidth, tooltipHeight); - tooltip.style.top = newCoordinates.y + 'px'; - tooltip.style.left = newCoordinates.x + 'px'; - } - _repositionWithinScreen(x, y, width, height) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - const bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - const offset = this.options.margin + this.options.transitionMovement; - const edges = Utils.checkWithinContainer(document.body, bounding, offset); - if (edges.left) { - newX = offset; - } - else if (edges.right) { - newX -= newX + width - window.innerWidth; - } - if (edges.top) { - newY = offset; - } - else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; - } - _animateIn() { - this._positionTooltip(); - this.tooltipEl.style.visibility = 'visible'; - const duration = this.options.inDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(${this.xMovement}px) translateY(${this.yMovement}px)`; - this.tooltipEl.style.opacity = (this.options.opacity || 1).toString(); - }, 1); - } - _animateOut() { - const duration = this.options.outDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(0px) translateY(0px)`; - this.tooltipEl.style.opacity = '0'; - }, 1); - /* - anim.remove(this.tooltipEl); - anim({ - targets: this.tooltipEl, - opacity: 0, - translateX: 0, - translateY: 0, - duration: this.options.outDuration, - easing: 'easeOutCubic' - }); - */ - } - _handleMouseEnter = () => { - this.isHovered = true; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.open(false); - }; - _handleMouseLeave = () => { - this.isHovered = false; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.close(); - }; - _handleFocus = () => { - if (Utils.tabPressed) { - this.isFocused = true; - this.open(false); - } - }; - _handleBlur = () => { - this.isFocused = false; - this.close(); - }; - _getAttributeOptions() { - const attributeOptions = {}; - const tooltipTextOption = this.el.getAttribute('data-tooltip'); - const tooltipId = this.el.getAttribute('data-tooltip-id'); - const positionOption = this.el.getAttribute('data-position'); - if (tooltipTextOption) { - attributeOptions.text = tooltipTextOption; - } - if (positionOption) { - attributeOptions.position = positionOption; - } - if (tooltipId) { - attributeOptions.tooltipId = tooltipId; - } - return attributeOptions; - } -} - -const _defaults$5 = {}; -// TODO: !!!!! -class Range extends Component { - _mousedown; - value; - thumb; - constructor(el, options) { - super(el, options, Range); - this.el['M_Range'] = this; - this.options = { - ...Range.defaults, - ...options - }; - this._mousedown = false; - this._setupThumb(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$5; - } - /** - * Initializes instances of Range. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Range); - } - static getInstance(el) { - return el['M_Range']; - } - destroy() { - this._removeEventHandlers(); - this._removeThumb(); - this.el['M_Range'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('change', this._handleRangeChange); - this.el.addEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.addEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.addEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.addEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.addEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _removeEventHandlers() { - this.el.removeEventListener('change', this._handleRangeChange); - this.el.removeEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.removeEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.removeEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _handleRangeChange = () => { - this.value.innerHTML = this.el.value; - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - }; - _handleRangeMousedownTouchstart = (e) => { - // Set indicator value - this.value.innerHTML = this.el.value; - this._mousedown = true; - this.el.classList.add('active'); - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - if (e.type !== 'input') { - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - } - }; - _handleRangeInputMousemoveTouchmove = () => { - if (this._mousedown) { - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - this.value.innerHTML = this.el.value; - } - }; - _handleRangeMouseupTouchend = () => { - this._mousedown = false; - this.el.classList.remove('active'); - }; - _handleRangeBlurMouseoutTouchleave = () => { - if (!this._mousedown) { - const paddingLeft = parseInt(getComputedStyle(this.el).paddingLeft); - const marginLeftText = 7 + paddingLeft + 'px'; - if (this.thumb.classList.contains('active')) { - const duration = 100; - // from - this.thumb.style.transition = 'none'; - setTimeout(() => { - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '0'; - this.thumb.style.width = '0'; - this.thumb.style.top = '0'; - this.thumb.style.marginLeft = marginLeftText; - }, 1); - } - this.thumb.classList.remove('active'); - } - }; - _setupThumb() { - this.thumb = document.createElement('span'); - this.value = document.createElement('span'); - this.thumb.classList.add('thumb'); - this.value.classList.add('value'); - this.thumb.append(this.value); - this.el.after(this.thumb); - } - _removeThumb() { - this.thumb.remove(); - } - _showRangeBubble() { - const paddingLeft = parseInt(getComputedStyle(this.thumb.parentElement).paddingLeft); - const marginLeftText = -7 + paddingLeft + 'px'; // TODO: fix magic number? - const duration = 300; - // easeOutQuint - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '30px'; - this.thumb.style.width = '30px'; - this.thumb.style.top = '-30px'; - this.thumb.style.marginLeft = marginLeftText; - } - _calcRangeOffset() { - const width = this.el.getBoundingClientRect().width - 15; - const max = parseFloat(this.el.getAttribute('max')) || 100; // Range default max - const min = parseFloat(this.el.getAttribute('min')) || 0; // Range default min - const percent = (parseFloat(this.el.value) - min) / (max - min); - return percent * width; - } - /** - * Initializes every range input in the current document. - */ - static Init() { - if (typeof document !== 'undefined') - Range.init(document?.querySelectorAll('input[type=range]'), {}); - } -} - -const _defaults$4 = { - onOpen: null, - onClose: null -}; -class TapTarget extends Component { - /** - * If the tap target is open. - */ - isOpen; - static _taptargets; - wrapper; - // private _origin: HTMLElement; - originEl; - waveEl; - contentEl; - constructor(el, options) { - super(el, options, TapTarget); - this.el['M_TapTarget'] = this; - this.options = { - ...TapTarget.defaults, - ...options - }; - this.isOpen = false; - // setup - this.originEl = document.querySelector(`#${el.dataset.target}`); - this.originEl.tabIndex = 0; - this._setup(); - this._calculatePositioning(); - this._setupEventHandlers(); - TapTarget._taptargets.push(this); - } - static get defaults() { - return _defaults$4; - } - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, TapTarget); - } - static getInstance(el) { - return el['M_TapTarget']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_TapTarget'] = undefined; - const index = TapTarget._taptargets.indexOf(this); - if (index >= 0) { - TapTarget._taptargets.splice(index, 1); - } - } - _setupEventHandlers() { - this.originEl.addEventListener('click', this._handleTargetToggle); - this.originEl.addEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.addEventListener('click', this._handleOriginClick); - // Resize - window.addEventListener('resize', this._handleThrottledResize); - } - _removeEventHandlers() { - this.originEl.removeEventListener('click', this._handleTargetToggle); - this.originEl.removeEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.removeEventListener('click', this._handleOriginClick); - window.removeEventListener('resize', this._handleThrottledResize); - } - _handleThrottledResize = () => Utils.throttle(this._handleResize, 200).bind(this); - _handleKeyboardInteraction = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleTargetToggle(); - } - }; - _handleTargetToggle = () => { - if (!this.isOpen) - this.open(); - else - this.close(); - }; - /*_handleOriginClick = () => { - this.close(); - }*/ - _handleResize = () => { - this._calculatePositioning(); - }; - _handleDocumentClick = (e) => { - if (e.target.closest(`#${this.el.dataset.target}`) !== this.originEl && - !e.target.closest('.tap-target-wrapper')) { - this.close(); - // e.preventDefault(); - // e.stopPropagation(); - } - }; - _setup() { - // Creating tap target - this.wrapper = this.el.parentElement; - this.waveEl = this.wrapper.querySelector('.tap-target-wave'); - this.el.parentElement.ariaExpanded = 'false'; - this.originEl.style.zIndex = '1002'; - // this.originEl = this.wrapper.querySelector('.tap-target-origin'); - this.contentEl = this.el.querySelector('.tap-target-content'); - // Creating wrapper - if (!this.wrapper.classList.contains('.tap-target-wrapper')) { - this.wrapper = document.createElement('div'); - this.wrapper.classList.add('tap-target-wrapper'); - this.el.before(this.wrapper); - this.wrapper.append(this.el); - } - // Creating content - if (!this.contentEl) { - this.contentEl = document.createElement('div'); - this.contentEl.classList.add('tap-target-content'); - this.el.append(this.contentEl); - } - // Creating foreground wave - if (!this.waveEl) { - this.waveEl = document.createElement('div'); - this.waveEl.classList.add('tap-target-wave'); - // Creating origin - /*if (!this.originEl) { - this.originEl = this._origin.cloneNode(true); // .clone(true, true); - this.originEl.classList.add('tap-target-origin'); - this.originEl.removeAttribute('id'); - this.originEl.removeAttribute('style'); - this.waveEl.append(this.originEl); - }*/ - this.wrapper.append(this.waveEl); - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - _calculatePositioning() { - // Element or parent is fixed position? - let isFixed = getComputedStyle(this.originEl).position === 'fixed'; - if (!isFixed) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let currentElem = this.originEl; - const parents = []; - while ((currentElem = currentElem.parentNode) && currentElem !== document) - parents.push(currentElem); - for (let i = 0; i < parents.length; i++) { - isFixed = getComputedStyle(parents[i]).position === 'fixed'; - if (isFixed) - break; - } - } - // Calculating origin - const originWidth = this.originEl.offsetWidth; - const originHeight = this.originEl.offsetHeight; - const originTop = isFixed - ? this._offset(this.originEl).top - Utils.getDocumentScrollTop() - : this._offset(this.originEl).top; - const originLeft = isFixed - ? this._offset(this.originEl).left - Utils.getDocumentScrollLeft() - : this._offset(this.originEl).left; - // Calculating screen - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - const scrollBarWidth = windowWidth - document.documentElement.clientWidth; - const centerX = windowWidth / 2; - const centerY = windowHeight / 2; - const isLeft = originLeft <= centerX; - const isRight = originLeft > centerX; - const isTop = originTop <= centerY; - const isBottom = originTop > centerY; - const isCenterX = originLeft >= windowWidth * 0.25 && originLeft <= windowWidth * 0.75; - // Calculating tap target - const tapTargetWidth = this.el.offsetWidth; - const tapTargetHeight = this.el.offsetHeight; - const tapTargetTop = originTop + originHeight / 2 - tapTargetHeight / 2; - const tapTargetLeft = originLeft + originWidth / 2 - tapTargetWidth / 2; - const tapTargetPosition = isFixed ? 'fixed' : 'absolute'; - // Calculating content - const tapTargetTextWidth = isCenterX ? tapTargetWidth : tapTargetWidth / 2 + originWidth; - const tapTargetTextHeight = tapTargetHeight / 2; - const tapTargetTextTop = isTop ? tapTargetHeight / 2 : 0; - const tapTargetTextBottom = 0; - const tapTargetTextLeft = isLeft && !isCenterX ? tapTargetWidth / 2 - originWidth : 0; - const tapTargetTextRight = 0; - const tapTargetTextPadding = originWidth; - const tapTargetTextAlign = isBottom ? 'bottom' : 'top'; - // Calculating wave - const tapTargetWaveWidth = originWidth > originHeight ? originWidth * 2 : originWidth * 2; - const tapTargetWaveHeight = tapTargetWaveWidth; - const tapTargetWaveTop = tapTargetHeight / 2 - tapTargetWaveHeight / 2; - const tapTargetWaveLeft = tapTargetWidth / 2 - tapTargetWaveWidth / 2; - // Setting tap target - this.wrapper.style.top = isTop ? tapTargetTop + 'px' : ''; - this.wrapper.style.right = isRight - ? windowWidth - tapTargetLeft - tapTargetWidth - scrollBarWidth + 'px' - : ''; - this.wrapper.style.bottom = isBottom - ? windowHeight - tapTargetTop - tapTargetHeight + 'px' - : ''; - this.wrapper.style.left = isLeft ? tapTargetLeft + 'px' : ''; - this.wrapper.style.position = tapTargetPosition; - // Setting content - this.contentEl.style.width = tapTargetTextWidth + 'px'; - this.contentEl.style.height = tapTargetTextHeight + 'px'; - this.contentEl.style.top = tapTargetTextTop + 'px'; - this.contentEl.style.right = tapTargetTextRight + 'px'; - this.contentEl.style.bottom = tapTargetTextBottom + 'px'; - this.contentEl.style.left = tapTargetTextLeft + 'px'; - this.contentEl.style.padding = tapTargetTextPadding + 'px'; - this.contentEl.style.verticalAlign = tapTargetTextAlign; - // Setting wave - this.waveEl.style.top = tapTargetWaveTop + 'px'; - this.waveEl.style.left = tapTargetWaveLeft + 'px'; - this.waveEl.style.width = tapTargetWaveWidth + 'px'; - this.waveEl.style.height = tapTargetWaveHeight + 'px'; - } - /** - * Open Tap Target. - */ - open = () => { - if (this.isOpen) - return; - // onOpen callback - if (typeof this.options.onOpen === 'function') { - this.options.onOpen.call(this, this.originEl); - } - this.isOpen = true; - this.wrapper.classList.add('open'); - this.wrapper.ariaExpanded = 'true'; - document.body.addEventListener('click', this._handleDocumentClick, true); - document.body.addEventListener('keypress', this._handleDocumentClick, true); - document.body.addEventListener('touchend', this._handleDocumentClick); - }; - /** - * Close Tap Target. - */ - close = () => { - if (!this.isOpen) - return; - // onClose callback - if (typeof this.options.onClose === 'function') { - this.options.onClose.call(this, this.originEl); - } - this.isOpen = false; - this.wrapper.classList.remove('open'); - this.wrapper.ariaExpanded = 'false'; - document.body.removeEventListener('click', this._handleDocumentClick, true); - document.body.removeEventListener('keypress', this._handleDocumentClick, true); - document.body.removeEventListener('touchend', this._handleDocumentClick); - }; - static { - TapTarget._taptargets = []; - } -} - -const _defaults$3 = Object.freeze({}); -class CharacterCounter extends Component { - /** Stores the reference to the counter HTML element. */ - counterEl; - /** Specifies whether the input is valid or not. */ - isInvalid; - /** Specifies whether the input text has valid length or not. */ - isValidLength; - constructor(el, options) { - super(el, {}, CharacterCounter); - this.el['M_CharacterCounter'] = this; - this.options = { - ...CharacterCounter.defaults, - ...options - }; - this.isInvalid = false; - this.isValidLength = false; - this._setupCounter(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$3; - } - /** - * Initializes instances of CharacterCounter. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, CharacterCounter); - } - static getInstance(el) { - return el['M_CharacterCounter']; - } - destroy() { - this._removeEventHandlers(); - this.el['CharacterCounter'] = undefined; - this._removeCounter(); - } - _setupEventHandlers() { - this.el.addEventListener('focus', this.updateCounter, true); - this.el.addEventListener('input', this.updateCounter, true); - } - _removeEventHandlers() { - this.el.removeEventListener('focus', this.updateCounter, true); - this.el.removeEventListener('input', this.updateCounter, true); - } - _setupCounter() { - this.counterEl = document.createElement('span'); - this.counterEl.classList.add('character-counter'); - this.counterEl.style.float = 'right'; - this.counterEl.style.fontSize = '12px'; - this.counterEl.style.height = '1'; - this.el.parentElement.appendChild(this.counterEl); - } - _removeCounter() { - this.counterEl.remove(); - } - updateCounter = () => { - const maxLength = parseInt(this.el.getAttribute('maxlength')), actualLength = this.el.value.length; - this.isValidLength = actualLength <= maxLength; - let counterString = actualLength.toString(); - if (maxLength) { - counterString += '/' + maxLength; - this._validateInput(); - } - this.counterEl.innerHTML = counterString; - }; - _validateInput() { - if (this.isValidLength && this.isInvalid) { - this.isInvalid = false; - this.el.classList.remove('invalid'); - } - else if (!this.isValidLength && !this.isInvalid) { - this.isInvalid = true; - this.el.classList.remove('valid'); - this.el.classList.add('invalid'); - } - } -} - -const _defaults$2 = { - responsiveThreshold: 0 // breakpoint for swipeable -}; -class Parallax extends Component { - _enabled; - _img; - static _parallaxes = []; - static _handleScrollThrottled; - static _handleWindowResizeThrottled; - constructor(el, options) { - super(el, options, Parallax); - this.el['M_Parallax'] = this; - this.options = { - ...Parallax.defaults, - ...options - }; - this._enabled = window.innerWidth > this.options.responsiveThreshold; - this._img = this.el.querySelector('img'); - this._updateParallax(); - this._setupEventHandlers(); - this._setupStyles(); - Parallax._parallaxes.push(this); - } - static get defaults() { - return _defaults$2; - } - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Parallax); - } - static getInstance(el) { - return el['M_Parallax']; - } - destroy() { - Parallax._parallaxes.splice(Parallax._parallaxes.indexOf(this), 1); - this._img.style.transform = ''; - this._removeEventHandlers(); - this.el['M_Parallax'] = undefined; - } - static _handleScroll() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._updateParallax.call(parallaxInstance); - } - } - static _handleWindowResize() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._enabled = window.innerWidth > parallaxInstance.options.responsiveThreshold; - } - } - _setupEventHandlers() { - this._img.addEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - if (!Parallax._handleScrollThrottled) { - Parallax._handleScrollThrottled = Utils.throttle(Parallax._handleScroll, 5); - } - if (!Parallax._handleWindowResizeThrottled) { - Parallax._handleWindowResizeThrottled = Utils.throttle(Parallax._handleWindowResize, 5); - } - window.addEventListener('scroll', Parallax._handleScrollThrottled); - window.addEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _removeEventHandlers() { - this._img.removeEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - window.removeEventListener('scroll', Parallax._handleScrollThrottled); - window.removeEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _setupStyles() { - this._img.style.opacity = '1'; - } - _handleImageLoad = () => { - this._updateParallax(); - }; - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateParallax() { - const containerHeight = this.el.getBoundingClientRect().height > 0 ? this.el.parentElement.offsetHeight : 500; - const imgHeight = this._img.offsetHeight; - const parallaxDist = imgHeight - containerHeight; - const bottom = this._offset(this.el).top + containerHeight; - const top = this._offset(this.el).top; - const scrollTop = Utils.getDocumentScrollTop(); - const windowHeight = window.innerHeight; - const windowBottom = scrollTop + windowHeight; - const percentScrolled = (windowBottom - top) / (containerHeight + windowHeight); - const parallax = parallaxDist * percentScrolled; - if (!this._enabled) { - this._img.style.transform = ''; - } - else if (bottom > scrollTop && top < scrollTop + windowHeight) { - this._img.style.transform = `translate3D(-50%, ${parallax}px, 0)`; - } - } -} - -const _defaults$1 = { - top: 0, - bottom: Infinity, - offset: 0, - onPositionChange: null -}; -class Pushpin extends Component { - static _pushpins; - originalOffset; - constructor(el, options) { - super(el, options, Pushpin); - this.el['M_Pushpin'] = this; - this.options = { - ...Pushpin.defaults, - ...options - }; - this.originalOffset = this.el.offsetTop; - Pushpin._pushpins.push(this); - this._setupEventHandlers(); - this._updatePosition(); - } - static get defaults() { - return _defaults$1; - } - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Pushpin); - } - static getInstance(el) { - return el['M_Pushpin']; - } - destroy() { - this.el.style.top = null; - this._removePinClasses(); - // Remove pushpin Inst - const index = Pushpin._pushpins.indexOf(this); - Pushpin._pushpins.splice(index, 1); - if (Pushpin._pushpins.length === 0) { - this._removeEventHandlers(); - } - this.el['M_Pushpin'] = undefined; - } - static _updateElements() { - for (const elIndex in Pushpin._pushpins) { - const pInstance = Pushpin._pushpins[elIndex]; - pInstance._updatePosition(); - } - } - _setupEventHandlers() { - document.addEventListener('scroll', Pushpin._updateElements); - } - _removeEventHandlers() { - document.removeEventListener('scroll', Pushpin._updateElements); - } - _updatePosition() { - const scrolled = Utils.getDocumentScrollTop() + this.options.offset; - if (this.options.top <= scrolled && - this.options.bottom >= scrolled && - !this.el.classList.contains('pinned')) { - this._removePinClasses(); - this.el.style.top = `${this.options.offset}px`; - this.el.classList.add('pinned'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pinned'); - } - } - // Add pin-top (when scrolled position is above top) - if (scrolled < this.options.top && !this.el.classList.contains('pin-top')) { - this._removePinClasses(); - this.el.style.top = '0'; - this.el.classList.add('pin-top'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-top'); - } - } - // Add pin-bottom (when scrolled position is below bottom) - if (scrolled > this.options.bottom && !this.el.classList.contains('pin-bottom')) { - this._removePinClasses(); - this.el.classList.add('pin-bottom'); - this.el.style.top = `${this.options.bottom - this.originalOffset}px`; - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-bottom'); - } - } - } - _removePinClasses() { - // IE 11 bug (can't remove multiple classes in one line) - this.el.classList.remove('pin-top'); - this.el.classList.remove('pinned'); - this.el.classList.remove('pin-bottom'); - } - static { - Pushpin._pushpins = []; - } -} - -const _defaults = { - throttle: 100, - scrollOffset: 200, // offset - 200 allows elements near bottom of page to scroll - activeClass: 'active', - getActiveElement: (id) => { - return 'a[href="#' + id + '"]'; - }, - keepTopElementActive: false, - animationDuration: null -}; -class ScrollSpy extends Component { - static _elements; - static _count; - static _increment; - static _elementsInView; - static _visibleElements; - static _ticks; - static _keptTopActiveElement = null; - tickId; - id; - constructor(el, options) { - super(el, options, ScrollSpy); - this.el['M_ScrollSpy'] = this; - this.options = { - ...ScrollSpy.defaults, - ...options - }; - ScrollSpy._elements.push(this); - ScrollSpy._count++; - ScrollSpy._increment++; - this.tickId = -1; - this.id = ScrollSpy._increment.toString(); - this._setupEventHandlers(); - this._handleWindowScroll(); - } - static get defaults() { - return _defaults; - } - /** - * Initializes instances of ScrollSpy. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, ScrollSpy); - } - static getInstance(el) { - return el['M_ScrollSpy']; - } - destroy() { - ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1); - ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1); - ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.el), 1); - ScrollSpy._count--; - this._removeEventHandlers(); - const actElem = document.querySelector(this.options.getActiveElement(this.el.id)); - actElem.classList.remove(this.options.activeClass); - this.el['M_ScrollSpy'] = undefined; - } - _setupEventHandlers() { - if (ScrollSpy._count === 1) { - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleThrottledResize); - document.body.addEventListener('click', this._handleTriggerClick); - } - } - _removeEventHandlers() { - if (ScrollSpy._count === 0) { - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleThrottledResize); - document.body.removeEventListener('click', this._handleTriggerClick); - } - } - _handleThrottledResize = () => Utils.throttle(this._handleWindowScroll, 200).bind(this); - _handleTriggerClick = (e) => { - const trigger = e.target; - for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) { - const scrollspy = ScrollSpy._elements[i]; - const x = document.querySelector('a[href="#' + scrollspy.el.id + '"]'); - if (trigger === x) { - e.preventDefault(); - if (scrollspy.el['M_ScrollSpy'].options.animationDuration) { - ScrollSpy._smoothScrollIntoView(scrollspy.el, scrollspy.el['M_ScrollSpy'].options.animationDuration); - } - else { - scrollspy.el.scrollIntoView({ behavior: 'smooth' }); - } - break; - } - } - }; - _handleWindowScroll = () => { - // unique tick id - ScrollSpy._ticks++; - // viewport rectangle - const top = Utils.getDocumentScrollTop(), left = Utils.getDocumentScrollLeft(), right = left + window.innerWidth, bottom = top + window.innerHeight; - // determine which elements are in view - const intersections = ScrollSpy._findElements(top, right, bottom, left); - for (let i = 0; i < intersections.length; i++) { - const scrollspy = intersections[i]; - const lastTick = scrollspy.tickId; - if (lastTick < 0) { - // entered into view - scrollspy._enter(); - } - // update tick id - scrollspy.tickId = ScrollSpy._ticks; - } - for (let i = 0; i < ScrollSpy._elementsInView.length; i++) { - const scrollspy = ScrollSpy._elementsInView[i]; - const lastTick = scrollspy.tickId; - if (lastTick >= 0 && lastTick !== ScrollSpy._ticks) { - // exited from view - scrollspy._exit(); - scrollspy.tickId = -1; - } - } - // remember elements in view for next tick - ScrollSpy._elementsInView = intersections; - if (ScrollSpy._elements.length) { - const options = ScrollSpy._elements[0].el['M_ScrollSpy'].options; - if (options.keepTopElementActive && ScrollSpy._visibleElements.length === 0) { - this._resetKeptTopActiveElementIfNeeded(); - const topElements = ScrollSpy._elements - .filter((value) => ScrollSpy._getDistanceToViewport(value.el) <= 0) - .sort((a, b) => { - const distanceA = ScrollSpy._getDistanceToViewport(a.el); - const distanceB = ScrollSpy._getDistanceToViewport(b.el); - if (distanceA < distanceB) - return -1; - if (distanceA > distanceB) - return 1; - return 0; - }); - const nearestTopElement = topElements.length - ? topElements[topElements.length - 1] - : ScrollSpy._elements[0]; - const actElem = document.querySelector(options.getActiveElement(nearestTopElement.el.id)); - actElem?.classList.add(options.activeClass); - ScrollSpy._keptTopActiveElement = actElem; - } - } - }; - static _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - static _findElements(top, right, bottom, left) { - const hits = []; - for (let i = 0; i < ScrollSpy._elements.length; i++) { - const scrollspy = ScrollSpy._elements[i]; - const currTop = top + scrollspy.options.scrollOffset || 200; - if (scrollspy.el.getBoundingClientRect().height > 0) { - const elTop = ScrollSpy._offset(scrollspy.el).top, elLeft = ScrollSpy._offset(scrollspy.el).left, elRight = elLeft + scrollspy.el.getBoundingClientRect().width, elBottom = elTop + scrollspy.el.getBoundingClientRect().height; - const isIntersect = !(elLeft > right || - elRight < left || - elTop > bottom || - elBottom < currTop); - if (isIntersect) { - hits.push(scrollspy); - } - } - } - return hits; - } - _enter() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - if (ScrollSpy._visibleElements[0]['M_ScrollSpy'] && - this.id < ScrollSpy._visibleElements[0]['M_ScrollSpy'].id) { - ScrollSpy._visibleElements.unshift(this.el); - } - else { - ScrollSpy._visibleElements.push(this.el); - } - } - else { - ScrollSpy._visibleElements.push(this.el); - } - this._resetKeptTopActiveElementIfNeeded(); - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - } - _exit() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((x) => x.id != this.el.id); - if (ScrollSpy._visibleElements[0]) { - // Check if empty - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - this._resetKeptTopActiveElementIfNeeded(); - } - } - } - _resetKeptTopActiveElementIfNeeded() { - if (ScrollSpy._keptTopActiveElement) { - ScrollSpy._keptTopActiveElement.classList.remove(this.options.activeClass); - ScrollSpy._keptTopActiveElement = null; - } - } - static _getDistanceToViewport(element) { - const rect = element.getBoundingClientRect(); - const distance = rect.top; - return distance; - } - static _smoothScrollIntoView(element, duration = 300) { - const targetPosition = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset); - const startPosition = window.scrollY || window.pageYOffset; - const distance = targetPosition - startPosition; - const startTime = performance.now(); - function scrollStep(currentTime) { - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - const scrollY = startPosition + distance * progress; - if (progress < 1) { - window.scrollTo(0, scrollY); - requestAnimationFrame(scrollStep); - } - else { - window.scrollTo(0, targetPosition); - } - } - requestAnimationFrame(scrollStep); - } - static { - ScrollSpy._elements = []; - ScrollSpy._elementsInView = []; - ScrollSpy._visibleElements = []; // Array. - ScrollSpy._count = 0; - ScrollSpy._increment = 0; - ScrollSpy._ticks = 0; - } -} - -// type RGBColor = { -// r: number; -// g: number; -// b: number; -// }; - -// type Position = { -// x: number; -// y: number; -// }; - -class Waves { - /** - * - * @param {HTMLElement} el - * @returns - */ - static #offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - - // https://phoenix-dx.com/css-techniques-for-material-ripple-effect/ - - /** - * - * @param {HTMLElement} targetElement - * @param position - * @param color - */ - static renderWaveEffect(targetElement, position = null, color = null) { - const isCentered = position === null; - const duration = 500; - let animationFrame, animationStart; - - const animationStep = (timestamp) => { - if (!animationStart) animationStart = timestamp; - const frame = timestamp - animationStart; - if (frame < duration) { - const easing = (frame / duration) * (2 - frame / duration); - const circle = isCentered - ? 'circle at 50% 50%' - : `circle at ${position.x}px ${position.y}px`; - const waveColor = `rgba(${color?.r || 0}, ${color?.g || 0}, ${color?.b || 0}, ${0.3 * (1 - easing)})`; - const stop = 90 * easing + '%'; - targetElement.style.backgroundImage = - 'radial-gradient(' + - circle + - ', ' + - waveColor + - ' ' + - stop + - ', transparent ' + - stop + - ')'; - animationFrame = window.requestAnimationFrame(animationStep); - } else { - targetElement.style.backgroundImage = 'none'; - window.cancelAnimationFrame(animationFrame); - } - }; - - animationFrame = window.requestAnimationFrame(animationStep); - } - - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.body.addEventListener('click', (e) => { - const trigger = e.target; - const el = trigger.closest('.waves-effect'); - if (el && el.contains(trigger)) { - const isCircular = el.classList.contains('waves-circle'); - const x = e.pageX - this.#offset(el).left; - const y = e.pageY - this.#offset(el).top; - let color = null; - if (el.classList.contains('waves-light')) color = { r: 255, g: 255, b: 255 }; - this.renderWaveEffect(el, isCircular ? null : { x, y }, color); - } - }); - }); - } -} - -/* eslint-disable @typescript-eslint/no-unused-vars */ -const version = '2.3.3'; -/** - * Automatically initialize components. - * @param context Root element to initialize. Defaults to `document.body`. - * @param options Options for each component. - */ -function AutoInit(context = document.body, options) { - const registry = { - Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'), - Cards: context.querySelectorAll('.cards:not(.no-autoinit)'), - Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'), - Chips: context.querySelectorAll('.chips:not(.no-autoinit)'), - Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'), - Datepicker: context.querySelectorAll('.datepicker:not(.no-autoinit)'), - Dropdown: context.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'), - Materialbox: context.querySelectorAll('.materialboxed:not(.no-autoinit)'), - Modal: context.querySelectorAll('.modal:not(.no-autoinit)'), - Parallax: context.querySelectorAll('.parallax:not(.no-autoinit)'), - Pushpin: context.querySelectorAll('.pushpin:not(.no-autoinit)'), - ScrollSpy: context.querySelectorAll('.scrollspy:not(.no-autoinit)'), - FormSelect: context.querySelectorAll('select:not(.no-autoinit)'), - Sidenav: context.querySelectorAll('.sidenav:not(.no-autoinit)'), - Tabs: context.querySelectorAll('.tabs:not(.no-autoinit)'), - TapTarget: context.querySelectorAll('.tap-target:not(.no-autoinit)'), - Timepicker: context.querySelectorAll('.timepicker:not(.no-autoinit)'), - Tooltip: context.querySelectorAll('.tooltipped:not(.no-autoinit)'), - FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)') - }; - Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {}); - Cards.init(registry.Cards, options?.Cards ?? {}); - Carousel.init(registry.Carousel, options?.Carousel ?? {}); - Chips.init(registry.Chips, options?.Chips ?? {}); - Collapsible.init(registry.Collapsible, options?.Collapsible ?? {}); - Datepicker.init(registry.Datepicker, options?.Datepicker ?? {}); - Dropdown.init(registry.Dropdown, options?.Dropdown ?? {}); - Materialbox.init(registry.Materialbox, options?.Materialbox ?? {}); - Modal.init(registry.Modal, options?.Modal ?? {}); - Parallax.init(registry.Parallax, options?.Parallax ?? {}); - Pushpin.init(registry.Pushpin, options?.Pushpin ?? {}); - ScrollSpy.init(registry.ScrollSpy, options?.ScrollSpy ?? {}); - FormSelect.init(registry.FormSelect, options?.FormSelect ?? {}); - Sidenav.init(registry.Sidenav, options?.Sidenav ?? {}); - Tabs.init(registry.Tabs, options?.Tabs ?? {}); - TapTarget.init(registry.TapTarget, options?.TapTarget ?? {}); - Timepicker.init(registry.Timepicker, options?.Timepicker ?? {}); - Tooltip.init(registry.Tooltip, options?.Tooltip ?? {}); - FloatingActionButton.init(registry.FloatingActionButton, options?.FloatingActionButton ?? {}); -} -// Init -if (typeof document !== 'undefined') { - document.addEventListener('keydown', Utils.docHandleKeydown, true); - document.addEventListener('keyup', Utils.docHandleKeyup, true); - document.addEventListener('focus', Utils.docHandleFocus, true); - document.addEventListener('blur', Utils.docHandleBlur, true); -} -Forms.Init(); -Chips.Init(); -Waves.Init(); -Range.Init(); -Cards.Init(); - -exports.AutoInit = AutoInit; -exports.Autocomplete = Autocomplete; -exports.Cards = Cards; -exports.Carousel = Carousel; -exports.CharacterCounter = CharacterCounter; -exports.Chips = Chips; -exports.Collapsible = Collapsible; -exports.Datepicker = Datepicker; -exports.Dropdown = Dropdown; -exports.FloatingActionButton = FloatingActionButton; -exports.FormSelect = FormSelect; -exports.Forms = Forms; -exports.Materialbox = Materialbox; -exports.Modal = Modal; -exports.Parallax = Parallax; -exports.Pushpin = Pushpin; -exports.Range = Range; -exports.ScrollSpy = ScrollSpy; -exports.Sidenav = Sidenav; -exports.Slider = Slider; -exports.Tabs = Tabs; -exports.TapTarget = TapTarget; -exports.Timepicker = Timepicker; -exports.Toast = Toast; -exports.Tooltip = Tooltip; -exports.Waves = Waves; -exports.version = version; diff --git a/dist/js/materialize.d.ts b/dist/js/materialize.d.ts deleted file mode 100644 index dcfdd7a314..0000000000 --- a/dist/js/materialize.d.ts +++ /dev/null @@ -1,2625 +0,0 @@ -/** - * Base options for component initialization. - */ -interface BaseOptions$1 { -} -type MElement = HTMLElement | Element; -type InitElements = NodeListOf | HTMLCollectionOf; -type ComponentConstructor, O extends BaseOptions$1> = { - new (el: HTMLElement, options: Partial): T; -}; -type ComponentType, O extends BaseOptions$1> = ComponentConstructor & typeof Component; -interface I18nOptions { - cancel: string; - clear: string; - done: string; -} -interface Openable { - isOpen: boolean; - open(): void; - close(): void; -} -/** - * Base class implementation for Materialize components. - */ -declare class Component { - /** - * The DOM element the plugin was initialized with. - */ - el: HTMLElement; - /** - * The options the instance was initialized with. - */ - options: O; - /** - * Constructs component instance and set everything up. - */ - constructor(el: HTMLElement, options: Partial, classDef: ComponentType, O>); - /** - * Initializes component instance. - * @param el HTML element. - * @param options Component options. - * @param classDef Class definition. - */ - protected static init>(el: I, options: O, classDef: ComponentType): C; - /** - * Initializes component instances. - * @param els HTML elements. - * @param options Component options. - * @param classDef Class definition. - */ - protected static init>(els: InitElements, options: Partial, classDef: ComponentType): C[]; - /** - * Initializes component instances. - * @param els HTML elements. - * @param options Component options. - * @param classDef Class definition. - */ - protected static init>(els: I | InitElements, options: Partial, classDef: ComponentType): C | C[]; - /** - * @returns default options for component instance. - */ - static get defaults(): BaseOptions$1; - /** - * Retrieves component instance for the given element. - * @param el Associated HTML Element. - */ - static getInstance(el: HTMLElement): Component; - /** - * Destroy plugin instance and teardown. - */ - destroy(): void; -} - -interface DropdownOptions extends BaseOptions$1 { - /** - * Defines the edge the menu is aligned to. - * @default 'left' - */ - alignment: 'left' | 'right'; - /** - * If true, automatically focus dropdown el for keyboard. - * @default true - */ - autoFocus: boolean; - /** - * If true, constrainWidth to the size of the dropdown activator. - * @default true - */ - constrainWidth: boolean; - /** - * Provide an element that will be the bounding container of the dropdown. - * @default null - */ - container: Element; - /** - * If false, the dropdown will show below the trigger. - * @default true - */ - coverTrigger: boolean; - /** - * If true, close dropdown on item click. - * @default true - */ - closeOnClick: boolean; - /** - * If true, the dropdown will open on hover. - * @default false - */ - hover: boolean; - /** - * The duration of the transition enter in milliseconds. - * @default 150 - */ - inDuration: number; - /** - * The duration of the transition out in milliseconds. - * @default 250 - */ - outDuration: number; - /** - * Function called when dropdown starts entering. - * @default null - */ - onOpenStart: (el: HTMLElement) => void; - /** - * Function called when dropdown finishes entering. - * @default null - */ - onOpenEnd: (el: HTMLElement) => void; - /** - * Function called when dropdown starts exiting. - * @default null - */ - onCloseStart: (el: HTMLElement) => void; - /** - * Function called when dropdown finishes exiting. - * @default null - */ - onCloseEnd: (el: HTMLElement) => void; - /** - * Function called when item is clicked. - * @default null - */ - onItemClick: (el: HTMLLIElement) => void; -} -declare class Dropdown extends Component implements Openable { - static _dropdowns: Dropdown[]; - /** ID of the dropdown element. */ - id: string; - /** The DOM element of the dropdown. */ - dropdownEl: HTMLElement; - /** If the dropdown is open. */ - isOpen: boolean; - /** If the dropdown content is scrollable. */ - isScrollable: boolean; - isTouchMoving: boolean; - /** The index of the item focused. */ - focusedIndex: number; - filterQuery: string[]; - filterTimeout: NodeJS.Timeout | number; - constructor(el: HTMLElement, options: Partial); - static get defaults(): DropdownOptions; - /** - * Initializes instance of Dropdown. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Dropdown; - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Dropdown[]; - static getInstance(el: HTMLElement): Dropdown; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _setupTemporaryEventHandlers(): void; - _removeTemporaryEventHandlers(): void; - _handleClick: (e: MouseEvent) => void; - _handleMouseEnter: () => void; - _handleMouseLeave: (e: MouseEvent) => void; - _handleDocumentClick: (e: MouseEvent) => void; - _handleTriggerKeydown: (e: KeyboardEvent) => void; - _handleDocumentTouchmove: (e: TouchEvent) => void; - _handleDropdownClick: (e: MouseEvent) => void; - _handleDropdownKeydown: (e: KeyboardEvent) => void; - _handleWindowResize: () => void; - _resetFilterQuery: () => void; - _resetDropdownStyles(): void; - _resetDropdownPositioningStyles(): void; - _moveDropdownToElement(containerEl?: HTMLElement): void; - _makeDropdownFocusable(): void; - _focusFocusedItem(): void; - _getDropdownPosition(closestOverflowParent: HTMLElement): { - x: number; - y: number; - verticalAlignment: string; - horizontalAlignment: "left" | "right"; - height: number; - width: number; - }; - _animateIn(): void; - _animateOut(): void; - private _getClosestAncestor; - _placeDropdown(): void; - /** - * Open dropdown. - */ - open: () => void; - /** - * Close dropdown. - */ - close: () => void; - /** - * While dropdown is open, you can recalculate its dimensions if its contents have changed. - */ - recalculateDimensions: () => void; -} - -interface AutocompleteData { - /** - * A primitive value that can be converted to string. - * If "text" is not provided, it will also be used as "option text" as well - */ - id: string | number; - /** - * This optional attribute is used as "display value" for the current entry. - * When provided, it will also be taken into consideration by the standard search function. - */ - text?: string; - /** - * This optional attribute is used to provide a valid image URL to the current option. - */ - image?: string; - /** - * Optional attributes which describes the option. - */ - description?: string; -} -interface AutocompleteOptions extends BaseOptions$1 { - /** - * Data object defining autocomplete options with - * optional icon strings. - */ - data: AutocompleteData[]; - /** - * Flag which can be set if multiple values can be selected. The Result will be an Array. - * @default false - */ - isMultiSelect: boolean; - /** - * Callback for when autocompleted. - */ - onAutocomplete: (entries: AutocompleteData[]) => void; - /** - * Minimum number of characters before autocomplete starts. - * @default 1 - */ - minLength: number; - /** - * The height of the Menu which can be set via css-property. - * @default '300px' - */ - maxDropDownHeight: string; - /** - * Function is called when the input text is altered and data can also be loaded asynchronously. - * If the results are collected the items in the list can be updated via the function setMenuItems(collectedItems). - * @param text Searched text. - * @param autocomplete Current autocomplete instance. - */ - onSearch: (text: string, autocomplete: Autocomplete) => void; - /** - * If true will render the key from each item directly as HTML. - * User input MUST be properly sanitized first. - * @default false - */ - allowUnsafeHTML: boolean; - /** - * Pass options object to select dropdown initialization. - * @default {} - */ - dropdownOptions: Partial; - /** - * Predefined selected values - */ - selected: number[] | string[]; -} -declare class Autocomplete extends Component { - el: HTMLInputElement; - /** If the autocomplete is open. */ - isOpen: boolean; - /** Number of matching autocomplete options. */ - count: number; - /** Index of the current selected option. */ - activeIndex: number; - private oldVal; - private $active; - private _mousedown; - container: HTMLElement; - /** Instance of the dropdown plugin for this autocomplete. */ - dropdown: Dropdown; - static _keydown: boolean; - selectedValues: AutocompleteData[]; - menuItems: AutocompleteData[]; - data: AutocompleteData[]; - constructor(el: HTMLInputElement, options: Partial); - static get defaults(): AutocompleteOptions; - /** - * Initializes instance of Autocomplete. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLInputElement, options?: Partial): Autocomplete; - /** - * Initializes instances of Autocomplete. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Autocomplete[]; - static getInstance(el: HTMLElement): Autocomplete; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _setupDropdown(): void; - _removeDropdown(): void; - _handleInputBlur: () => void; - _handleInputKeyup: (e: KeyboardEvent) => void; - _handleInputFocus: () => void; - _inputChangeDetection: (value: string) => void; - _handleInputKeydown: (e: KeyboardEvent) => void; - _handleInputClick: () => void; - _handleContainerMousedownAndTouchstart: () => void; - _handleContainerMouseupAndTouchend: () => void; - _resetCurrentElementPosition(): void; - _resetAutocomplete(): void; - _highlightPartialText(input: string, label: string): string[]; - _createDropdownItem(entry: AutocompleteData): HTMLLIElement; - _renderDropdown(): void; - _setStatusLoading(): void; - _updateSelectedInfo(): void; - _refreshInputText(): void; - _triggerChanged(): void; - /** - * Show autocomplete. - */ - open: () => void; - /** - * Hide autocomplete. - */ - close: () => void; - /** - * Updates the visible or selectable items shown in the menu. - * @param menuItems Items to be available. - * @param selected Selected item ids - * @param open Option to conditionally open dropdown - * @param initial Condition to set initial data - */ - setMenuItems(menuItems: AutocompleteData[], selected?: number[] | string[], open?: boolean, initial?: boolean): void; - /** - * Sets selected values. - * @deprecated @see https://github.com/materializecss/materialize/issues/552 - * @param entries - */ - setValues(entries: AutocompleteData[]): void; - /** - * Select a specific autocomplete option via id-property. - * @param id The id of a data-entry. - */ - selectOption(id: number | string): void; - selectOptions(ids: []): void; -} - -interface FloatingActionButtonOptions extends BaseOptions$1 { - /** - * Direction FAB menu opens. - * @default "top" - */ - direction: 'top' | 'right' | 'bottom' | 'left'; - /** - * true: FAB menu appears on hover, false: FAB menu appears on click. - * @default true - */ - hoverEnabled: boolean; - /** - * Enable transit the FAB into a toolbar on click. - * @default false - */ - toolbarEnabled: boolean; -} -declare class FloatingActionButton extends Component implements Openable { - #private; - /** - * Describes open/close state of FAB. - */ - isOpen: boolean; - offsetY: number; - offsetX: number; - btnBottom: number; - btnLeft: number; - btnWidth: number; - constructor(el: HTMLElement, options: Partial); - static get defaults(): FloatingActionButtonOptions; - /** - * Initializes instance of FloatingActionButton. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): FloatingActionButton; - /** - * Initializes instances of FloatingActionButton. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): FloatingActionButton[]; - static getInstance(el: HTMLElement): FloatingActionButton; - destroy(): void; - /** - * Open FAB. - */ - open: () => void; - /** - * Close FAB. - */ - close: () => void; -} - -interface CardsOptions extends BaseOptions$1 { - onOpen: (el: Element) => void; - onClose: (el: Element) => void; - inDuration: number; - outDuration: number; -} -declare class Cards extends Component implements Openable { - #private; - isOpen: boolean; - constructor(el: HTMLElement, options: Partial); - static get defaults(): CardsOptions; - /** - * Initializes instance of Cards. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Cards; - /** - * Initializes instances of Cards. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Cards[]; - static getInstance(el: HTMLElement): Cards; - /** - * {@inheritDoc} - */ - destroy(): void; - /** - * Show card reveal. - */ - open: () => void; - /** - * Hide card reveal. - */ - close: () => void; - static Init(): void; -} - -interface CarouselOptions extends BaseOptions$1 { - /** - * Transition duration in milliseconds. - * @default 200 - */ - duration: number; - /** - * Perspective zoom. If 0, all items are the same size. - * @default -100 - */ - dist: number; - /** - * Set the spacing of the center item. - * @default 0 - */ - shift: number; - /** - * Set the padding between non center items. - * @default 0 - */ - padding: number; - /** - * Set the number of visible items. - * @default 5 - */ - numVisible: number; - /** - * Make the carousel a full width slider like the second example. - * @default false - */ - fullWidth: boolean; - /** - * Set to true to show indicators. - * @default false - */ - indicators: boolean; - /** - * Don't wrap around and cycle through items. - * @default false - */ - noWrap: boolean; - /** - * Callback for when a new slide is cycled to. - * @default null - */ - onCycleTo: (current: Element, dragged: boolean) => void; -} -declare class Carousel extends Component { - hasMultipleSlides: boolean; - showIndicators: boolean; - noWrap: boolean; - /** If the carousel is being clicked or tapped. */ - pressed: boolean; - /** If the carousel is currently being dragged. */ - dragged: boolean; - offset: number; - target: number; - images: HTMLElement[]; - itemWidth: number; - itemHeight: number; - dim: number; - _indicators: HTMLUListElement; - count: number; - xform: string; - verticalDragged: boolean; - reference: number; - referenceY: number; - velocity: number; - frame: number; - timestamp: number; - ticker: string | number | NodeJS.Timeout; - amplitude: number; - /** The index of the center carousel item. */ - center: number; - imageHeight: number; - scrollingTimeout: number | NodeJS.Timeout; - oneTimeCallback: (current: Element, dragged: boolean) => void | null; - constructor(el: HTMLElement, options: Partial); - static get defaults(): CarouselOptions; - /** - * Initializes instance of Carousel. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Carousel; - /** - * Initializes instances of Carousel. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Carousel[]; - static getInstance(el: HTMLElement): Carousel; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleThrottledResize: () => void; - _handleCarouselTap: (e: MouseEvent | TouchEvent) => void; - _handleCarouselDrag: (e: MouseEvent | TouchEvent) => boolean; - _handleCarouselRelease: (e: MouseEvent | TouchEvent) => boolean; - _handleCarouselClick: (e: MouseEvent | TouchEvent) => boolean; - _handleIndicatorClick: (e: Event) => void; - _handleIndicatorKeyPress: (e: KeyboardEvent) => void; - _handleIndicatorInteraction: (e: Event) => void; - _handleResize: () => void; - _setCarouselHeight(imageOnly?: boolean): void; - _xpos(e: MouseEvent | TouchEvent): number; - _ypos(e: MouseEvent | TouchEvent): number; - _wrap(x: number): any; - _track: () => void; - _autoScroll: () => void; - _scroll(x?: number): void; - _updateItemStyle(el: HTMLElement, opacity: number, zIndex: number, transform: string): void; - _cycleTo(n: number, callback?: CarouselOptions['onCycleTo']): void; - /** - * Move carousel to next slide or go forward a given amount of slides. - * @param n How many times the carousel slides. - */ - next(n?: number): void; - /** - * Move carousel to previous slide or go back a given amount of slides. - * @param n How many times the carousel slides. - */ - prev(n?: number): void; - /** - * Move carousel to nth slide. - * @param n Index of slide. - * @param callback "onCycleTo" optional callback. - */ - set(n: number, callback?: CarouselOptions['onCycleTo']): void; -} - -interface ChipData { - /** Unique identifier. */ - id: number | string; - /** Chip text. If not specified, "id" will be used. */ - text?: string; - /** Chip image (URL). */ - image?: string; -} -interface ChipsOptions extends BaseOptions$1 { - /** - * Set the chip data. - * @default [] - */ - data: ChipData[]; - /** - * Set first placeholder when there are no tags. - * @default "" - */ - placeholder: string; - /** - * Set second placeholder when adding additional tags. - * @default "" - */ - secondaryPlaceholder: string; - /** - * Set autocomplete options. - * @default {} - */ - autocompleteOptions: Partial; - /** - * Toggles abililty to add custom value not in autocomplete list. - * @default false - */ - autocompleteOnly: boolean; - /** - * Set chips limit. - * @default Infinity - */ - limit: number; - /** - * Specifies class to be used in "close" button (useful when working with Material Symbols icon set). - * @default 'material-icons' - */ - closeIconClass: string; - /** - * Specifies option to render user input field - * @default false; - */ - allowUserInput: boolean; - /** - * Callback for chip add. - * @default null - */ - onChipAdd: (element: HTMLElement, chip: HTMLElement) => void; - /** - * Callback for chip select. - * @default null - */ - onChipSelect: (element: HTMLElement, chip: HTMLElement) => void; - /** - * Callback for chip delete. - * @default null - */ - onChipDelete: (element: HTMLElement, chip: HTMLElement) => void; -} -declare class Chips extends Component { - #private; - /** Array of the current chips data. */ - chipsData: ChipData[]; - /** If the chips has autocomplete enabled. */ - hasAutocomplete: boolean; - /** Autocomplete instance, if any. */ - autocomplete: Autocomplete; - constructor(el: HTMLElement, options: Partial); - static get defaults(): ChipsOptions; - /** - * Initializes instance of Chips. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Chips; - /** - * Initializes instances of Chips. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Chips[]; - static getInstance(el: HTMLElement): Chips; - getData(): ChipData[]; - destroy(): void; - /** - * Add chip to input. - * @param chip Chip data object - */ - addChip(chip: ChipData): void; - /** - * Delete nth chip. - * @param chipIndex Index of chip - */ - deleteChip(chipIndex: number): void; - /** - * Select nth chip. - * @param chipIndex Index of chip - */ - selectChip(chipIndex: number): void; - static Init(): void; -} - -interface CollapsibleOptions extends BaseOptions$1 { - /** - * If accordion versus collapsible. - * @default true - */ - accordion: boolean; - /** - * Transition in duration in milliseconds. - * @default 300 - */ - inDuration: number; - /** - * Transition out duration in milliseconds. - * @default 300 - */ - outDuration: number; - /** - * Callback function called before collapsible is opened. - * @default null - */ - onOpenStart: (el: Element) => void; - /** - * Callback function called after collapsible is opened. - * @default null - */ - onOpenEnd: (el: Element) => void; - /** - * Callback function called before collapsible is closed. - * @default null - */ - onCloseStart: (el: Element) => void; - /** - * Callback function called after collapsible is closed. - * @default null - */ - onCloseEnd: (el: Element) => void; -} -declare class Collapsible extends Component { - private _headers; - constructor(el: HTMLElement, options: Partial); - static get defaults(): CollapsibleOptions; - /** - * Initializes instance of Collapsible. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Collapsible; - /** - * Initializes instances of Collapsible. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Collapsible[]; - static getInstance(el: HTMLElement): Collapsible; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleCollapsibleClick: (e: MouseEvent | KeyboardEvent) => void; - _handleCollapsibleKeydown: (e: KeyboardEvent) => void; - private _setExpanded; - _animateIn(index: number): void; - _animateOut(index: number): void; - /** - * Open collapsible section. - * @param n Nth section to open. - */ - open: (index: number) => void; - /** - * Close collapsible section. - * @param n Nth section to close. - */ - close: (index: number) => void; -} - -interface DateI18nOptions extends I18nOptions { - previousMonth: string; - nextMonth: string; - months: string[]; - monthsShort: string[]; - weekdays: string[]; - weekdaysShort: string[]; - weekdaysAbbrev: string[]; -} -interface DatepickerOptions extends BaseOptions$1 { - /** - * The date output format for the input field value - * or a function taking the date and outputting the - * formatted date string. - * @default 'mmm dd, yyyy' - */ - format: string | ((d: Date) => string); - /** - * Used to create date object from current input string. - * @default null - */ - parse: ((value: string, format: string) => Date) | null; - /** - * The initial condition if the datepicker is based on date range. - * @default false - */ - isDateRange: boolean; - /** - * The selector of the user specified date range end element - */ - dateRangeEndEl: string | null; - /** - * The initial condition if the datepicker is based on multiple date selection. - * @default false - */ - isMultipleSelection: boolean; - /** - * The initial date to view when first opened. - * @default null - */ - defaultDate: Date | null; - /** - * The initial end date to view when first opened. - * @default null - */ - defaultEndDate: Date | null; - /** - * Make the `defaultDate` the initial selected value. - * @default false - */ - setDefaultDate: boolean; - /** - * Make the `defaultEndDate` the initial selected value. - * @default false - */ - setDefaultEndDate: boolean; - /** - * Prevent selection of any date on the weekend. - * @default false - */ - disableWeekends: boolean; - /** - * Custom function to disable certain days. - * @default null - */ - disableDayFn: ((day: Date) => boolean) | null; - /** - * First day of week (0: Sunday, 1: Monday etc). - * @default 0 - */ - firstDay: number; - /** - * The earliest date that can be selected. - * @default null - */ - minDate: Date | null; - /** - * The latest date that can be selected. - * @default null - */ - maxDate: Date | null; - /** - * Number of years either side, or array of upper/lower range. - * @default 10 - */ - yearRange: number | number[]; - /** - * Sort year range in reverse order. - * @default false - */ - yearRangeReverse: boolean; - /** - * Changes Datepicker to RTL. - * @default false - */ - isRTL: boolean; - /** - * Show month after year in Datepicker title. - * @default false - */ - showMonthAfterYear: boolean; - /** - * Render days of the calendar grid that fall in the next - * or previous month. - * @default false - */ - showDaysInNextAndPreviousMonths: boolean; - /** - * Specify if the docked datepicker is in open state by default - */ - openByDefault: boolean; - /** - * Specify a DOM element OR selector for a DOM element to render - * the calendar in, by default it will be placed before the input. - * @default null - */ - container: HTMLElement | string | null; - /** - * Show the clear button in the datepicker. - * @default false - */ - showClearBtn: boolean; - /** - * Autosubmit calendar day select to input field - * @default false - */ - autoSubmit: true; - /** - * Internationalization options. - */ - i18n: Partial; - /** - * An array of string returned by `Date.toDateString()`, - * indicating there are events in the specified days. - * @default [] - */ - events: string[]; - /** - * Callback function when date is selected, - * first parameter is the newly selected date. - * @default null - */ - onSelect: ((selectedDate: Date) => void) | null; - /** - * Callback function when Datepicker is closed. - * @default null - */ - /** - * Callback function when Datepicker HTML is refreshed. - * @default null - */ - onDraw: (() => void) | null; - /** - * Callback function for interaction with input field. - * @default null - */ - onInputInteraction: (() => void) | null; - /** - * Callback function for interaction with confirm button. - * @default null - */ - onConfirm: (() => void) | null; - /** - * Callback function for interaction with close button. - * @default null - */ - onCancel: (() => void) | null; - /** Field used for internal calculations DO NOT CHANGE IT */ - minYear?: number; - /** Field used for internal calculations DO NOT CHANGE IT */ - maxYear?: number; - /** Field used for internal calculations DO NOT CHANGE IT */ - minMonth?: number; - /** Field used for internal calculations DO NOT CHANGE IT */ - maxMonth?: number; - /** Field used for internal calculations DO NOT CHANGE IT */ - startRange?: Date; - /** Field used for internal calculations DO NOT CHANGE IT */ - endRange?: Date; - /** - * Display plugin - */ - displayPlugin: string; - /** - * Configurable display plugin options - */ - displayPluginOptions: object; -} -declare class Datepicker extends Component { - el: HTMLInputElement; - id: string; - multiple: boolean; - calendarEl: HTMLElement; - /** CLEAR button instance. */ - /** DONE button instance */ - containerEl: HTMLElement; - yearTextEl: HTMLElement; - dateTextEl: HTMLElement; - endDateEl: HTMLInputElement; - dateEls: HTMLInputElement[]; - /** The selected Date. */ - date: Date; - endDate: null | Date; - dates: Date[]; - formats: object; - calendars: [{ - month: number; - year: number; - }]; - private _y; - private _m; - private displayPlugin; - private footer; - static _template: string; - constructor(el: HTMLInputElement, options: Partial); - static get defaults(): DatepickerOptions; - /** - * Initializes instance of Datepicker. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLInputElement, options?: Partial): Datepicker; - /** - * Initializes instances of Datepicker. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Datepicker[]; - static _isDate(obj: any): boolean; - static _isWeekend(date: any): boolean; - /** - * @deprecated as this function has no effect without any return statement or global parameter setter. - */ - static _setToStartOfDay(date: any): void; - static _getDaysInMonth(year: any, month: any): number; - static _isLeapYear(year: any): boolean; - static _compareDates(a: any, b: any): boolean; - static _compareWithinRange(day: Date, date: Date, dateEnd: Date): boolean; - static _comparePastDate(a: Date, b: Date): boolean; - static getInstance(el: HTMLElement): Datepicker; - destroy(): void; - destroySelects(): void; - _insertHTMLIntoDOM(): void; - /** - * Renders the date input format - */ - _renderDateInputFormat(el: HTMLInputElement): void; - /** - * Gets a string representation of the given date. - */ - toString(date?: Date, format?: string | ((d: Date) => string)): string; - /** - * Returns the formatted date. - */ - formatDate(date: Date, format: string): string; - /** - * Sets date from input field. - */ - setDateFromInput(el: HTMLInputElement): void; - /** - * Set a date on the datepicker. - * @param date Date to set on the datepicker. - * @param preventOnSelect Undocumented as of 5 March 2018. - * @param isEndDate - * @param fromUserInput - */ - setDate(date?: Date, preventOnSelect?: boolean, isEndDate?: boolean, fromUserInput?: boolean): void; - validateDate(date: Date): void | Date; - /** - * Set a single date on the datepicker. - * @param date Date to set on the datepicker. - * @param isEndDate - */ - setSingleDate(date: Date, isEndDate: boolean): void; - /** - * Set a multi date on the datepicker. - * @param date Date to set on the datepicker. - */ - setMultiDate(date: Date): void; - /** - * Sets the data-date attribute on the date input field - */ - setDataDate(el: any, date: any): void; - /** - * Sets dates on the input values. - */ - setInputValues(): void; - setMultipleSelectionInputValues(): void; - /** - * Sets given date as the input value on the given element. - */ - setInputValue(el: any, date: any): void; - /** - * Display plugin setup. - */ - _setupDisplayPlugin(): void; - /** - * Renders the date in the modal head section. - */ - _renderDateDisplay(date: Date, endDate?: Date): void; - /** - * Change date view to a specific date on the datepicker. - * @param date Date to show on the datepicker. - */ - gotoDate(date: Date): void; - adjustCalendars(): void; - adjustCalendar(calendar: any): any; - nextMonth(): void; - prevMonth(): void; - render(year: any, month: any, randId: any): string; - renderDay(opts: any): string; - renderRow(days: any, isRTL: any, isRowSelected: any): string; - renderTable(opts: any, data: any, randId: any): string; - renderHead(opts: any): string; - renderBody(rows: any): string; - renderTitle(instance: any, c: any, year: any, month: any, refYear: any, randId: any): string; - draw(): void; - _setupEventHandlers(): void; - _setupVariables(): void; - _pickerSetup(): void; - _removeEventHandlers(): void; - _handleInputClick: (e: any) => void; - _handleInputKeydown: (e: KeyboardEvent) => void; - _handleCalendarClick: (e: any) => void; - _handleDateRangeCalendarClick: (date: Date) => void; - _handleClearClick: () => void; - _clearDates: () => void; - _handleMonthChange: (e: any) => void; - _handleYearChange: (e: any) => void; - gotoMonth(month: any): void; - gotoYear(year: any): void; - _handleInputChange: (e: Event) => void; - renderDayName(opts: any, day: any, abbr?: boolean): any; - createDateInput(): HTMLInputElement; - _finishSelection: () => void; - _confirm: () => void; - _cancel: () => void; - open(): this; - close(): this; -} - -declare class Forms { - /** - * Checks if the label has validation and apply - * the correct class and styles - * @param textfield - */ - static validateField(textfield: HTMLInputElement): void; - /** - * Resizes the given TextArea after updating the - * value content dynamically. - * @param e EventTarget - */ - static textareaAutoResize(e: EventTarget): void; - static Init(): void; - static InitTextarea(textarea: HTMLTextAreaElement): void; - static InitFileInputPath(fileInput: HTMLInputElement): void; -} - -interface MaterialboxOptions extends BaseOptions$1 { - /** - * Transition in duration in milliseconds. - * @default 275 - */ - inDuration: number; - /** - * Transition out duration in milliseconds. - * @default 200 - */ - outDuration: number; - /** - * Callback function called before materialbox is opened. - * @default null - */ - onOpenStart: (el: Element) => void; - /** - * Callback function called after materialbox is opened. - * @default null - */ - onOpenEnd: (el: Element) => void; - /** - * Callback function called before materialbox is closed. - * @default null - */ - onCloseStart: (el: Element) => void; - /** - * Callback function called after materialbox is closed. - * @default null - */ - onCloseEnd: (el: Element) => void; -} -declare class Materialbox extends Component { - /** If the materialbox overlay is showing. */ - overlayActive: boolean; - /** If the materialbox is no longer being animated. */ - doneAnimating: boolean; - /** Caption, if specified. */ - caption: string; - /** Original width of image. */ - originalWidth: number; - /** Original height of image. */ - originalHeight: number; - private originInlineStyles; - private placeholder; - private _changedAncestorList; - private newHeight; - private newWidth; - private windowWidth; - private windowHeight; - private attrWidth; - private attrHeight; - private _overlay; - private _photoCaption; - constructor(el: HTMLElement, options: Partial); - static get defaults(): MaterialboxOptions; - /** - * Initializes instance of MaterialBox. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Materialbox; - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Materialbox[]; - static getInstance(el: HTMLElement): Materialbox; - destroy(): void; - private _setupEventHandlers; - private _removeEventHandlers; - private _handleMaterialboxClick; - private _handleMaterialboxKeypress; - private _handleMaterialboxToggle; - private _handleWindowScroll; - private _handleWindowResize; - private _handleWindowEscape; - private _makeAncestorsOverflowVisible; - private _offset; - private _updateVars; - private _animateImageIn; - private _animateImageOut; - private _addCaption; - private _removeCaption; - private _addOverlay; - private _removeOverlay; - /** - * Open materialbox. - */ - open: () => void; - /** - * Close materialbox. - */ - close: () => void; -} - -interface ModalOptions extends BaseOptions$1 { - opacity: number; - inDuration: number; - outDuration: number; - preventScrolling: boolean; - onOpenStart: (this: Modal, el: HTMLElement) => void; - onOpenEnd: (this: Modal, el: HTMLElement) => void; - onCloseStart: (el: HTMLElement) => void; - onCloseEnd: (el: HTMLElement) => void; - dismissible: boolean; - startingTop: string; - endingTop: string; -} -declare class Modal extends Component { - #private; - constructor(el: HTMLElement, options: Partial); - static get defaults(): { - opacity: number; - inDuration: number; - outDuration: number; - onOpenStart: any; - onOpenEnd: any; - onCloseStart: any; - onCloseEnd: any; - preventScrolling: boolean; - dismissible: boolean; - startingTop: string; - endingTop: string; - }; - static init(el: HTMLElement, options?: Partial): Modal; - static init(els: InitElements, options?: Partial): Modal[]; - static getInstance(el: HTMLElement): Modal; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleTriggerClick(): void; - _handleOverlayClick(): void; - _handleModalCloseClick(): void; - _handleKeydown(): void; - _handleFocus(): void; - open(): this; - close(): this; - static create(config: any): string | HTMLDialogElement; -} - -interface FormSelectOptions extends BaseOptions$1 { - /** - * Classes to be added to the select wrapper element. - * @default "" - */ - classes: string; - /** - * Pass options object to select dropdown initialization. - * @default {} - */ - dropdownOptions: Partial; -} -type ValueStruct = { - el: HTMLOptionElement; - optionEl: HTMLElement; -}; -declare class FormSelect extends Component { - el: HTMLSelectElement; - /** If this is a multiple select. */ - isMultiple: boolean; - /** - * Label associated with the current select element. - * Is "null", if not detected. - */ - labelEl: HTMLLabelElement; - /** Dropdown UL element. */ - dropdownOptions: HTMLUListElement; - /** Text input that shows current selected option. */ - input: HTMLInputElement; - /** Instance of the dropdown plugin for this select. */ - dropdown: Dropdown; - /** The select wrapper element. */ - wrapper: HTMLDivElement; - selectOptions: (HTMLOptionElement | HTMLOptGroupElement)[]; - private _values; - nativeTabIndex: number; - constructor(el: HTMLSelectElement, options: FormSelectOptions); - static get defaults(): FormSelectOptions; - /** - * Initializes instance of FormSelect. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLSelectElement, options?: Partial): FormSelect; - /** - * Initializes instances of FormSelect. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): FormSelect[]; - static getInstance(el: HTMLElement): FormSelect; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleSelectChange: () => void; - _handleOptionClick: (e: MouseEvent | KeyboardEvent) => void; - _arraysEqual(a: T[], b: (E | T)[]): boolean; - _selectOptionElement(virtualOption: HTMLElement): void; - _handleInputClick: () => void; - _setupDropdown(): void; - _addOptionToValues(realOption: HTMLOptionElement, virtualOption: HTMLElement): void; - _removeDropdown(): void; - _createAndAppendOptionWithIcon(realOption: HTMLOptionElement | HTMLOptGroupElement, type: string): HTMLLIElement; - _selectValue(value: ValueStruct): void; - _deselectValue(value: ValueStruct): void; - _deselectAll(): void; - _isValueSelected(value: ValueStruct): boolean; - _toggleEntryFromArray(value: ValueStruct): void; - _getSelectedOptions(): HTMLOptionElement[]; - _setValueToInput(): void; - _setSelectedStates(): void; - _activateOption(ul: HTMLElement, li: HTMLElement): void; - getSelectedValues(): string[]; -} - -interface SidenavOptions extends BaseOptions$1 { - /** - * Side of screen on which Sidenav appears. - * @default 'left' - */ - edge: 'left' | 'right'; - /** - * Allow swipe gestures to open/close Sidenav. - * @default true - */ - draggable: boolean; - /** - * Width of the area where you can start dragging. - * @default '10px' - */ - dragTargetWidth: string; - /** - * Length in ms of enter transition. - * @default 250 - */ - inDuration: number; - /** - * Length in ms of exit transition. - * @default 200 - */ - outDuration: number; - /** - * Prevent page from scrolling while sidenav is open. - * @default true - */ - preventScrolling: boolean; - /** - * Function called when sidenav starts entering. - */ - onOpenStart: (elem: HTMLElement) => void; - /** - * Function called when sidenav finishes entering. - */ - onOpenEnd: (elem: HTMLElement) => void; - /** - * Function called when sidenav starts exiting. - */ - onCloseStart: (elem: HTMLElement) => void; - /** - * Function called when sidenav finishes exiting. - */ - onCloseEnd: (elem: HTMLElement) => void; -} -declare class Sidenav extends Component implements Openable { - id: string; - /** Describes open/close state of Sidenav. */ - isOpen: boolean; - /** Describes if sidenav is fixed. */ - isFixed: boolean; - /** Describes if Sidenav is being dragged. */ - isDragged: boolean; - lastWindowWidth: number; - lastWindowHeight: number; - static _sidenavs: Sidenav[]; - private _overlay; - dragTarget: Element; - private _startingXpos; - private _xPos; - private _time; - private _width; - private _initialScrollTop; - private _verticallyScrolling; - private deltaX; - private velocityX; - private percentOpen; - constructor(el: HTMLElement, options: Partial); - static get defaults(): SidenavOptions; - /** - * Initializes instance of Sidenav. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Sidenav; - /** - * Initializes instances of Sidenav. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Sidenav[]; - static getInstance(el: HTMLElement): Sidenav; - destroy(): void; - private _createOverlay; - private _setupEventHandlers; - private _removeEventHandlers; - private _handleTriggerClick; - private _startDrag; - private _dragMoveUpdate; - private _handleDragTargetDrag; - private _handleDragTargetRelease; - private _handleCloseDrag; - private _calculateDelta; - private _handleCloseRelease; - private _handleCloseTriggerClick; - private _handleWindowResize; - private _setupClasses; - private _removeClasses; - private _setupFixed; - private _isDraggable; - private _isCurrentlyFixed; - private _createDragTarget; - private _preventBodyScrolling; - private _enableBodyScrolling; - /** - * Opens Sidenav. - */ - open: () => void; - /** - * Closes Sidenav. - */ - close: () => void; - private _animateIn; - private _animateOut; - private _animateSidenavIn; - private _animateSidenavOut; - private _animateOverlayIn; - private _animateOverlayOut; - private _setAriaHidden; - private _setTabIndex; -} - -interface SliderOptions extends BaseOptions$1 { - /** - * Set to false to hide slide indicators. - * @default true - */ - indicators: boolean; - /** - * Set height of slider. - * @default 400 - */ - height: number; - /** - * Set the duration of the transition animation in ms. - * @default 500 - */ - duration: number; - /** - * Set the duration between transitions in ms. - * @default 6000 - */ - interval: number; - /** - * If slider should pause when keyboard focus is received. - * @default true - */ - pauseOnFocus: boolean; - /** - * If slider should pause when is hovered by a pointer. - * @default true - */ - pauseOnHover: boolean; - /** - * Optional function used to generate ARIA label to indicators (for accessibility purposes). - * @param index Current index, starting from "1". - * @param current A which indicates whether it is the current element or not - * @returns a string to be used as label indicator. - * @default null - */ - indicatorLabelFunc: (index: number, current: boolean) => string; -} -declare class Slider extends Component { - /** Index of current slide. */ - activeIndex: number; - interval: string | number | NodeJS.Timeout; - eventPause: boolean; - _slider: HTMLUListElement; - _slides: HTMLLIElement[]; - _activeSlide: HTMLLIElement; - _indicators: HTMLLIElement[]; - _hovered: boolean; - _focused: boolean; - _focusCurrent: boolean; - _sliderId: string; - constructor(el: HTMLElement, options: Partial); - static get defaults(): SliderOptions; - /** - * Initializes instance of Slider. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Slider; - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Slider[]; - static getInstance(el: HTMLElement): Slider; - destroy(): void; - private _setupEventHandlers; - private _removeEventHandlers; - private _handleIndicatorClick; - private _handleAutoPauseHover; - private _handleAutoPauseFocus; - private _handleAutoStartHover; - private _handleAutoStartFocus; - private _handleInterval; - private _animateSlide; - private _setSliderHeight; - private _setupIndicators; - private _removeIndicators; - set(index: number): void; - _pause(fromEvent: boolean): void; - /** - * Pause slider autoslide. - */ - pause: () => void; - /** - * Start slider autoslide. - */ - start: () => void; - /** - * Move to next slider. - */ - next: () => void; - /** - * Move to prev slider. - */ - prev: () => void; -} - -interface TabsOptions extends BaseOptions$1 { - /** - * Transition duration in milliseconds. - * @default 300 - */ - duration: number; - /** - * Callback for when a new tab content is shown. - * @default null - */ - onShow: (newContent: Element) => void; - /** - * Set to true to enable swipeable tabs. - * This also uses the responsiveThreshold option. - * @default false - */ - swipeable: boolean; - /** - * The maximum width of the screen, in pixels, - * where the swipeable functionality initializes. - * @default infinity - */ - responsiveThreshold: number; -} -declare class Tabs extends Component { - _tabLinks: NodeListOf; - _index: number; - _indicator: HTMLLIElement; - _tabWidth: number; - _tabsWidth: number; - _tabsCarousel: Carousel; - _activeTabLink: HTMLAnchorElement; - _content: HTMLElement; - constructor(el: HTMLElement, options: Partial); - static get defaults(): TabsOptions; - /** - * Initializes instance of Tabs. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Tabs; - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Tabs[]; - static getInstance(el: HTMLElement): Tabs; - destroy(): void; - /** - * The index of tab that is currently shown. - */ - get index(): number; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleWindowResize: () => void; - _handleTabClick: (e: MouseEvent) => void; - _createIndicator(): void; - _setupActiveTabLink(): void; - _setupSwipeableTabs(): void; - _teardownSwipeableTabs(): void; - _setupNormalTabs(): void; - _teardownNormalTabs(): void; - _setTabsAndTabWidth(): void; - _calcRightPos(el: HTMLElement): number; - _calcLeftPos(el: HTMLElement): number; - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ - updateTabIndicator(): void; - _animateIndicator(prevIndex: number): void; - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ - select(tabId: string): void; -} - -type Views = 'hours' | 'minutes'; -interface TimepickerOptions extends BaseOptions$1 { - /** - * Dial radius. - * @default 135 - */ - dialRadius: number; - /** - * Outer radius. - * @default 105 - */ - outerRadius: number; - /** - * Inner radius. - * @default 70 - */ - innerRadius: number; - /** - * Tick radius. - * @default 20 - */ - tickRadius: number; - /** - * Duration of the transition from/to the hours/minutes view. - * @default 350 - */ - duration: number; - /** - * Specify a DOM element OR selector for a DOM element to render - * the time picker in, by default it will be placed before the input. - * @default null - */ - container: HTMLElement | string | null; - /** - * Show the clear button in the Timepicker. - * @default false - */ - showClearBtn: boolean; - /** - * Autosubmit timepicker selection to input field - * @default true - */ - autoSubmit: true; - /** - * Default time to set on the timepicker 'now' or '13:14'. - * @default 'now'; - */ - defaultTime: string; - /** - * Millisecond offset from the defaultTime. - * @default 0 - */ - fromNow: number; - /** - * Internationalization options. - */ - i18n: Partial; - /** - * Use 12 hour AM/PM clock instead of 24 hour clock. - * @default true - */ - twelveHour: boolean; - /** - * Vibrate device when dragging clock hand. - * @default true - */ - vibrate: boolean; - /** - * Callback function when a time is selected. - * @default null - */ - onSelect: (hour: number, minute: number) => void; - /** - * Callback function for interaction with input field. - * @default null - */ - onInputInteraction: (() => void) | null; - /** - * Callback function for done. - * @default null - */ - onDone: (() => void) | null; - /** - * Callback function for cancel. - * @default null - */ - onCancel: (() => void) | null; - /** - * Display plugin - */ - displayPlugin: string; - /** - * Configurable display plugin options - */ - displayPluginOptions: object; -} -type Point = { - x: number; - y: number; -}; -declare class Timepicker extends Component { - el: HTMLInputElement; - id: string; - containerEl: HTMLElement; - plate: HTMLElement; - digitalClock: HTMLElement; - inputHours: HTMLInputElement; - inputMinutes: HTMLInputElement; - x0: number; - y0: number; - moved: boolean; - dx: number; - dy: number; - /** - * Current view on the timepicker. - * @default 'hours' - */ - currentView: Views; - hand: SVGElement; - minutesView: HTMLElement; - hours: number; - minutes: number; - /** The selected time. */ - time: string; - /** - * If the time is AM or PM on twelve-hour clock. - * @default 'PM' - */ - amOrPm: 'AM' | 'PM'; - static _template: any; - /** Vibrate device when dragging clock hand. */ - vibrate: 'vibrate' | 'webkitVibrate' | null; - _canvas: HTMLElement; - hoursView: HTMLElement; - spanAmPm: HTMLSpanElement; - footer: HTMLElement; - private _amBtn; - private _pmBtn; - bg: Element; - bearing: Element; - g: Element; - toggleViewTimer: string | number | NodeJS.Timeout; - vibrateTimer: NodeJS.Timeout | number; - private displayPlugin; - constructor(el: HTMLInputElement, options: Partial); - static get defaults(): TimepickerOptions; - /** - * Initializes instance of Timepicker. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLInputElement, options?: Partial): Timepicker; - /** - * Initializes instances of Timepicker. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Timepicker[]; - static _addLeadingZero(num: number): string; - static _createSVGEl(name: string): SVGElement; - static _Pos(e: TouchEvent | MouseEvent): Point; - static getInstance(el: HTMLElement): Timepicker; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleInputClick: () => void; - _handleInputKeydown: (e: KeyboardEvent) => void; - _handleTimeInputEnterKey: (e: KeyboardEvent) => void; - _handleClockClickStart: (e: any) => void; - _handleDocumentClickMove: (e: any) => void; - _handleDocumentClickEnd: (e: any) => void; - _insertHTMLIntoDOM(): void; - _setupVariables(): void; - _pickerSetup(): void; - private _setupDisplayPlugin; - _clockSetup(): void; - _buildSVGClock(): void; - _buildHoursView(): void; - _buildHoursTick(i: number, radian: number, radius: number): void; - _buildMinutesView(): void; - _handleAmPmClick: (e: MouseEvent) => void; - _handleAmPmKeypress: (e: KeyboardEvent) => void; - _handleAmPmInteraction: (e: HTMLElement) => void; - _updateAmPmView(): void; - _updateTimeFromInput(): void; - /** - * Show hours or minutes view on timepicker. - * @param view The name of the view you want to switch to, 'hours' or 'minutes'. - * @param delay - */ - showView: (view: Views, delay?: number) => void; - resetClock(delay: any): void; - _inputFromTextField: () => void; - drawClockFromTimeInput(value: any, isHours: any): void; - setHand(x: any, y: any, roundBy5?: boolean): void; - setClockAttributes(radian: number, radius: number): void; - formatHours(): void; - formatMinutes(): void; - setHoursDefault(): void; - done: (clearValue?: any) => void; - confirm: () => void; - cancel: () => void; - clear: () => void; - open(): this; - close(): this; -} - -interface ToastOptions extends BaseOptions$1 { - /** - * The content of the Toast. - * @default "" - */ - text: string; - /** - * Element Id for the tooltip. - * @default "" - */ - toastId?: string; - /** - * Length in ms the Toast stays before dismissal. - * @default 4000 - */ - displayLength: number; - /** - * Transition in duration in milliseconds. - * @default 300 - */ - inDuration: number; - /** - * Transition out duration in milliseconds. - * @default 375 - */ - outDuration: number; - /** - * Classes to be added to the toast element. - * @default "" - */ - classes: string; - /** - * Callback function called when toast is dismissed. - * @default null - */ - completeCallback: () => void; - /** - * The percentage of the toast's width it takes fora drag - * to dismiss a Toast. - * @default 0.8 - */ - activationPercent: number; -} -declare class Toast { - /** The toast element. */ - el: HTMLElement; - /** - * The remaining amount of time in ms that the toast - * will stay before dismissal. - */ - timeRemaining: number; - /** - * Describes the current pan state of the Toast. - */ - panning: boolean; - options: ToastOptions; - message: string; - counterInterval: NodeJS.Timeout | number; - wasSwiped: boolean; - startingXPos: number; - xPos: number; - time: number; - deltaX: number; - velocityX: number; - static _toasts: Toast[]; - private static _container; - static _draggedToast: Toast; - constructor(options: Partial); - static get defaults(): ToastOptions; - static getInstance(el: HTMLElement): Toast; - static _createContainer(): void; - static _removeContainer(): void; - static _onDragStart(e: TouchEvent | MouseEvent): void; - static _onDragMove(e: TouchEvent | MouseEvent): void; - static _onDragEnd(): void; - static _xPos(e: TouchEvent | MouseEvent): number; - /** - * dismiss all toasts. - */ - static dismissAll(): void; - _createToast(): HTMLElement; - _animateIn(): void; - /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. - */ - _setTimer(): void; - /** - * Dismiss toast with animation. - */ - dismiss(): void; -} - -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; -interface TooltipOptions extends BaseOptions$1 { - /** - * Delay time before tooltip disappears. - * @default 200 - */ - exitDelay: number; - /** - * Delay time before tooltip appears. - * @default 0 - */ - enterDelay: number; - /** - * Element Id for the tooltip. - * @default "" - */ - tooltipId?: string; - /** - * Text string for the tooltip. - * @default "" - */ - text: string; - /** - * Set distance tooltip appears away from its activator - * excluding transitionMovement. - * @default 5 - */ - margin: number; - /** - * Enter transition duration. - * @default 300 - */ - inDuration: number; - /** - * Opacity of the tooltip. - * @default 1 - */ - opacity: number; - /** - * Exit transition duration. - * @default 250 - */ - outDuration: number; - /** - * Set the direction of the tooltip. - * @default 'bottom' - */ - position: TooltipPosition; - /** - * Amount in px that the tooltip moves during its transition. - * @default 10 - */ - transitionMovement: number; -} -declare class Tooltip extends Component { - /** - * If tooltip is open. - */ - isOpen: boolean; - /** - * If tooltip is hovered. - */ - isHovered: boolean; - /** - * If tooltip is focused. - */ - isFocused: boolean; - tooltipEl: HTMLElement; - private _exitDelayTimeout; - private _enterDelayTimeout; - xMovement: number; - yMovement: number; - constructor(el: HTMLElement, options: Partial); - static get defaults(): TooltipOptions; - /** - * Initializes instance of Tooltip. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Tooltip; - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Tooltip[]; - static getInstance(el: HTMLElement): Tooltip; - destroy(): void; - _appendTooltipEl(): void; - _setTooltipContent(tooltipContentEl: HTMLElement): void; - _updateTooltipContent(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - /** - * Show tooltip. - */ - open: (isManual: boolean) => void; - /** - * Hide tooltip. - */ - close: () => void; - _setExitDelayTimeout(): void; - _setEnterDelayTimeout(isManual: any): void; - _positionTooltip(): void; - _repositionWithinScreen(x: number, y: number, width: number, height: number): { - x: number; - y: number; - }; - _animateIn(): void; - _animateOut(): void; - _handleMouseEnter: () => void; - _handleMouseLeave: () => void; - _handleFocus: () => void; - _handleBlur: () => void; - _getAttributeOptions(): Partial; -} - -interface RangeOptions extends BaseOptions$1 { -} -declare class Range extends Component { - el: HTMLInputElement; - private _mousedown; - value: HTMLElement; - thumb: HTMLElement; - constructor(el: HTMLInputElement, options: Partial); - static get defaults(): RangeOptions; - /** - * Initializes instance of Range. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLInputElement, options?: Partial): Range; - /** - * Initializes instances of Range. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Range[]; - static getInstance(el: HTMLInputElement): Range; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleRangeChange: () => void; - _handleRangeMousedownTouchstart: (e: MouseEvent | TouchEvent) => void; - _handleRangeInputMousemoveTouchmove: () => void; - _handleRangeMouseupTouchend: () => void; - _handleRangeBlurMouseoutTouchleave: () => void; - _setupThumb(): void; - _removeThumb(): void; - _showRangeBubble(): void; - _calcRangeOffset(): number; - /** - * Initializes every range input in the current document. - */ - static Init(): void; -} - -interface TapTargetOptions extends BaseOptions$1 { - /** - * Callback function called when Tap Target is opened. - * @default null - */ - onOpen: (origin: HTMLElement) => void; - /** - * Callback function called when Tap Target is closed. - * @default null - */ - onClose: (origin: HTMLElement) => void; -} -declare class TapTarget extends Component implements Openable { - /** - * If the tap target is open. - */ - isOpen: boolean; - static _taptargets: TapTarget[]; - private wrapper; - private originEl; - private waveEl; - private contentEl; - constructor(el: HTMLElement, options: Partial); - static get defaults(): TapTargetOptions; - /** - * Initializes instance of TapTarget. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): TapTarget; - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): TapTarget[]; - static getInstance(el: HTMLElement): TapTarget; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleThrottledResize: () => void; - _handleKeyboardInteraction: (e: KeyboardEvent) => void; - _handleTargetToggle: () => void; - _handleResize: () => void; - _handleDocumentClick: (e: MouseEvent | TouchEvent | KeyboardEvent) => void; - _setup(): void; - private _offset; - _calculatePositioning(): void; - /** - * Open Tap Target. - */ - open: () => void; - /** - * Close Tap Target. - */ - close: () => void; -} - -interface BaseOptions { -} -type InputElement = HTMLInputElement | HTMLTextAreaElement; -declare class CharacterCounter extends Component { - el: InputElement; - /** Stores the reference to the counter HTML element. */ - counterEl: HTMLSpanElement; - /** Specifies whether the input is valid or not. */ - isInvalid: boolean; - /** Specifies whether the input text has valid length or not. */ - isValidLength: boolean; - constructor(el: HTMLInputElement | HTMLTextAreaElement, options: Partial); - static get defaults(): BaseOptions; - /** - * Initializes instance of CharacterCounter. - * @param el HTML element. - * @param options Component options. - */ - static init(el: InputElement, options?: Partial): CharacterCounter; - /** - * Initializes instances of CharacterCounter. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): CharacterCounter[]; - static getInstance(el: InputElement): CharacterCounter; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _setupCounter(): void; - _removeCounter(): void; - updateCounter: () => void; - _validateInput(): void; -} - -declare class Edges { - top: boolean; - right: boolean; - bottom: boolean; - left: boolean; -} - -declare class Bounding { - left: number; - top: number; - width: number; - height: number; -} - -/** - * Class with utilitary functions for global usage. - */ -declare class Utils { - /** Specifies wether tab is pressed or not. */ - static tabPressed: boolean; - /** Specifies wether there is a key pressed. */ - static keyDown: boolean; - /** - * Key maps. - */ - static keys: { - TAB: string[]; - ENTER: string[]; - ESC: string[]; - BACKSPACE: string[]; - ARROW_UP: string[]; - ARROW_DOWN: string[]; - ARROW_LEFT: string[]; - ARROW_RIGHT: string[]; - DELETE: string[]; - }; - /** - * Detects when a key is pressed. - * @param e Event instance. - */ - static docHandleKeydown(e: KeyboardEvent): void; - /** - * Detects when a key is released. - * @param e Event instance. - */ - static docHandleKeyup(e: KeyboardEvent): void; - /** - * Detects when document is focused. - * @param e Event instance. - */ - static docHandleFocus(e: FocusEvent): void; - /** - * Detects when document is not focused. - * @param e Event instance. - */ - static docHandleBlur(e: FocusEvent): void; - /** - * Generates a unique string identifier. - */ - static guid(): string; - /** - * Checks for exceeded edges - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkWithinContainer(container: HTMLElement, bounding: Bounding, offset: number): Edges; - /** - * Checks if element can be aligned in multiple directions. - * @param el Element to be inspected. - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkPossibleAlignments(el: HTMLElement, container: HTMLElement, bounding: Bounding, offset: number): { - top: boolean; - right: boolean; - bottom: boolean; - left: boolean; - spaceOnTop: number; - spaceOnRight: number; - spaceOnBottom: number; - spaceOnLeft: number; - }; - /** - * Retrieves target element id from trigger. - * @param trigger Trigger element. - */ - static getIdFromTrigger(trigger: HTMLElement): string; - /** - * Retrieves document scroll postion from top. - */ - static getDocumentScrollTop(): number; - /** - * Retrieves document scroll postion from left. - */ - static getDocumentScrollLeft(): number; - /** - * Fires the given function after a certain ammount of time. - * @param func Function to be fired. - * @param wait Wait time. - * @param options Additional options. - */ - static throttle(func: (Function: object) => void, wait: number, options?: Partial<{ - leading: boolean; - trailing: boolean; - }>): (...args: any[]) => any; - /** - * Renders confirm/close buttons with callback function - */ - static createConfirmationContainer(container: HTMLElement, confirmText: string, cancelText: string, onConfirm: (Function: object) => void, onCancel: (Function: object) => void): void; - /** - * Renders a button with optional callback function - */ - static createButton(container: HTMLElement, text: string, className?: string[], visibility?: boolean, callback?: (Function: object) => void): void; - static _setAbsolutePosition(origin: HTMLElement, container: HTMLElement, position: string, margin: number, transitionMovement: number, align?: string): { - x: number; - y: number; - }; - static _repositionWithinScreen(x: number, y: number, width: number, height: number, margin: number, transitionMovement: number, align: string): { - x: number; - y: number; - }; -} - -interface ParallaxOptions extends BaseOptions$1 { - /** - * The minimum width of the screen, in pixels, where the parallax functionality starts working. - * @default 0 - */ - responsiveThreshold: number; -} -declare class Parallax extends Component { - private _enabled; - private _img; - static _parallaxes: Parallax[]; - static _handleScrollThrottled: () => Utils; - static _handleWindowResizeThrottled: () => Utils; - constructor(el: HTMLElement, options: Partial); - static get defaults(): ParallaxOptions; - /** - * Initializes instance of Parallax. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Parallax; - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Parallax[]; - static getInstance(el: HTMLElement): Parallax; - destroy(): void; - static _handleScroll(): void; - static _handleWindowResize(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _setupStyles(): void; - _handleImageLoad: () => void; - private _offset; - _updateParallax(): void; -} - -interface PushpinOptions extends BaseOptions$1 { - /** - * The distance in pixels from the top of the page where - * the element becomes fixed. - * @default 0 - */ - top: number; - /** - * The distance in pixels from the top of the page where - * the elements stops being fixed. - * @default Infinity - */ - bottom: number; - /** - * The offset from the top the element will be fixed at. - * @default 0 - */ - offset: number; - /** - * Callback function called when pushpin position changes. - * You are provided with a position string. - * @default null - */ - onPositionChange: (position: 'pinned' | 'pin-top' | 'pin-bottom') => void; -} -declare class Pushpin extends Component { - static _pushpins: Pushpin[]; - originalOffset: number; - constructor(el: HTMLElement, options: Partial); - static get defaults(): PushpinOptions; - /** - * Initializes instance of Pushpin. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Pushpin; - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Pushpin[]; - static getInstance(el: HTMLElement): Pushpin; - destroy(): void; - static _updateElements(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _updatePosition(): void; - _removePinClasses(): void; -} - -interface ScrollSpyOptions extends BaseOptions$1 { - /** - * Throttle of scroll handler. - * @default 100 - */ - throttle: number; - /** - * Offset for centering element when scrolled to. - * @default 200 - */ - scrollOffset: number; - /** - * Class applied to active elements. - * @default 'active' - */ - activeClass: string; - /** - * Used to find active element. - * @default id => 'a[href="#' + id + '"]' - */ - getActiveElement: (id: string) => string; - /** - * Used to keep last top element active even if - * scrollbar goes outside of scrollspy elements. - * - * If there is no last top element, - * then the active one will be the first element. - * - * @default false - */ - keepTopElementActive: boolean; - /** - * Used to set scroll animation duration in milliseconds. - * @default null (browser's native animation implementation/duration) - */ - animationDuration: number | null; -} -declare class ScrollSpy extends Component { - static _elements: ScrollSpy[]; - static _count: number; - static _increment: number; - static _elementsInView: ScrollSpy[]; - static _visibleElements: HTMLElement[]; - static _ticks: number; - static _keptTopActiveElement: HTMLElement | null; - private tickId; - private id; - constructor(el: HTMLElement, options: Partial); - static get defaults(): ScrollSpyOptions; - /** - * Initializes instance of ScrollSpy. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): ScrollSpy; - /** - * Initializes instances of ScrollSpy. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): ScrollSpy[]; - static getInstance(el: HTMLElement): ScrollSpy; - destroy(): void; - _setupEventHandlers(): void; - _removeEventHandlers(): void; - _handleThrottledResize: () => void; - _handleTriggerClick: (e: MouseEvent) => void; - _handleWindowScroll: () => void; - static _offset(el: any): { - top: number; - left: number; - }; - static _findElements(top: number, right: number, bottom: number, left: number): ScrollSpy[]; - _enter(): void; - _exit(): void; - private _resetKeptTopActiveElementIfNeeded; - private static _getDistanceToViewport; - private static _smoothScrollIntoView; -} - -// type RGBColor = { -// r: number; -// g: number; -// b: number; -// }; - -// type Position = { -// x: number; -// y: number; -// }; - -class Waves { - /** - * - * @param {HTMLElement} el - * @returns - */ - static #offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - - // https://phoenix-dx.com/css-techniques-for-material-ripple-effect/ - - /** - * - * @param {HTMLElement} targetElement - * @param position - * @param color - */ - static renderWaveEffect(targetElement, position = null, color = null) { - const isCentered = position === null; - const duration = 500; - let animationFrame, animationStart; - - const animationStep = (timestamp) => { - if (!animationStart) animationStart = timestamp; - const frame = timestamp - animationStart; - if (frame < duration) { - const easing = (frame / duration) * (2 - frame / duration); - const circle = isCentered - ? 'circle at 50% 50%' - : `circle at ${position.x}px ${position.y}px`; - const waveColor = `rgba(${color?.r || 0}, ${color?.g || 0}, ${color?.b || 0}, ${0.3 * (1 - easing)})`; - const stop = 90 * easing + '%'; - targetElement.style.backgroundImage = - 'radial-gradient(' + - circle + - ', ' + - waveColor + - ' ' + - stop + - ', transparent ' + - stop + - ')'; - animationFrame = window.requestAnimationFrame(animationStep); - } else { - targetElement.style.backgroundImage = 'none'; - window.cancelAnimationFrame(animationFrame); - } - }; - - animationFrame = window.requestAnimationFrame(animationStep); - } - - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.body.addEventListener('click', (e) => { - const trigger = e.target; - const el = trigger.closest('.waves-effect'); - if (el && el.contains(trigger)) { - const isCircular = el.classList.contains('waves-circle'); - const x = e.pageX - this.#offset(el).left; - const y = e.pageY - this.#offset(el).top; - let color = null; - if (el.classList.contains('waves-light')) color = { r: 255, g: 255, b: 255 }; - this.renderWaveEffect(el, isCircular ? null : { x, y }, color); - } - }); - }); - } -} - -declare const version = "2.3.3"; -interface AutoInitOptions { - Autocomplete?: Partial; - Cards?: Partial; - Carousel?: Partial; - Chips?: Partial; - Collapsible?: Partial; - Datepicker?: Partial; - Dropdown?: Partial; - Materialbox?: Partial; - Modal?: Partial; - Parallax?: Partial; - Pushpin?: Partial; - ScrollSpy?: Partial; - FormSelect?: Partial; - Sidenav?: Partial; - Tabs?: Partial; - TapTarget?: Partial; - Timepicker?: Partial; - Tooltip?: Partial; - FloatingActionButton?: Partial; -} -/** - * Automatically initialize components. - * @param context Root element to initialize. Defaults to `document.body`. - * @param options Options for each component. - */ -declare function AutoInit(context?: HTMLElement, options?: Partial): void; - -export { AutoInit, Autocomplete, Cards, Carousel, CharacterCounter, Chips, Collapsible, Datepicker, Dropdown, FloatingActionButton, FormSelect, Forms, Materialbox, Modal, Parallax, Pushpin, Range, ScrollSpy, Sidenav, Slider, Tabs, TapTarget, Timepicker, Toast, Tooltip, Waves, version }; -export type { AutoInitOptions }; diff --git a/dist/js/materialize.js b/dist/js/materialize.js deleted file mode 100644 index 19d59fb6fd..0000000000 --- a/dist/js/materialize.js +++ /dev/null @@ -1,8409 +0,0 @@ -/*! -* Materialize v2.3.3 (https://materializeweb.com) -* Copyright 2014-2026 Materialize -* MIT License (https://raw.githubusercontent.com/materializecss/materialize/master/LICENSE) -*/ -var M = (function (exports) { - 'use strict'; - - /** - * Class with utilitary functions for global usage. - */ - class Utils { - /** Specifies wether tab is pressed or not. */ - static tabPressed = false; - /** Specifies wether there is a key pressed. */ - static keyDown = false; - /** - * Key maps. - */ - static keys = { - TAB: ['Tab'], - ENTER: ['Enter'], - ESC: ['Escape', 'Esc'], - BACKSPACE: ['Backspace'], - ARROW_UP: ['ArrowUp', 'Up'], - ARROW_DOWN: ['ArrowDown', 'Down'], - ARROW_LEFT: ['ArrowLeft', 'Left'], - ARROW_RIGHT: ['ArrowRight', 'Right'], - DELETE: ['Delete', 'Del'] - }; - /** - * Detects when a key is pressed. - * @param e Event instance. - */ - static docHandleKeydown(e) { - Utils.keyDown = true; - if ([...Utils.keys.TAB, ...Utils.keys.ARROW_DOWN, ...Utils.keys.ARROW_UP].includes(e.key)) { - Utils.tabPressed = true; - } - } - /** - * Detects when a key is released. - * @param e Event instance. - */ - static docHandleKeyup(e) { - Utils.keyDown = false; - if ([...Utils.keys.TAB, ...Utils.keys.ARROW_DOWN, ...Utils.keys.ARROW_UP].includes(e.key)) { - Utils.tabPressed = false; - } - } - /** - * Detects when document is focused. - * @param e Event instance. - */ - /* eslint-disabled as of required event type condition check */ - /* eslint-disable-next-line */ - static docHandleFocus(e) { - if (Utils.keyDown) { - document.body.classList.add('keyboard-focused'); - } - } - /** - * Detects when document is not focused. - * @param e Event instance. - */ - /* eslint-disabled as of required event type condition check */ - /* eslint-disable-next-line */ - static docHandleBlur(e) { - document.body.classList.remove('keyboard-focused'); - } - /** - * Generates a unique string identifier. - */ - static guid() { - const s4 = () => { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - }; - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); - } - /** - * Checks for exceeded edges - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkWithinContainer(container, bounding, offset) { - const edges = { - top: false, - right: false, - bottom: false, - left: false - }; - const containerRect = container.getBoundingClientRect(); - // If body element is smaller than viewport, use viewport height instead. - const containerBottom = container === document.body - ? Math.max(containerRect.bottom, window.innerHeight) - : containerRect.bottom; - const scrollLeft = container.scrollLeft; - const scrollTop = container.scrollTop; - const scrolledX = bounding.left - scrollLeft; - const scrolledY = bounding.top - scrollTop; - // Check for container and viewport for each edge - if (scrolledX < containerRect.left + offset || scrolledX < offset) { - edges.left = true; - } - if (scrolledX + bounding.width > containerRect.right - offset || - scrolledX + bounding.width > window.innerWidth - offset) { - edges.right = true; - } - if (scrolledY < containerRect.top + offset || scrolledY < offset) { - edges.top = true; - } - if (scrolledY + bounding.height > containerBottom - offset || - scrolledY + bounding.height > window.innerHeight - offset) { - edges.bottom = true; - } - return edges; - } - /** - * Checks if element can be aligned in multiple directions. - * @param el Element to be inspected. - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkPossibleAlignments(el, container, bounding, offset) { - const canAlign = { - top: true, - right: true, - bottom: true, - left: true, - spaceOnTop: null, - spaceOnRight: null, - spaceOnBottom: null, - spaceOnLeft: null - }; - const containerAllowsOverflow = getComputedStyle(container).overflow === 'visible'; - const containerRect = container.getBoundingClientRect(); - const containerHeight = Math.min(containerRect.height, window.innerHeight); - const containerWidth = Math.min(containerRect.width, window.innerWidth); - const elOffsetRect = el.getBoundingClientRect(); - const scrollLeft = container.scrollLeft; - const scrollTop = container.scrollTop; - const scrolledX = bounding.left - scrollLeft; - const scrolledYTopEdge = bounding.top - scrollTop; - const scrolledYBottomEdge = bounding.top + elOffsetRect.height - scrollTop; - // Check for container and viewport for left - canAlign.spaceOnRight = !containerAllowsOverflow - ? containerWidth - (scrolledX + bounding.width) - : window.innerWidth - (elOffsetRect.left + bounding.width); - if (canAlign.spaceOnRight < 0) { - canAlign.left = false; - } - // Check for container and viewport for Right - canAlign.spaceOnLeft = !containerAllowsOverflow - ? scrolledX - bounding.width + elOffsetRect.width - : elOffsetRect.right - bounding.width; - if (canAlign.spaceOnLeft < 0) { - canAlign.right = false; - } - // Check for container and viewport for Top - canAlign.spaceOnBottom = !containerAllowsOverflow - ? containerHeight - (scrolledYTopEdge + bounding.height + offset) - : window.innerHeight - (elOffsetRect.top + bounding.height + offset); - if (canAlign.spaceOnBottom < 0) { - canAlign.top = false; - } - // Check for container and viewport for Bottom - canAlign.spaceOnTop = !containerAllowsOverflow - ? scrolledYBottomEdge - (bounding.height - offset) - : elOffsetRect.bottom - (bounding.height + offset); - if (canAlign.spaceOnTop < 0) { - canAlign.bottom = false; - } - return canAlign; - } - /** - * Retrieves target element id from trigger. - * @param trigger Trigger element. - */ - static getIdFromTrigger(trigger) { - let id = trigger.dataset.target; - if (!id) { - id = trigger.getAttribute('href'); - return id ? id.slice(1) : ''; - } - return id; - } - /** - * Retrieves document scroll postion from top. - */ - static getDocumentScrollTop() { - return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; - } - /** - * Retrieves document scroll postion from left. - */ - static getDocumentScrollLeft() { - return window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft || 0; - } - /** - * Fires the given function after a certain ammount of time. - * @param func Function to be fired. - * @param wait Wait time. - * @param options Additional options. - */ - static throttle(func, wait, options = {}) { - let context, args, result, timeout = null, previous = 0; - const later = () => { - previous = options.leading === false ? 0 : new Date().getTime(); - timeout = null; - result = func.apply(context, args); - context = args = null; - }; - return (...args) => { - const now = new Date().getTime(); - if (!previous && options.leading === false) - previous = now; - const remaining = wait - (now - previous); - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(this, args); - } - else if (!timeout && options.trailing !== false) { - timeout = setTimeout(later, remaining); - } - return result; - }; - } - /** - * Renders confirm/close buttons with callback function - */ - static createConfirmationContainer(container, confirmText, cancelText, onConfirm, onCancel) { - const confirmationButtonsContainer = document.createElement('div'); - confirmationButtonsContainer.classList.add('confirmation-btns'); - container.append(confirmationButtonsContainer); - this.createButton(confirmationButtonsContainer, cancelText, ['btn-cancel'], true, onCancel); - this.createButton(confirmationButtonsContainer, confirmText, ['btn-confirm'], true, onConfirm); - } - /** - * Renders a button with optional callback function - */ - static createButton(container, text, className = [], visibility = true, callback = null) { - className = className.concat(['btn', 'waves-effect', 'text']); - const button = document.createElement('button'); - button.className = className.join(' '); - button.style.visibility = visibility ? 'visible' : 'hidden'; - button.type = 'button'; - button.tabIndex = !!visibility ? 0 : -1; - button.innerText = text; - button.addEventListener('click', callback); - button.addEventListener('keypress', (e) => { - if (Utils.keys.ENTER.includes(e.key)) - callback(e); - }); - container.append(button); - } - static _setAbsolutePosition(origin, container, position, margin, transitionMovement, align = 'center') { - const originHeight = origin.offsetHeight, originWidth = origin.offsetWidth, containerHeight = container.offsetHeight, containerWidth = container.offsetWidth; - let xMovement = 0, yMovement = 0, targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(), targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (position === 'top') { - targetTop += -containerHeight - margin; - if (align === 'center') { - targetLeft += originWidth / 2 - containerWidth / 2; // This is center align - } - yMovement = -transitionMovement; - } - else if (position === 'right') { - targetTop += originHeight / 2 - containerHeight / 2; - targetLeft = originWidth + margin; - xMovement = transitionMovement; - } - else if (position === 'left') { - targetTop += originHeight / 2 - containerHeight / 2; - targetLeft = -containerWidth - margin; - xMovement = -transitionMovement; - } - else { - targetTop += originHeight + margin; - if (align === 'center') { - targetLeft += originWidth / 2 - containerWidth / 2; // This is center align - } - yMovement = transitionMovement; - } - if (align === 'right') { - targetLeft += originWidth - containerWidth - margin; - } - const newCoordinates = Utils._repositionWithinScreen(targetLeft, targetTop, containerWidth, containerHeight, margin, transitionMovement, align); - container.style.top = newCoordinates.y + 'px'; - container.style.left = newCoordinates.x + 'px'; - return { x: xMovement, y: yMovement }; - } - static _repositionWithinScreen(x, y, width, height, margin, transitionMovement, align) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - const bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - let offset; - if (align === 'left' || align == 'center') { - offset = margin + transitionMovement; - } - else if (align === 'right') { - offset = margin - transitionMovement; - } - const edges = Utils.checkWithinContainer(document.body, bounding, offset); - if (edges.left) { - newX = offset; - } - else if (edges.right) { - newX -= newX + width - window.innerWidth; - } - if (edges.top) { - newY = offset; - } - else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; - } - } - - /** - * Base class implementation for Materialize components. - */ - class Component { - /** - * The DOM element the plugin was initialized with. - */ - el; - /** - * The options the instance was initialized with. - */ - options; - /** - * Constructs component instance and set everything up. - */ - constructor(el, options, classDef) { - // Display error if el is not a valid HTML Element - if (!(el instanceof HTMLElement)) { - console.error(Error(el + ' is not an HTML Element')); - } - // If exists, destroy and reinitialize in child - const ins = classDef.getInstance(el); - if (!!ins) { - ins.destroy(); - } - this.el = el; - } - /** - * Initializes component instances. - * @param els HTML elements. - * @param options Component options. - * @param classDef Class definition. - */ - static init(els, options, classDef) { - let instances = null; - if (els instanceof Element) { - instances = new classDef(els, options); - } - else if (!!els && els.length) { - instances = []; - for (let i = 0; i < els.length; i++) { - instances.push(new classDef(els[i], options)); - } - } - return instances; - } - /** - * @returns default options for component instance. - */ - static get defaults() { - return {}; - } - /** - * Retrieves component instance for the given element. - * @param el Associated HTML Element. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - static getInstance(el) { - throw new Error('This method must be implemented.'); - } - /** - * Destroy plugin instance and teardown. - */ - destroy() { - throw new Error('This method must be implemented.'); - } - } - - const _defaults$o = { - alignment: 'left', - autoFocus: true, - constrainWidth: true, - container: null, - coverTrigger: true, - closeOnClick: true, - hover: false, - inDuration: 150, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - onItemClick: null - }; - class Dropdown extends Component { - static _dropdowns = []; - /** ID of the dropdown element. */ - id; - /** The DOM element of the dropdown. */ - dropdownEl; - /** If the dropdown is open. */ - isOpen; - /** If the dropdown content is scrollable. */ - isScrollable; - isTouchMoving; - /** The index of the item focused. */ - focusedIndex; - filterQuery; - filterTimeout; - constructor(el, options) { - super(el, options, Dropdown); - this.el['M_Dropdown'] = this; - Dropdown._dropdowns.push(this); - this.id = Utils.getIdFromTrigger(el); - this.dropdownEl = document.getElementById(this.id); - this.options = { - ...Dropdown.defaults, - ...options - }; - this.isOpen = false; - this.isScrollable = false; - this.isTouchMoving = false; - this.focusedIndex = -1; - this.filterQuery = []; - this.el.ariaExpanded = 'false'; - // Move dropdown-content after dropdown-trigger - this._moveDropdownToElement(); - this._makeDropdownFocusable(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$o; - } - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Dropdown); - } - static getInstance(el) { - return el['M_Dropdown']; - } - destroy() { - this._resetDropdownStyles(); - this._removeEventHandlers(); - Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); - this.el['M_Dropdown'] = undefined; - } - _setupEventHandlers() { - // Trigger keydown handler - this.el.addEventListener('keydown', this._handleTriggerKeydown); - // Item click handler - this.dropdownEl?.addEventListener('click', this._handleDropdownClick); - // Hover event handlers - if (this.options.hover) { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave); - // Click event handlers - } - else { - this.el.addEventListener('click', this._handleClick); - } - } - _removeEventHandlers() { - this.el.removeEventListener('keydown', this._handleTriggerKeydown); - this.dropdownEl.removeEventListener('click', this._handleDropdownClick); - if (this.options.hover) { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave); - } - else { - this.el.removeEventListener('click', this._handleClick); - } - } - _setupTemporaryEventHandlers() { - document.body.addEventListener('click', this._handleDocumentClick); - document.body.addEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); - window.addEventListener('resize', this._handleWindowResize); - } - _removeTemporaryEventHandlers() { - document.body.removeEventListener('click', this._handleDocumentClick); - document.body.removeEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); - window.removeEventListener('resize', this._handleWindowResize); - } - _handleClick = (e) => { - e.preventDefault(); - //this._moveDropdown((e.target).closest('li')); - if (this.isOpen) { - this.close(); - } - else { - this.open(); - } - }; - _handleMouseEnter = () => { - //this._moveDropdown((e.target).closest('li')); - this.open(); - }; - _handleMouseLeave = (e) => { - const toEl = e.relatedTarget; - const leaveToDropdownContent = !!toEl.closest('.dropdown-content'); - let leaveToActiveDropdownTrigger = false; - const closestTrigger = toEl.closest('.dropdown-trigger'); - if (closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen) { - leaveToActiveDropdownTrigger = true; - } - // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content - if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { - this.close(); - } - }; - _handleDocumentClick = (e) => { - const target = e.target; - if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) { - // isTouchMoving to check if scrolling on mobile. - this.close(); - } - else if (!target.closest('.dropdown-content')) { - // Do this one frame later so that if the element clicked also triggers _handleClick - // For example, if a label for a select was clicked, that we don't close/open the dropdown - setTimeout(() => { - if (this.isOpen) { - this.close(); - } - }, 0); - } - this.isTouchMoving = false; - }; - _handleTriggerKeydown = (e) => { - // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown - const arrowDownOrEnter = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key); - if (arrowDownOrEnter && !this.isOpen) { - e.preventDefault(); - this.open(); - } - }; - _handleDocumentTouchmove = (e) => { - const target = e.target; - if (target.closest('.dropdown-content')) { - this.isTouchMoving = true; - } - }; - _handleDropdownClick = (e) => { - // onItemClick callback - if (typeof this.options.onItemClick === 'function') { - const itemEl = e.target.closest('li'); - this.options.onItemClick.call(this, itemEl); - } - }; - _handleDropdownKeydown = (e) => { - const arrowUpOrDown = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key); - if (Utils.keys.TAB.includes(e.key)) { - e.preventDefault(); - this.close(); - } - // Navigate down dropdown list - else if (arrowUpOrDown && this.isOpen) { - e.preventDefault(); - const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1; - let newFocusedIndex = this.focusedIndex; - let hasFoundNewIndex = false; - do { - newFocusedIndex = newFocusedIndex + direction; - if (!!this.dropdownEl.children[newFocusedIndex] && - this.dropdownEl.children[newFocusedIndex].tabIndex !== -1) { - hasFoundNewIndex = true; - break; - } - } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); - if (hasFoundNewIndex) { - // Remove active class from old element - if (this.focusedIndex >= 0) - this.dropdownEl.children[this.focusedIndex].classList.remove('active'); - this.focusedIndex = newFocusedIndex; - this._focusFocusedItem(); - } - } - // ENTER selects choice on focused item - else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) { - // Search for and ` + - ''); - } - renderRow(days, isRTL, isRowSelected) { - return ('' + - (isRTL ? days.reverse() : days).join('') + - ''); - } - renderTable(opts, data, randId) { - return ('
' + - this.renderHead(opts) + - this.renderBody(data) + - '
'); - } - renderHead(opts) { - const arr = []; - let i; - for (i = 0; i < 7; i++) { - arr.push(`${this.renderDayName(opts, i, true)}`); - } - return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; - } - renderBody(rows) { - return '' + rows.join('') + ''; - } - renderTitle(instance, c, year, month, refYear, randId) { - const opts = this.options, isMinYear = year === opts.minYear, isMaxYear = year === opts.maxYear; - let i, j, arr = [], html = '
', prev = true, next = true; - for (i = 0; i < 12; i++) { - arr.push(''); - } - const monthHtml = ''; - if (Array.isArray(opts.yearRange)) { - i = opts.yearRange[0]; - j = opts.yearRange[1] + 1; - } - else { - i = year - opts.yearRange; - j = 1 + year + opts.yearRange; - } - for (arr = []; i < j && i <= opts.maxYear; i++) { - if (i >= opts.minYear) { - arr.push(``); - } - } - if (opts.yearRangeReverse) - arr.reverse(); - const yearHtml = ``; - const leftArrow = ''; - html += ``; - html += '
'; - if (opts.showMonthAfterYear) { - html += yearHtml + monthHtml; - } - else { - html += monthHtml + yearHtml; - } - html += '
'; - if (isMinYear && (month === 0 || opts.minMonth >= month)) { - prev = false; - } - if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { - next = false; - } - const rightArrow = ''; - html += ``; - return (html += '
'); - } - // refresh HTML - draw() { - const opts = this.options, minYear = opts.minYear, maxYear = opts.maxYear, minMonth = opts.minMonth, maxMonth = opts.maxMonth; - let html = ''; - if (this._y <= minYear) { - this._y = minYear; - if (!isNaN(minMonth) && this._m < minMonth) { - this._m = minMonth; - } - } - if (this._y >= maxYear) { - this._y = maxYear; - if (!isNaN(maxMonth) && this._m > maxMonth) { - this._m = maxMonth; - } - } - const randId = 'datepicker-title-' + - Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .substr(0, 2); - for (let c = 0; c < 1; c++) { - if (!this.options.isDateRange) { - this._renderDateDisplay(this.date); - } - else { - this._renderDateDisplay(this.date, this.endDate); - } - html += - this.renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId); - } - this.destroySelects(); - this.calendarEl.innerHTML = html; - // Init Materialize Select - const yearSelect = this.calendarEl.querySelector('.orig-select-year'); - const monthSelect = this.calendarEl.querySelector('.orig-select-month'); - // @todo fix accessibility @see https://github.com/materializecss/materialize/issues/522 - FormSelect.init(yearSelect, { - classes: 'select-year', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - FormSelect.init(monthSelect, { - classes: 'select-month', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - // Add change handlers for select - yearSelect.addEventListener('change', this._handleYearChange); - monthSelect.addEventListener('change', this._handleMonthChange); - if (typeof this.options.onDraw === 'function') { - this.options.onDraw.call(this); - } - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - this.el.addEventListener('keydown', this._handleInputKeydown); - this.el.addEventListener('change', this._handleInputChange); - this.calendarEl.addEventListener('click', this._handleCalendarClick); - } - _setupVariables() { - const template = document.createElement('template'); - template.innerHTML = Datepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.calendarEl = this.containerEl.querySelector('.datepicker-calendar'); - this.yearTextEl = this.containerEl.querySelector('.year-text'); - this.dateTextEl = this.containerEl.querySelector('.date-text'); - this.footer = this.containerEl.querySelector('.datepicker-footer'); - this.formats = { - d: (date) => { - return date.getDate(); - }, - dd: (date) => { - const d = date.getDate(); - return (d < 10 ? '0' : '') + d; - }, - ddd: (date) => { - return this.options.i18n.weekdaysShort[date.getDay()]; - }, - dddd: (date) => { - return this.options.i18n.weekdays[date.getDay()]; - }, - m: (date) => { - return date.getMonth() + 1; - }, - mm: (date) => { - const m = date.getMonth() + 1; - return (m < 10 ? '0' : '') + m; - }, - mmm: (date) => { - return this.options.i18n.monthsShort[date.getMonth()]; - }, - mmmm: (date) => { - return this.options.i18n.months[date.getMonth()]; - }, - yy: (date) => { - return ('' + date.getFullYear()).slice(2); - }, - yyyy: (date) => { - return date.getFullYear(); - } - }; - } - _pickerSetup() { - if (!this.options.defaultDate) { - this.options.defaultDate = new Date(Date.parse(this.el.value)); - } - const defDate = this.options.defaultDate; - if (Datepicker._isDate(defDate)) { - if (this.options.setDefaultDate) { - this.setDate(defDate, true); - this.setInputValue(this.el, defDate); - } - else { - this.gotoDate(defDate); - } - } - else { - this.gotoDate(new Date()); - } - if (this.options.isDateRange) { - this.multiple = true; - const defEndDate = this.options.defaultEndDate; - if (Datepicker._isDate(defEndDate)) { - if (this.options.setDefaultEndDate) { - this.setDate(defEndDate, true, true); - this.setInputValue(this.endDateEl, defEndDate); - } - } - } - if (this.options.isMultipleSelection) { - this.multiple = true; - this.dates = []; - this.dateEls = []; - this.dateEls.push(this.el); - } - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - this.el.removeEventListener('change', this._handleInputChange); - this.calendarEl.removeEventListener('click', this._handleCalendarClick); - if (this.options.isDateRange) { - this.endDateEl.removeEventListener('click', this._handleInputClick); - this.endDateEl.removeEventListener('keypress', this._handleInputKeydown); - this.endDateEl.removeEventListener('change', this._handleInputChange); - } - } - _handleInputClick = (e) => { - // Prevents default browser datepicker modal rendering - if (e.type == 'date') { - e.preventDefault(); - } - this.setDateFromInput(e.target); - this.draw(); - this.gotoDate(e.target === this.el ? this.date : this.endDate); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.setDateFromInput(e.target); - this.draw(); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - } - }; - _handleCalendarClick = (e) => { - const target = e.target; - if (!target.classList.contains('is-disabled')) { - if (target.classList.contains('datepicker-day-button') && - !target.classList.contains('is-empty') && - !target.parentElement.classList.contains('is-disabled')) { - const selectedDate = new Date(e.target.getAttribute('data-year'), e.target.getAttribute('data-month'), e.target.getAttribute('data-day')); - if (!this.multiple || (this.multiple && this.options.isMultipleSelection)) { - this.setDate(selectedDate); - } - if (this.options.isDateRange) { - const confirmAfterSelection = Datepicker._isDate(this.date) && this.options.autoSubmit; - this._handleDateRangeCalendarClick(selectedDate); - if (confirmAfterSelection) { - this._confirm(); - } - } - if (!this.options.isDateRange && this.options.autoSubmit) - this._confirm(); - } - else if (target.closest('.month-prev')) { - this.prevMonth(); - } - else if (target.closest('.month-next')) { - this.nextMonth(); - } - } - }; - _handleDateRangeCalendarClick = (date) => { - if (this.endDate == null || !Datepicker._compareDates(date, this.endDate)) { - if (Datepicker._isDate(this.date) && Datepicker._comparePastDate(date, this.date)) { - return; - } - this.setDate(date, true, Datepicker._isDate(this.date)); - return; - } - this._clearDates(); - this.draw(); - }; - _handleClearClick = () => { - this._clearDates(); - this.setInputValues(); - }; - _clearDates = () => { - this.date = null; - this.endDate = null; - this.draw(); - }; - _handleMonthChange = (e) => { - this.gotoMonth(e.target.value); - }; - _handleYearChange = (e) => { - this.gotoYear(e.target.value); - }; - // change view to a specific month (zero-index, e.g. 0: January) - gotoMonth(month) { - if (!isNaN(month)) { - this.calendars[0].month = parseInt(month, 10); - this.adjustCalendars(); - } - } - // change view to a specific full year (e.g. "2012") - gotoYear(year) { - if (!isNaN(year)) { - this.calendars[0].year = parseInt(year, 10); - this.adjustCalendars(); - } - } - _handleInputChange = (e) => { - let date; - const el = e.target; - // Prevent change event from being fired when triggered by the plugin - if (e['detail']?.firedBy === this) - return; - // Prevent change event from being fired if an end date is set without a start date - if (el == this.endDateEl && !this.date) - return; - if (this.options.parse) { - date = this.options.parse(e.target.value, typeof this.options.format === 'function' - ? this.options.format(new Date(this.el.value)) - : this.options.format); - } - else { - date = new Date(Date.parse(e.target.value)); - } - if (Datepicker._isDate(date)) { - this.setDate(date, false, el == this.endDateEl, true); - if (e.type == 'date') { - this.setDataDate(e, date); - this.setInputValues(); - } - } - }; - renderDayName(opts, day, abbr = false) { - day += opts.firstDay; - while (day >= 7) { - day -= 7; - } - return abbr ? opts.i18n.weekdaysAbbrev[day] : opts.i18n.weekdays[day]; - } - createDateInput() { - const dateInput = this.el.cloneNode(true); - dateInput.addEventListener('click', this._handleInputClick); - dateInput.addEventListener('keypress', this._handleInputKeydown); - dateInput.addEventListener('change', this._handleInputChange); - this.el.parentElement.appendChild(dateInput); - return dateInput; - } - // Set input value to the selected date and close Datepicker - _finishSelection = () => { - this.setInputValues(); - // Commented out because of function deprecations - // this.close(); - }; - _confirm = () => { - this._finishSelection(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onConfirm === 'function') - this.options.onConfirm.call(this); - }; - _cancel = () => { - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - // deprecated - open() { - console.warn('Datepicker.open() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Datepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Datepicker._template = ` -
-
- - -
-
-
- -
-
`; - } - } - - class Forms { - /** - * Checks if the label has validation and apply - * the correct class and styles - * @param textfield - */ - static validateField(textfield) { - if (!textfield) { - console.error('No text field element found'); - return; - } - const hasLength = textfield.getAttribute('data-length') !== null; - const lenAttr = parseInt(textfield.getAttribute('data-length')); - const len = textfield.value.length; - if (len === 0 && - textfield.validity.badInput === false && - !textfield.required && - textfield.classList.contains('validate')) { - textfield.classList.remove('invalid'); - } - else if (textfield.classList.contains('validate')) { - // Check for character counter attributes - if ((textfield.validity.valid && hasLength && len <= lenAttr) || - (textfield.validity.valid && !hasLength)) { - textfield.classList.remove('invalid'); - } - else { - textfield.classList.add('invalid'); - } - } - } - /** - * Resizes the given TextArea after updating the - * value content dynamically. - * @param e EventTarget - */ - static textareaAutoResize(e) { - const textarea = e; - // if (!textarea) { - // console.error('No textarea element found'); - // return; - // } - // Textarea Auto Resize - let hiddenDiv = document.querySelector('.hiddendiv'); - if (!hiddenDiv) { - hiddenDiv = document.createElement('div'); - hiddenDiv.classList.add('hiddendiv', 'common'); - document.body.append(hiddenDiv); - } - const style = getComputedStyle(textarea); - // Set font properties of hiddenDiv - const fontFamily = style.fontFamily; //textarea.css('font-family'); - const fontSize = style.fontSize; //textarea.css('font-size'); - const lineHeight = style.lineHeight; //textarea.css('line-height'); - // Firefox can't handle padding shorthand. - const paddingTop = style.paddingTop; //getComputedStyle(textarea).css('padding-top'); - const paddingRight = style.paddingRight; //textarea.css('padding-right'); - const paddingBottom = style.paddingBottom; //textarea.css('padding-bottom'); - const paddingLeft = style.paddingLeft; //textarea.css('padding-left'); - if (fontSize) - hiddenDiv.style.fontSize = fontSize; //('font-size', fontSize); - if (fontFamily) - hiddenDiv.style.fontFamily = fontFamily; //css('font-family', fontFamily); - if (lineHeight) - hiddenDiv.style.lineHeight = lineHeight; //css('line-height', lineHeight); - if (paddingTop) - hiddenDiv.style.paddingTop = paddingTop; //ss('padding-top', paddingTop); - if (paddingRight) - hiddenDiv.style.paddingRight = paddingRight; //css('padding-right', paddingRight); - if (paddingBottom) - hiddenDiv.style.paddingBottom = paddingBottom; //css('padding-bottom', paddingBottom); - if (paddingLeft) - hiddenDiv.style.paddingLeft = paddingLeft; //css('padding-left', paddingLeft); - // Set original-height, if none - if (!textarea.hasAttribute('original-height')) - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - if (textarea.getAttribute('wrap') === 'off') { - hiddenDiv.style.overflowWrap = 'normal'; // ('overflow-wrap', 'normal') - hiddenDiv.style.whiteSpace = 'pre'; //.css('white-space', 'pre'); - } - hiddenDiv.innerText = textarea.value + '\n'; - hiddenDiv.innerHTML = hiddenDiv.innerHTML.replace(/\n/g, '
'); - // When textarea is hidden, width goes crazy. - // Approximate with half of window size - if (textarea.offsetWidth > 0 && textarea.offsetHeight > 0) { - hiddenDiv.style.width = textarea.getBoundingClientRect().width + 'px'; // ('width', textarea.width() + 'px'); - } - else { - hiddenDiv.style.width = window.innerWidth / 2 + 'px'; //css('width', window.innerWidth / 2 + 'px'); - } - // Resize if the new height is greater than the - // original height of the textarea - const originalHeight = parseInt(textarea.getAttribute('original-height')); - const prevLength = parseInt(textarea.getAttribute('previous-length')); - if (isNaN(originalHeight)) - return; - if (originalHeight <= hiddenDiv.clientHeight) { - textarea.style.height = hiddenDiv.clientHeight + 'px'; //css('height', hiddenDiv.innerHeight() + 'px'); - } - else if (textarea.value.length < prevLength) { - // In case the new height is less than original height, it - // means the textarea has less text than before - // So we set the height to the original one - textarea.style.height = originalHeight + 'px'; - } - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - } - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.addEventListener('change', (e) => { - const target = e.target; - if (target instanceof HTMLInputElement) { - if (target.value.length !== 0 || target.getAttribute('placeholder') !== null) { - for (const child of target.parentNode.children) { - if (child.tagName == 'label') { - child.classList.add('active'); - } - } - } - Forms.validateField(target); - } - }); - document.addEventListener('keyup', (e) => { - const target = e.target; - // Radio and Checkbox focus class - if (target instanceof HTMLInputElement && ['radio', 'checkbox'].includes(target.type)) { - // TAB, check if tabbing to radio or checkbox. - if (Utils.keys.TAB.includes(e.key)) { - target.classList.add('tabbed'); - target.addEventListener('blur', () => target.classList.remove('tabbed'), { - once: true - }); - } - } - }); - document - .querySelectorAll('.materialize-textarea') - .forEach((textArea) => { - Forms.InitTextarea(textArea); - }); - // File Input Path - document - .querySelectorAll('.file-field input[type="file"]') - .forEach((fileInput) => { - Forms.InitFileInputPath(fileInput); - }); - }); - } - static InitTextarea(textarea) { - // Save Data in Element - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - Forms.textareaAutoResize(textarea); - textarea.addEventListener('keyup', (e) => Forms.textareaAutoResize(e.target)); - textarea.addEventListener('keydown', (e) => Forms.textareaAutoResize(e.target)); - } - static InitFileInputPath(fileInput) { - fileInput.addEventListener('change', () => { - const fileField = fileInput.closest('.file-field'); - const pathInput = fileField.querySelector('input.file-path'); - const files = fileInput.files; - const filenames = []; - for (let i = 0; i < files.length; i++) { - filenames.push(files[i].name); - } - pathInput.value = filenames.join(', '); - pathInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - }); - } - } - - const _defaults$d = { - inDuration: 275, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null - }; - class Materialbox extends Component { - /** If the materialbox overlay is showing. */ - overlayActive; - /** If the materialbox is no longer being animated. */ - doneAnimating; - /** Caption, if specified. */ - caption; - /** Original width of image. */ - originalWidth; - /** Original height of image. */ - originalHeight; - originInlineStyles; - placeholder; - _changedAncestorList; - newHeight; - newWidth; - windowWidth; - windowHeight; - attrWidth; - attrHeight; - _overlay; - _photoCaption; - constructor(el, options) { - super(el, options, Materialbox); - this.el['M_Materialbox'] = this; - this.options = { - ...Materialbox.defaults, - ...options - }; - this.overlayActive = false; - this.doneAnimating = true; - this.placeholder = document.createElement('div'); - this.placeholder.classList.add('material-placeholder'); - this.originalWidth = 0; - this.originalHeight = 0; - this.originInlineStyles = this.el.getAttribute('style'); - this.caption = this.el.getAttribute('data-caption') || ''; - this.el.tabIndex = 0; - // Wrap - this.el.before(this.placeholder); - this.placeholder.append(this.el); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$d; - } - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Materialbox); - } - static getInstance(el) { - return el['M_Materialbox']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_Materialbox'] = undefined; - // Unwrap image - //this.placeholder.after(this.el).remove(); - this.placeholder.remove(); - this.el.removeAttribute('style'); - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleMaterialboxClick); - this.el.addEventListener('keypress', this._handleMaterialboxKeypress); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleMaterialboxClick); - this.el.removeEventListener('keypress', this._handleMaterialboxKeypress); - } - _handleMaterialboxClick = () => { - this._handleMaterialboxToggle(); - }; - _handleMaterialboxKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleMaterialboxToggle(); - } - }; - _handleMaterialboxToggle = () => { - // If already modal, return to original - if (this.doneAnimating === false || (this.overlayActive && this.doneAnimating)) - this.close(); - else - this.open(); - }; - _handleWindowScroll = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowResize = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowEscape = (e) => { - if (Utils.keys.ESC.includes(e.key) && this.doneAnimating && this.overlayActive) - this.close(); - }; - _makeAncestorsOverflowVisible() { - this._changedAncestorList = []; - let ancestor = this.placeholder.parentNode; - while (ancestor !== null && ancestor !== document) { - const curr = ancestor; - if (curr.style.overflow !== 'visible') { - curr.style.overflow = 'visible'; - this._changedAncestorList.push(curr); - } - ancestor = ancestor.parentNode; - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateVars() { - this.windowWidth = window.innerWidth; - this.windowHeight = window.innerHeight; - this.caption = this.el.getAttribute('data-caption') || ''; - } - // Image - _animateImageIn() { - this.el.style.maxHeight = this.newHeight.toString() + 'px'; - this.el.style.maxWidth = this.newWidth.toString() + 'px'; - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.height = this.originalHeight + 'px'; - this.el.style.width = this.originalWidth + 'px'; - setTimeout(() => { - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.newHeight + 'px'; - this.el.style.width = this.newWidth + 'px'; - this.el.style.left = - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2 + - 'px'; - this.el.style.top = - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2 + - 'px'; - }, 1); - setTimeout(() => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - /* - anim({ - targets: this.el, // image - height: [this.originalHeight, this.newHeight], - width: [this.originalWidth, this.newWidth], - left: - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2, - top: - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2, - - duration: this.options.inDuration, - easing: 'easeOutQuad', - complete: () => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') this.options.onOpenEnd.call(this, this.el); - } - }); - */ - } - _animateImageOut() { - const duration = this.options.outDuration; - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.originalWidth + 'px'; - this.el.style.width = this.originalWidth + 'px'; - this.el.style.left = '0'; - this.el.style.top = '0'; - setTimeout(() => { - this.placeholder.style.height = ''; - this.placeholder.style.width = ''; - this.placeholder.style.position = ''; - this.placeholder.style.top = ''; - this.placeholder.style.left = ''; - // Revert to width or height attribute - if (this.attrWidth) - this.el.setAttribute('width', this.attrWidth.toString()); - if (this.attrHeight) - this.el.setAttribute('height', this.attrHeight.toString()); - this.el.removeAttribute('style'); - if (this.originInlineStyles) - this.el.setAttribute('style', this.originInlineStyles); - // Remove class - this.el.classList.remove('active'); - this.doneAnimating = true; - // Remove overflow overrides on ancestors - this._changedAncestorList.forEach((anchestor) => (anchestor.style.overflow = '')); - // onCloseEnd callback - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - // Caption - _addCaption() { - this._photoCaption = document.createElement('div'); - this._photoCaption.classList.add('materialbox-caption'); - this._photoCaption.innerText = this.caption; - document.body.append(this._photoCaption); - this._photoCaption.style.display = 'inline'; - // Animate - this._photoCaption.style.transition = 'none'; - this._photoCaption.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '1'; - }, 1); - } - _removeCaption() { - const duration = this.options.outDuration; - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '0'; - setTimeout(() => { - this._photoCaption.remove(); - }, duration); - } - // Overlay - _addOverlay() { - this._overlay = document.createElement('div'); - this._overlay.id = 'materialbox-overlay'; - this._overlay.addEventListener('click', () => { - if (this.doneAnimating) - this.close(); - }, { once: true }); - // Put before in origin image to preserve z-index layering. - this.el.before(this._overlay); - // Set dimensions if needed - const overlayOffset = this._overlay.getBoundingClientRect(); - this._overlay.style.width = this.windowWidth + 'px'; - this._overlay.style.height = this.windowHeight + 'px'; - this._overlay.style.left = -1 * overlayOffset.left + 'px'; - this._overlay.style.top = -1 * overlayOffset.top + 'px'; - // Animate - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '1'; - }, 1); - } - _removeOverlay() { - const duration = this.options.outDuration; - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '0'; - setTimeout(() => { - this.overlayActive = false; - this._overlay.remove(); - }, duration); - } - /** - * Open materialbox. - */ - open = () => { - this._updateVars(); - this.originalWidth = this.el.getBoundingClientRect().width; - this.originalHeight = this.el.getBoundingClientRect().height; - // Set states - this.doneAnimating = false; - this.el.classList.add('active'); - this.overlayActive = true; - // onOpenStart callback - if (typeof this.options.onOpenStart === 'function') - this.options.onOpenStart.call(this, this.el); - // Set positioning for placeholder - this.placeholder.style.width = this.placeholder.getBoundingClientRect().width + 'px'; - this.placeholder.style.height = this.placeholder.getBoundingClientRect().height + 'px'; - this.placeholder.style.position = 'relative'; - this.placeholder.style.top = '0'; - this.placeholder.style.left = '0'; - this._makeAncestorsOverflowVisible(); - // Set css on origin - this.el.style.position = 'absolute'; - this.el.style.zIndex = '1000'; - this.el.style.willChange = 'left, top, width, height'; - // Change from width or height attribute to css - this.attrWidth = this.el.getAttribute('width'); - this.attrHeight = this.el.getAttribute('height'); - if (this.attrWidth) { - this.el.style.width = this.attrWidth + 'px'; - this.el.removeAttribute('width'); - } - if (this.attrHeight) { - this.el.style.width = this.attrHeight + 'px'; - this.el.removeAttribute('height'); - } - this._addOverlay(); - // Add and animate caption if it exists - if (this.caption !== '') - this._addCaption(); - // Resize Image - const widthPercent = this.originalWidth / this.windowWidth; - const heightPercent = this.originalHeight / this.windowHeight; - this.newWidth = 0; - this.newHeight = 0; - if (widthPercent > heightPercent) { - // Width first - const ratio = this.originalHeight / this.originalWidth; - this.newWidth = this.windowWidth * 0.9; - this.newHeight = this.windowWidth * 0.9 * ratio; - } - else { - // Height first - const ratio = this.originalWidth / this.originalHeight; - this.newWidth = this.windowHeight * 0.9 * ratio; - this.newHeight = this.windowHeight * 0.9; - } - this._animateImageIn(); - // Handle Exit triggers - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleWindowResize); - window.addEventListener('keyup', this._handleWindowEscape); - }; - /** - * Close materialbox. - */ - close = () => { - this._updateVars(); - this.doneAnimating = false; - // onCloseStart callback - if (typeof this.options.onCloseStart === 'function') - this.options.onCloseStart.call(this, this.el); - //anim.remove(this.el); - //anim.remove(this._overlay); - //if (this.caption !== '') anim.remove(this._photoCaption); - // disable exit handlers - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleWindowResize); - window.removeEventListener('keyup', this._handleWindowEscape); - this._removeOverlay(); - this._animateImageOut(); - if (this.caption !== '') - this._removeCaption(); - }; - } - - const _defaults$c = { - opacity: 0.5, - inDuration: 250, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true, - dismissible: true, - startingTop: '4%', - endingTop: '10%' - }; - class Modal extends Component { - constructor(el, options) { - super(el, options, Modal); - this.el['M_Modal'] = this; - this.options = { - ...Modal.defaults, - ...options - }; - this.el.tabIndex = 0; - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$c; - } - static init(els, options = {}) { - return super.init(els, options, Modal); - } - static getInstance(el) { - return el['M_Modal']; - } - destroy() { } - _setupEventHandlers() { } - _removeEventHandlers() { } - _handleTriggerClick() { } - _handleOverlayClick() { } - _handleModalCloseClick() { } - _handleKeydown() { } - _handleFocus() { } - open() { - return this; - } - close() { - return this; - } - // Experimental! - static #createHtml(config) { - return ` - ${config.header ? '' : ''} - - ${config.header ? '' : ''} - `; - } - static #createHtmlElement(config) { - const dialog = document.createElement('dialog'); - dialog.id = config.id; - return dialog; - } - static create(config) { - return this.#createHtmlElement(config); - } - static { } - } - - const _defaults$b = { - edge: 'left', - draggable: true, - dragTargetWidth: '10px', - inDuration: 250, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true - }; - class Sidenav extends Component { - id; - /** Describes open/close state of Sidenav. */ - isOpen; - /** Describes if sidenav is fixed. */ - isFixed; - /** Describes if Sidenav is being dragged. */ - isDragged; - lastWindowWidth; - lastWindowHeight; - static _sidenavs; - _overlay; - dragTarget; - _startingXpos; - _xPos; - _time; - _width; - _initialScrollTop; - _verticallyScrolling; - deltaX; - velocityX; - percentOpen; - constructor(el, options) { - super(el, options, Sidenav); - this.el['M_Sidenav'] = this; - this.options = { - ...Sidenav.defaults, - ...options - }; - this.id = this.el.id; - this.isOpen = false; - this.isFixed = this.el.classList.contains('sidenav-fixed'); - this.isDragged = false; - // Window size variables for window resize checks - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - this._createOverlay(); - this._createDragTarget(); - this._setupEventHandlers(); - this._setupClasses(); - this._setupFixed(); - Sidenav._sidenavs.push(this); - } - static get defaults() { - return _defaults$b; - } - /** - * Initializes instances of Sidenav. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Sidenav); - } - static getInstance(el) { - return el['M_Sidenav']; - } - destroy() { - this._removeEventHandlers(); - this._enableBodyScrolling(); - this._overlay.parentNode.removeChild(this._overlay); - this.dragTarget.parentNode.removeChild(this.dragTarget); - this.el['M_Sidenav'] = undefined; - this.el.style.transform = ''; - const index = Sidenav._sidenavs.indexOf(this); - if (index >= 0) { - Sidenav._sidenavs.splice(index, 1); - } - } - _createOverlay() { - this._overlay = document.createElement('div'); - this._overlay.classList.add('sidenav-overlay'); - this._overlay.addEventListener('click', this.close); - document.body.appendChild(this._overlay); - } - _setupEventHandlers() { - if (Sidenav._sidenavs.length === 0) { - document.body.addEventListener('click', this._handleTriggerClick); - } - const passiveIfSupported = null; - this.dragTarget.addEventListener('touchmove', this._handleDragTargetDrag, passiveIfSupported); - this.dragTarget.addEventListener('touchend', this._handleDragTargetRelease); - this._overlay.addEventListener('touchmove', this._handleCloseDrag, passiveIfSupported); - this._overlay.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('touchmove', this._handleCloseDrag); // , passiveIfSupported); - this.el.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('click', this._handleCloseTriggerClick); - // Add resize for side nav fixed - if (this.isFixed) { - window.addEventListener('resize', this._handleWindowResize); - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - _removeEventHandlers() { - if (Sidenav._sidenavs.length === 1) { - document.body.removeEventListener('click', this._handleTriggerClick); - } - this.dragTarget.removeEventListener('touchmove', this._handleDragTargetDrag); - this.dragTarget.removeEventListener('touchend', this._handleDragTargetRelease); - this._overlay.removeEventListener('touchmove', this._handleCloseDrag); - this._overlay.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('touchmove', this._handleCloseDrag); - this.el.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('click', this._handleCloseTriggerClick); - // Remove resize for side nav fixed - if (this.isFixed) { - window.removeEventListener('resize', this._handleWindowResize); - } - } - _handleTriggerClick(e) { - const trigger = e.target.closest('.sidenav-trigger'); - if (e.target && trigger) { - const sidenavId = Utils.getIdFromTrigger(trigger); - const sidenavInstance = document.getElementById(sidenavId)['M_Sidenav']; - if (sidenavInstance) { - sidenavInstance.open(); - } - e.preventDefault(); - } - } - // Set variables needed at the beginning of drag and stop any current transition. - _startDrag(e) { - const clientX = e.targetTouches[0].clientX; - this.isDragged = true; - this._startingXpos = clientX; - this._xPos = this._startingXpos; - this._time = Date.now(); - this._width = this.el.getBoundingClientRect().width; - this._overlay.style.display = 'block'; - this._initialScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this._verticallyScrolling = false; - } - //Set variables needed at each drag move update tick - _dragMoveUpdate(e) { - const clientX = e.targetTouches[0].clientX; - const currentScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this.deltaX = Math.abs(this._xPos - clientX); - this._xPos = clientX; - this.velocityX = this.deltaX / (Date.now() - this._time); - this._time = Date.now(); - if (this._initialScrollTop !== currentScrollTop) { - this._verticallyScrolling = true; - } - } - _handleDragTargetDrag = (e) => { - // Check if draggable - if (!this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge === dragDirection) { - totalDeltaX = 0; - } - /** - * transformX is the drag displacement - * transformPrefix is the initial transform placement - * Invert values if Sidenav is right edge - */ - let transformX = totalDeltaX; - let transformPrefix = 'translateX(-100%)'; - if (this.options.edge === 'right') { - transformPrefix = 'translateX(100%)'; - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `${transformPrefix} translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _handleDragTargetRelease = () => { - if (this.isDragged) { - if (this.percentOpen > 0.2) { - this.open(); - } - else { - this._animateOut(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - _handleCloseDrag = (e) => { - // Check if open and draggable - if (!this.isOpen || !this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - // dragDirection is the attempted user drag direction - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge !== dragDirection) { - totalDeltaX = 0; - } - let transformX = -totalDeltaX; - if (this.options.edge === 'right') { - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, 1 - totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _calculateDelta = (e) => { - // If not being dragged, set initial drag start variables - if (!this.isDragged) { - this._startDrag(e); - } - // Run touchmove updates - this._dragMoveUpdate(e); - // Calculate raw deltaX - return this._xPos - this._startingXpos; - }; - _handleCloseRelease = () => { - if (this.isOpen && this.isDragged) { - if (this.percentOpen > 0.8) { - this._animateIn(); - } - else { - this.close(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - // Handles closing of Sidenav when element with class .sidenav-close - _handleCloseTriggerClick = (e) => { - const closeTrigger = e.target.closest('.sidenav-close'); - if (closeTrigger && !this._isCurrentlyFixed()) { - this.close(); - } - }; - _handleWindowResize = () => { - // Only handle horizontal resizes - if (this.lastWindowWidth !== window.innerWidth) { - if (window.innerWidth > 992) { - this.open(); - } - else { - this.close(); - } - } - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - }; - _setupClasses() { - if (this.options.edge === 'right') { - this.el.classList.add('right-aligned'); - this.dragTarget.classList.add('right-aligned'); - } - } - _removeClasses() { - this.el.classList.remove('right-aligned'); - this.dragTarget.classList.remove('right-aligned'); - } - _setupFixed() { - if (this._isCurrentlyFixed()) - this.open(); - } - _isDraggable() { - return this.options.draggable && !this._isCurrentlyFixed() && !this._verticallyScrolling; - } - _isCurrentlyFixed() { - return this.isFixed && window.innerWidth > 992; - } - _createDragTarget() { - const dragTarget = document.createElement('div'); - dragTarget.classList.add('drag-target'); - dragTarget.style.width = this.options.dragTargetWidth; - document.body.appendChild(dragTarget); - this.dragTarget = dragTarget; - } - _preventBodyScrolling() { - document.body.style.overflow = 'hidden'; - } - _enableBodyScrolling() { - document.body.style.overflow = ''; - } - /** - * Opens Sidenav. - */ - open = () => { - if (this.isOpen === true) - return; - this.isOpen = true; - // Run onOpenStart callback - if (typeof this.options.onOpenStart === 'function') { - this.options.onOpenStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - // Show if fixed - this.el.style.transform = 'translateX(0)'; - this._enableBodyScrolling(); - this._overlay.style.display = 'none'; - } - // Handle non-fixed Sidenav - else { - if (this.options.preventScrolling) - this._preventBodyScrolling(); - if (!this.isDragged || this.percentOpen != 1) - this._animateIn(); - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - /** - * Closes Sidenav. - */ - close = () => { - if (this.isOpen === false) - return; - this.isOpen = false; - // Run onCloseStart callback - if (typeof this.options.onCloseStart === 'function') { - this.options.onCloseStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - const transformX = this.options.edge === 'left' ? '-105%' : '105%'; - this.el.style.transform = `translateX(${transformX})`; - } - // Handle non-fixed Sidenav - else { - this._enableBodyScrolling(); - if (!this.isDragged || this.percentOpen != 0) { - this._animateOut(); - } - else { - this._overlay.style.display = 'none'; - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - _animateIn() { - this._animateSidenavIn(); - this._animateOverlayIn(); - } - _animateOut() { - this._animateSidenavOut(); - this._animateOverlayOut(); - } - _animateSidenavIn() { - let slideOutPercent = this.options.edge === 'left' ? -1 : 1; - if (this.isDragged) { - slideOutPercent = - this.options.edge === 'left' - ? slideOutPercent + this.percentOpen - : slideOutPercent - this.percentOpen; - } - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.transform = 'translateX(' + slideOutPercent * 100 + '%)'; - setTimeout(() => { - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(0)'; - }, 1); - setTimeout(() => { - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - } - _animateSidenavOut() { - const endPercent = this.options.edge === 'left' ? -1 : 1; - // let slideOutPercent = 0; - // if (this.isDragged) { - // // @todo unused variable - // slideOutPercent = - // this.options.edge === 'left' - // ? endPercent + this.percentOpen - // : endPercent - this.percentOpen; - // } - const duration = this.options.outDuration; - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(' + endPercent * 100 + '%)'; - setTimeout(() => { - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - _animateOverlayIn() { - let start = 0; - if (this.isDragged) - start = this.percentOpen; - else - this._overlay.style.display = 'block'; - // Animation - const duration = this.options.inDuration; - // from - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = start.toString(); - // easeOutQuad - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '1'; - }, 1); - } - _animateOverlayOut() { - const duration = this.options.outDuration; - // easeOutQuad - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '0'; - setTimeout(() => { - this._overlay.style.display = 'none'; - }, duration); - } - _setAriaHidden = () => { - this.el.ariaHidden = this.isOpen ? 'false' : 'true'; - const navWrapper = document.querySelector('.nav-wrapper ul'); - if (navWrapper) - navWrapper.ariaHidden = this.isOpen.toString(); - }; - _setTabIndex = () => { - const navLinks = document.querySelectorAll('.nav-wrapper ul li a'); - const sideNavLinks = document.querySelectorAll('.sidenav li a'); - if (navLinks) - navLinks.forEach((navLink) => { - navLink.tabIndex = this.isOpen ? -1 : 0; - }); - if (sideNavLinks) - sideNavLinks.forEach((sideNavLink) => { - sideNavLink.tabIndex = this.isOpen ? 0 : -1; - }); - }; - static { - Sidenav._sidenavs = []; - } - } - - const _defaults$a = { - indicators: true, - height: 400, - duration: 500, - interval: 6000, - pauseOnFocus: true, - pauseOnHover: true, - indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA) - }; - class Slider extends Component { - /** Index of current slide. */ - activeIndex; - interval; - eventPause; - _slider; - _slides; - _activeSlide; - _indicators; - _hovered; - _focused; - _focusCurrent; - _sliderId; - constructor(el, options) { - super(el, options, Slider); - this.el['M_Slider'] = this; - this.options = { - ...Slider.defaults, - ...options - }; - // init props - this.interval = null; - this.eventPause = false; - this._hovered = false; - this._focused = false; - this._focusCurrent = false; - // setup - this._slider = this.el.querySelector('.slides'); - this._slides = Array.from(this._slider.querySelectorAll('li')); - this.activeIndex = this._slides.findIndex((li) => li.classList.contains('active')); - if (this.activeIndex !== -1) { - this._activeSlide = this._slides[this.activeIndex]; - } - this._setSliderHeight(); - // Sets element id if it does not have one - if (this._slider.hasAttribute('id')) - this._sliderId = this._slider.getAttribute('id'); - else { - this._sliderId = 'slider-' + Utils.guid(); - this._slider.setAttribute('id', this._sliderId); - } - const placeholderBase64 = 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; - // Set initial positions of captions - this._slides.forEach((slide) => { - // Caption - //const caption = slide.querySelector('.caption'); - //if (caption) this._animateCaptionIn(caption, 0); - // Set Images as Background Images - const img = slide.querySelector('img'); - if (img) { - if (img.src !== placeholderBase64) { - img.style.backgroundImage = 'url(' + img.src + ')'; - img.src = placeholderBase64; - } - } - // Sets slide as focusable by code - if (!slide.hasAttribute('tabindex')) - slide.setAttribute('tabindex', '-1'); - // Removes initial visibility from "inactive" slides - slide.style.visibility = 'hidden'; - }); - this._setupIndicators(); - // Show active slide - if (this._activeSlide) { - this._activeSlide.style.display = 'block'; - this._activeSlide.style.visibility = 'visible'; - } - else { - this.activeIndex = 0; - this._slides[0].classList.add('active'); - this._slides[0].style.visibility = 'visible'; - this._activeSlide = this._slides[0]; - this._animateSlide(this._slides[0], true); - // Update indicators - if (this.options.indicators) { - this._indicators[this.activeIndex].children[0].classList.add('active'); - } - } - this._setupEventHandlers(); - // auto scroll - this.start(); - } - static get defaults() { - return _defaults$a; - } - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Slider); - } - static getInstance(el) { - return el['M_Slider']; - } - destroy() { - this.pause(); - this._removeIndicators(); - this._removeEventHandlers(); - this.el['M_Slider'] = undefined; - } - _setupEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.addEventListener('focusin', this._handleAutoPauseFocus); - this.el.addEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.addEventListener('mouseenter', this._handleAutoPauseHover); - this.el.addEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.addEventListener('click', this._handleIndicatorClick); - }); - } - } - _removeEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.removeEventListener('focusin', this._handleAutoPauseFocus); - this.el.removeEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.removeEventListener('mouseenter', this._handleAutoPauseHover); - this.el.removeEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.removeEventListener('click', this._handleIndicatorClick); - }); - } - } - _handleIndicatorClick = (e) => { - const el = e.target.parentElement; - const currIndex = [...el.parentNode.children].indexOf(el); - this._focusCurrent = true; - this.set(currIndex); - }; - _handleAutoPauseHover = () => { - this._hovered = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoPauseFocus = () => { - this._focused = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoStartHover = () => { - this._hovered = false; - if (!(this.options.pauseOnFocus && this._focused) && this.eventPause) { - this.start(); - } - }; - _handleAutoStartFocus = () => { - this._focused = false; - if (!(this.options.pauseOnHover && this._hovered) && this.eventPause) { - this.start(); - } - }; - _handleInterval = () => { - const activeElem = this._slider.querySelector('.active'); - let newActiveIndex = [...activeElem.parentNode.children].indexOf(activeElem); - if (this._slides.length === newActiveIndex + 1) - newActiveIndex = 0; // loop to start - else - newActiveIndex += 1; - this.set(newActiveIndex); - }; - _animateSlide(slide, isDirectionIn) { - let dx = 0, dy = 0; - // from - slide.style.opacity = isDirectionIn ? '0' : '1'; - setTimeout(() => { - slide.style.transition = `opacity ${this.options.duration}ms ease`; - // to - slide.style.opacity = isDirectionIn ? '1' : '0'; - }, 1); - // Caption - const caption = slide.querySelector('.caption'); - if (!caption) - return; - if (caption.classList.contains('center-align')) - dy = -100; - else if (caption.classList.contains('right-align')) - dx = 100; - else if (caption.classList.contains('left-align')) - dx = -100; - // from - caption.style.opacity = isDirectionIn ? '0' : '1'; - caption.style.transform = isDirectionIn ? `translate(${dx}px, ${dy}px)` : `translate(0, 0)`; - setTimeout(() => { - caption.style.transition = `opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`; - // to - caption.style.opacity = isDirectionIn ? '1' : '0'; - caption.style.transform = isDirectionIn ? `translate(0, 0)` : `translate(${dx}px, ${dy}px)`; - }, this.options.duration); // delay - } - _setSliderHeight() { - // If fullscreen, do nothing - if (!this.el.classList.contains('fullscreen')) { - if (this.options.indicators) { - // Add height if indicators are present - this.el.style.height = this.options.height + 40 + 'px'; //.css('height', this.options.height + 40 + 'px'); - } - else { - this.el.style.height = this.options.height + 'px'; - } - this._slider.style.height = this.options.height + 'px'; - } - } - _setupIndicators() { - if (this.options.indicators) { - const ul = document.createElement('ul'); - ul.classList.add('indicators'); - const arrLi = []; - this._slides.forEach((el, i) => { - const label = this.options.indicatorLabelFunc - ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) - : `${i + 1}`; - const li = document.createElement('li'); - li.classList.add('indicator-item'); - li.innerHTML = ``; - arrLi.push(li); - ul.append(li); - }); - this.el.append(ul); - this._indicators = arrLi; - } - } - _removeIndicators() { - this.el.querySelector('ul.indicators').remove(); //find('ul.indicators').remove(); - } - set(index) { - // Wrap around indices. - if (index >= this._slides.length) - index = 0; - else if (index < 0) - index = this._slides.length - 1; - // Only do if index changes - if (this.activeIndex === index) - return; - this._activeSlide = this._slides[this.activeIndex]; - const _caption = this._activeSlide.querySelector('.caption'); - this._activeSlide.classList.remove('active'); - // Enables every slide - this._slides.forEach((slide) => (slide.style.visibility = 'visible')); - //--- Hide active Slide + Caption - this._activeSlide.style.opacity = '0'; - setTimeout(() => { - this._slides.forEach((slide) => { - if (slide.classList.contains('active')) - return; - slide.style.opacity = '0'; - slide.style.transform = 'translate(0, 0)'; - // Disables invisible slides (for assistive technologies) - slide.style.visibility = 'hidden'; - }); - }, this.options.duration); - // Hide active Caption - //this._animateCaptionIn(_caption, this.options.duration); - _caption.style.opacity = '0'; - // Update indicators - if (this.options.indicators) { - const activeIndicator = this._indicators[this.activeIndex].children[0]; - const nextIndicator = this._indicators[index].children[0]; - activeIndicator.classList.remove('active'); - nextIndicator.classList.add('active'); - if (typeof this.options.indicatorLabelFunc === 'function') { - activeIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, this.activeIndex, false); - nextIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, index, true); - } - } - //--- Show new Slide + Caption - this._animateSlide(this._slides[index], true); - this._slides[index].classList.add('active'); - this.activeIndex = index; - // Reset interval, if allowed. This check prevents autostart - // when slider is paused, since it can be changed though indicators. - if (this.interval != null) { - this.start(); - } - } - _pause(fromEvent) { - clearInterval(this.interval); - this.eventPause = fromEvent; - this.interval = null; - } - /** - * Pause slider autoslide. - */ - pause = () => { - this._pause(false); - }; - /** - * Start slider autoslide. - */ - start = () => { - clearInterval(this.interval); - this.interval = setInterval(this._handleInterval, this.options.duration + this.options.interval); - this.eventPause = false; - }; - /** - * Move to next slider. - */ - next = () => { - let newIndex = this.activeIndex + 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; - /** - * Move to prev slider. - */ - prev = () => { - let newIndex = this.activeIndex - 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; - } - - const _defaults$9 = { - duration: 300, - onShow: null, - swipeable: false, - responsiveThreshold: Infinity // breakpoint for swipeable - }; - class Tabs extends Component { - _tabLinks; - _index; - _indicator; - _tabWidth; - _tabsWidth; - _tabsCarousel; - _activeTabLink; - _content; - constructor(el, options) { - super(el, options, Tabs); - this.el['M_Tabs'] = this; - this.options = { - ...Tabs.defaults, - ...options - }; - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - this._index = 0; - this._setupActiveTabLink(); - if (this.options.swipeable) { - this._setupSwipeableTabs(); - } - else { - this._setupNormalTabs(); - } - // Setup tabs indicator after content to ensure accurate widths - this._setTabsAndTabWidth(); - this._createIndicator(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$9; - } - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tabs); - } - static getInstance(el) { - return el['M_Tabs']; - } - destroy() { - this._removeEventHandlers(); - this._indicator.parentNode.removeChild(this._indicator); - if (this.options.swipeable) { - this._teardownSwipeableTabs(); - } - else { - this._teardownNormalTabs(); - } - this.el['M_Tabs'] = undefined; - } - /** - * The index of tab that is currently shown. - */ - get index() { - return this._index; - } - _setupEventHandlers() { - window.addEventListener('resize', this._handleWindowResize); - this.el.addEventListener('click', this._handleTabClick); - // map vertical scrolling to horizontal scrolling because no scrollbar is shown on desktop - this.el.addEventListener('wheel', (event) => { - if (event.deltaY !== 0) { - event.preventDefault(); - this.el.scrollLeft += event.deltaY * 1; - } - }); - } - _removeEventHandlers() { - window.removeEventListener('resize', this._handleWindowResize); - this.el.removeEventListener('click', this._handleTabClick); - } - _handleWindowResize = () => { - this._setTabsAndTabWidth(); - if (this._tabWidth !== 0 && this._tabsWidth !== 0) { - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - }; - _handleTabClick = (e) => { - let tabLink = e.target; - if (!tabLink) - return; - let tab = tabLink.parentElement; - while (tab && !tab.classList.contains('tab')) { - tabLink = tabLink.parentElement; - tab = tab.parentElement; - } - // Handle click on tab link only - if (!tabLink || !tab.classList.contains('tab')) - return; - // is disabled? - if (tab.classList.contains('disabled')) { - e.preventDefault(); - return; - } - // Act as regular link if target attribute is specified. - if (tabLink.hasAttribute('target')) - return; - // Make the old tab inactive. - this._activeTabLink.classList.remove('active'); - const _oldContent = this._content; - // Update the variables with the new link and content - this._activeTabLink = tabLink; - if (tabLink.hash) - this._content = document.querySelector(tabLink.hash); - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - // Make the tab active - this._activeTabLink.classList.add('active'); - const prevIndex = this._index; - this._index = Math.max(Array.from(this._tabLinks).indexOf(tabLink), 0); - // Swap content - if (this.options.swipeable) { - if (this._tabsCarousel) { - this._tabsCarousel.set(this._index, () => { - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - }); - } - } - else { - if (this._content) { - this._content.style.display = 'block'; - this._content.classList.add('active'); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - if (_oldContent && _oldContent !== this._content) { - _oldContent.style.display = 'none'; - _oldContent.classList.remove('active'); - } - } - } - // Update widths after content is swapped (scrollbar bugfix) - this._setTabsAndTabWidth(); - this._animateIndicator(prevIndex); - e.preventDefault(); - }; - _createIndicator() { - const indicator = document.createElement('li'); - indicator.classList.add('indicator'); - this.el.appendChild(indicator); - this._indicator = indicator; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - _setupActiveTabLink() { - // If the location.hash matches one of the links, use that as the active tab. - this._activeTabLink = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === location.hash); - // If no match is found, use the first link or any with class 'active' as the initial active tab. - if (!this._activeTabLink) { - let activeTabLink = this.el.querySelector('li.tab a.active'); - if (!activeTabLink) { - activeTabLink = this.el.querySelector('li.tab a'); - } - this._activeTabLink = activeTabLink; - } - Array.from(this._tabLinks).forEach((a) => a.classList.remove('active')); - this._activeTabLink.classList.add('active'); - this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); - if (this._activeTabLink && this._activeTabLink.hash) { - this._content = document.querySelector(this._activeTabLink.hash); - if (this._content) - this._content.classList.add('active'); - } - } - _setupSwipeableTabs() { - // Change swipeable according to responsive threshold - if (window.innerWidth > this.options.responsiveThreshold) - this.options.swipeable = false; - const tabsContent = []; - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - currContent.classList.add('carousel-item'); - tabsContent.push(currContent); - } - }); - // Create Carousel-Wrapper around Tab-Contents - const tabsWrapper = document.createElement('div'); - tabsWrapper.classList.add('tabs-content', 'carousel', 'carousel-slider'); - // Wrap around - tabsContent[0].parentElement.insertBefore(tabsWrapper, tabsContent[0]); - tabsContent.forEach((tabContent) => { - tabsWrapper.appendChild(tabContent); - tabContent.style.display = ''; - }); - // Keep active tab index to set initial carousel slide - const tab = this._activeTabLink.parentElement; - const activeTabIndex = Array.from(tab.parentNode.children).indexOf(tab); - this._tabsCarousel = Carousel.init(tabsWrapper, { - fullWidth: true, - noWrap: true, - onCycleTo: (item) => { - const prevIndex = this._index; - this._index = Array.from(item.parentNode.children).indexOf(item); - this._activeTabLink.classList.remove('active'); - this._activeTabLink = Array.from(this._tabLinks)[this._index]; - this._activeTabLink.classList.add('active'); - this._animateIndicator(prevIndex); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - } - }); - // Set initial carousel slide to active tab - this._tabsCarousel.set(activeTabIndex); - } - _teardownSwipeableTabs() { - const tabsWrapper = this._tabsCarousel.el; - this._tabsCarousel.destroy(); - // Unwrap - tabsWrapper.append(tabsWrapper.parentElement); - tabsWrapper.remove(); - } - _setupNormalTabs() { - // Hide Tabs Content - Array.from(this._tabLinks).forEach((a) => { - if (a === this._activeTabLink) - return; - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = 'none'; - } - }); - } - _teardownNormalTabs() { - // show Tabs Content - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = ''; - } - }); - } - _setTabsAndTabWidth() { - this._tabsWidth = this.el.getBoundingClientRect().width; - this._tabWidth = Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length; - } - _calcRightPos(el) { - return Math.ceil(this._tabsWidth - el.offsetLeft - el.getBoundingClientRect().width); - } - _calcLeftPos(el) { - return Math.floor(el.offsetLeft); - } - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ - updateTabIndicator() { - this._setTabsAndTabWidth(); - this._animateIndicator(this._index); - } - _animateIndicator(prevIndex) { - let leftDelay = 0, rightDelay = 0; - const isMovingLeftOrStaying = this._index - prevIndex >= 0; - if (isMovingLeftOrStaying) - leftDelay = 90; - else - rightDelay = 90; - // in v1: easeOutQuad - this._indicator.style.transition = ` - left ${this.options.duration}ms ease-out ${leftDelay}ms, - right ${this.options.duration}ms ease-out ${rightDelay}ms`; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ - select(tabId) { - const tab = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === '#' + tabId); - if (tab) - tab.click(); - } - } - - const _defaults$8 = { - dialRadius: 135, - outerRadius: 105, - innerRadius: 70, - tickRadius: 20, - duration: 350, - container: null, - defaultTime: 'now', // default time, 'now' or '13:14' e.g. - fromNow: 0, // Millisecond offset from the defaultTime - showClearBtn: false, - autoSubmit: true, - // internationalization - i18n: { - cancel: 'Cancel', - clear: 'Clear', - done: 'Ok' - }, - twelveHour: true, // change to 12 hour AM/PM clock from 24 hour - vibrate: true, // vibrate the device when dragging clock hand - // Callbacks - onSelect: null, - onInputInteraction: null, - onDone: null, - onCancel: null, - displayPlugin: null, - displayPluginOptions: null - }; - class Timepicker extends Component { - id; - containerEl; - plate; - digitalClock; - inputHours; - inputMinutes; - x0; - y0; - moved; - dx; - dy; - /** - * Current view on the timepicker. - * @default 'hours' - */ - currentView; - hand; - minutesView; - hours; - minutes; - /** The selected time. */ - time; - /** - * If the time is AM or PM on twelve-hour clock. - * @default 'PM' - */ - amOrPm; - static _template; - /** Vibrate device when dragging clock hand. */ - vibrate; - _canvas; - hoursView; - spanAmPm; - footer; - _amBtn; - _pmBtn; - bg; - bearing; - g; - toggleViewTimer; - vibrateTimer; - displayPlugin; - constructor(el, options) { - super(el, options, Timepicker); - this.el['M_Timepicker'] = this; - this.options = { - ...Timepicker.defaults, - ...options - }; - this.id = Utils.guid(); - this._insertHTMLIntoDOM(); - this._setupVariables(); - this._setupEventHandlers(); - this._clockSetup(); - if (this.options.displayPlugin) - this._setupDisplayPlugin(); - this._pickerSetup(); - } - static get defaults() { - return _defaults$8; - } - /** - * Initializes instances of Timepicker. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Timepicker); - } - static _addLeadingZero(num) { - return (num < 10 ? '0' : '') + num; - } - static _createSVGEl(name) { - const svgNS = 'http://www.w3.org/2000/svg'; - return document.createElementNS(svgNS, name); - } - static _Pos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return { - x: e.targetTouches[0].clientX, - y: e.targetTouches[0].clientY - }; - } - // mouse event - return { x: e.clientX, y: e.clientY }; - } - static getInstance(el) { - return el['M_Timepicker']; - } - destroy() { - this._removeEventHandlers(); - this.containerEl.remove(); - this.el['M_Timepicker'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - // @todo allow input field to fill values from input field when container/modal opens - this.el.addEventListener('keydown', this._handleInputKeydown); - this.plate.addEventListener('mousedown', this._handleClockClickStart); - this.plate.addEventListener('touchstart', this._handleClockClickStart); - this.digitalClock.addEventListener('keyup', this._inputFromTextField); - this.inputHours.addEventListener('focus', () => this.showView('hours')); - this.inputHours.addEventListener('focusout', () => this.formatHours()); - this.inputMinutes.addEventListener('focus', () => this.showView('minutes')); - this.inputMinutes.addEventListener('focusout', () => this.formatMinutes()); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - } - _handleInputClick = () => { - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - } - }; - _handleTimeInputEnterKey = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this._inputFromTextField(); - } - }; - _handleClockClickStart = (e) => { - e.preventDefault(); - const clockPlateBR = this.plate.getBoundingClientRect(); - const offset = { x: clockPlateBR.left, y: clockPlateBR.top }; - this.x0 = offset.x + this.options.dialRadius; - this.y0 = offset.y + this.options.dialRadius; - this.moved = false; - const clickPos = Timepicker._Pos(e); - this.dx = clickPos.x - this.x0; - this.dy = clickPos.y - this.y0; - // Set clock hands - this.setHand(this.dx, this.dy, false); - // Mousemove on document - document.addEventListener('mousemove', this._handleDocumentClickMove); - document.addEventListener('touchmove', this._handleDocumentClickMove); - // Mouseup on document - document.addEventListener('mouseup', this._handleDocumentClickEnd); - document.addEventListener('touchend', this._handleDocumentClickEnd); - }; - _handleDocumentClickMove = (e) => { - e.preventDefault(); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - this.moved = true; - this.setHand(x, y, false); - }; - _handleDocumentClickEnd = (e) => { - e.preventDefault(); - document.removeEventListener('mouseup', this._handleDocumentClickEnd); - document.removeEventListener('touchend', this._handleDocumentClickEnd); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - if (this.moved && x === this.dx && y === this.dy) { - this.setHand(x, y); - } - if (this.currentView === 'hours') { - this.inputMinutes.focus(); - this.showView('minutes', this.options.duration / 2); - } - else { - // this.minutesView.classList.add('timepicker-dial-out'); - setTimeout(() => { - if (this.options.autoSubmit) - this.done(); - }, this.options.duration / 2); - } - if (typeof this.options.onSelect === 'function') { - this.options.onSelect.call(this, this.hours, this.minutes); - } - // Unbind mousemove event - document.removeEventListener('mousemove', this._handleDocumentClickMove); - document.removeEventListener('touchmove', this._handleDocumentClickMove); - }; - _insertHTMLIntoDOM() { - const template = document.createElement('template'); - template.innerHTML = Timepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.containerEl.id = 'container-' + this.id; - // Append popover to input by default - const optEl = this.options.container; - const containerEl = optEl instanceof HTMLElement ? optEl : document.querySelector(optEl); - if (this.options.container && !!containerEl) { - containerEl.append(this.containerEl); - } - else { - this.el.parentElement.appendChild(this.containerEl); - } - } - _setupVariables() { - this.currentView = 'hours'; - this.vibrate = navigator.vibrate - ? 'vibrate' - : navigator['webkitVibrate'] - ? 'webkitVibrate' - : null; - this._canvas = this.containerEl.querySelector('.timepicker-canvas'); - this.plate = this.containerEl.querySelector('.timepicker-plate'); - this.digitalClock = this.containerEl.querySelector('.timepicker-display-column'); - this.hoursView = this.containerEl.querySelector('.timepicker-hours'); - this.minutesView = this.containerEl.querySelector('.timepicker-minutes'); - this.inputHours = this.containerEl.querySelector('.timepicker-input-hours'); - this.inputMinutes = this.containerEl.querySelector('.timepicker-input-minutes'); - this.spanAmPm = this.containerEl.querySelector('.timepicker-span-am-pm'); - this.footer = this.containerEl.querySelector('.timepicker-footer'); - this.amOrPm = 'PM'; - } - /*private _createButton(text: string, visibility: string): HTMLButtonElement { - const button = document.createElement('button'); - button.classList.add('btn', 'waves-effect', 'text'); - button.style.visibility = visibility; - button.type = 'button'; - button.tabIndex = -1; - button.innerText = text; - return button; - }*/ - _pickerSetup() { - // clearButton.classList.add('timepicker-clear'); - // clearButton.addEventListener('click', this.clear); - // this.footer.appendChild(clearButton); - if (this.options.showClearBtn) { - Utils.createButton(this.footer, this.options.i18n.clear, ['timepicker-clear'], true, this.clear); - } - if (!this.options.autoSubmit) { - /*const confirmationBtnsContainer = document.createElement('div'); - confirmationBtnsContainer.classList.add('confirmation-btns'); - this.footer.append(confirmationBtnsContainer); - - const cancelButton = this._createButton(this.options.i18n.cancel, ''); - cancelButton.classList.add('timepicker-close'); - cancelButton.addEventListener('click', this.close); - confirmationBtnsContainer.appendChild(cancelButton); - - const doneButton = this._createButton(this.options.i18n.done, ''); - doneButton.classList.add('timepicker-close'); - //doneButton.addEventListener('click', this._finishSelection); - confirmationBtnsContainer.appendChild(doneButton);*/ - Utils.createConfirmationContainer(this.footer, this.options.i18n.done, this.options.i18n.cancel, this.confirm, this.cancel); - } - this._updateTimeFromInput(); - this.showView('hours'); - } - _setupDisplayPlugin() { - if (this.options.displayPlugin === 'docked') - this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); - if (this.options.displayPlugin === 'modal') { - this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { - ...this.options.displayPluginOptions, - ...{ classList: ['timepicker-modal'] } - }); - this.footer.remove(); - this.footer = this.displayPlugin.footer; - } - } - _clockSetup() { - if (this.options.twelveHour) { - // AM Button - this._amBtn = document.createElement('div'); - this._amBtn.classList.add('am-btn', 'btn'); - this._amBtn.innerText = 'AM'; - this._amBtn.tabIndex = 0; - this._amBtn.addEventListener('click', this._handleAmPmClick); - this._amBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._amBtn); - // PM Button - this._pmBtn = document.createElement('div'); - this._pmBtn.classList.add('pm-btn', 'btn'); - this._pmBtn.innerText = 'PM'; - this._pmBtn.tabIndex = 0; - this._pmBtn.addEventListener('click', this._handleAmPmClick); - this._pmBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._pmBtn); - } - this._buildHoursView(); - this._buildMinutesView(); - this._buildSVGClock(); - } - _buildSVGClock() { - // Draw clock hands and others - const dialRadius = this.options.dialRadius; - const tickRadius = this.options.tickRadius; - const diameter = dialRadius * 2; - const svg = Timepicker._createSVGEl('svg'); - svg.setAttribute('class', 'timepicker-svg'); - svg.setAttribute('width', diameter.toString()); - svg.setAttribute('height', diameter.toString()); - const g = Timepicker._createSVGEl('g'); - g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')'); - const bearing = Timepicker._createSVGEl('circle'); - bearing.setAttribute('class', 'timepicker-canvas-bearing'); - bearing.setAttribute('cx', '0'); - bearing.setAttribute('cy', '0'); - bearing.setAttribute('r', '4'); - const hand = Timepicker._createSVGEl('line'); - hand.setAttribute('x1', '0'); - hand.setAttribute('y1', '0'); - const bg = Timepicker._createSVGEl('circle'); - bg.setAttribute('class', 'timepicker-canvas-bg'); - bg.setAttribute('r', tickRadius.toString()); - g.appendChild(hand); - g.appendChild(bg); - g.appendChild(bearing); - svg.appendChild(g); - this._canvas.appendChild(svg); - this.hand = hand; - this.bg = bg; - this.bearing = bearing; - this.g = g; - } - _buildHoursView() { - // const $tick = document.createElement('div'); - // $tick.classList.add('timepicker-tick'); - // Hours view - if (this.options.twelveHour) { - for (let i = 1; i < 13; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const radius = this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - else { - for (let i = 0; i < 24; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const inner = i > 0 && i < 13; - const radius = inner ? this.options.innerRadius : this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - } - _buildHoursTick(i, radian, radius) { - const tick = document.createElement('div'); - tick.classList.add('timepicker-tick'); - tick.style.left = - this.options.dialRadius + Math.sin(radian) * radius - this.options.tickRadius + 'px'; - tick.style.top = - this.options.dialRadius - Math.cos(radian) * radius - this.options.tickRadius + 'px'; - tick.innerHTML = i === 0 ? '00' : i.toString(); - this.hoursView.appendChild(tick); - } - _buildMinutesView() { - const _tick = document.createElement('div'); - _tick.classList.add('timepicker-tick'); - // Minutes view - for (let i = 0; i < 60; i += 5) { - const tick = _tick.cloneNode(true); - const radian = (i / 30) * Math.PI; - tick.style.left = - this.options.dialRadius + - Math.sin(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.style.top = - this.options.dialRadius - - Math.cos(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.innerHTML = Timepicker._addLeadingZero(i); - this.minutesView.appendChild(tick); - } - } - _handleAmPmClick = (e) => { - this._handleAmPmInteraction(e.target); - }; - _handleAmPmKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleAmPmInteraction(e.target); - } - }; - _handleAmPmInteraction = (e) => { - this.amOrPm = e.classList.contains('am-btn') ? 'AM' : 'PM'; - this._updateAmPmView(); - }; - _updateAmPmView() { - if (this.options.twelveHour) { - if (this.amOrPm === 'PM') { - this._amBtn.classList.remove('filled'); - this._pmBtn.classList.add('filled'); - } - else if (this.amOrPm === 'AM') { - this._amBtn.classList.add('filled'); - this._pmBtn.classList.remove('filled'); - } - } - } - _updateTimeFromInput() { - // Get the time - let value = ((this.el.value || this.options.defaultTime || '') + '').split(':'); - if (this.options.twelveHour && !(typeof value[1] === 'undefined')) { - if (value[1].toUpperCase().indexOf('AM') > 0) { - this.amOrPm = 'AM'; - } - else { - this.amOrPm = 'PM'; - } - value[1] = value[1].replace('AM', '').replace('PM', ''); - } - if (value[0] === 'now') { - const now = new Date(+new Date() + this.options.fromNow); - value = [now.getHours().toString(), now.getMinutes().toString()]; - if (this.options.twelveHour) { - this.amOrPm = parseInt(value[0]) >= 12 && parseInt(value[0]) < 24 ? 'PM' : 'AM'; - } - } - this.hours = +value[0] || 0; - this.minutes = +value[1] || 0; - this.inputHours.value = Timepicker._addLeadingZero(this.hours); - this.inputMinutes.value = Timepicker._addLeadingZero(this.minutes); - this._updateAmPmView(); - } - /** - * Show hours or minutes view on timepicker. - * @param view The name of the view you want to switch to, 'hours' or 'minutes'. - * @param delay - */ - showView = (view, delay = null) => { - if (view === 'minutes' && getComputedStyle(this.hoursView).visibility === 'visible') ; - const isHours = view === 'hours', nextView = isHours ? this.hoursView : this.minutesView, hideView = isHours ? this.minutesView : this.hoursView; - this.currentView = view; - /*if (isHours) { - this.inputHours.classList.add('text-primary'); - this.inputMinutes.classList.remove('text-primary'); - } else { - this.inputHours.classList.remove('text-primary'); - this.inputMinutes.classList.add('text-primary'); - }*/ - // Transition view - hideView.classList.add('timepicker-dial-out'); - nextView.style.visibility = 'visible'; - nextView.classList.remove('timepicker-dial-out'); - // Reset clock hand - this.resetClock(delay); - // After transitions ended - clearTimeout(this.toggleViewTimer); - this.toggleViewTimer = setTimeout(() => { - hideView.style.visibility = 'hidden'; - }, this.options.duration); - }; - resetClock(delay) { - const view = this.currentView, value = this[view], isHours = view === 'hours', unit = Math.PI / (isHours ? 6 : 30), radian = value * unit, radius = isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius, x = Math.sin(radian) * radius, y = -Math.cos(radian) * radius; - if (delay) { - this._canvas?.classList.add('timepicker-canvas-out'); - setTimeout(() => { - this._canvas?.classList.remove('timepicker-canvas-out'); - this.setHand(x, y); - }, delay); - } - else { - this.setHand(x, y); - } - } - _inputFromTextField = () => { - const isHours = this.currentView === 'hours'; - if (isHours && this.inputHours.value !== '') { - const value = parseInt(this.inputHours.value); - if (value > 0 && value < (this.options.twelveHour ? 13 : 24)) { - this.hours = value; - } - else { - this.setHoursDefault(); - } - this.drawClockFromTimeInput(this.hours, isHours); - } - else if (!isHours && this.inputMinutes.value !== '') { - const value = parseInt(this.inputMinutes.value); - if (value >= 0 && value < 60) { - this.minutes = value; - } - else { - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = this.minutes.toString(); - } - this.drawClockFromTimeInput(this.minutes, isHours); - } - }; - drawClockFromTimeInput(value, isHours) { - const unit = Math.PI / (isHours ? 6 : 30); - const radian = value * unit; - let radius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - else { - radius = - isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius; - } - this.setClockAttributes(radian, radius); - } - setHand(x, y, roundBy5 = false) { - const isHours = this.currentView === 'hours', unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), inner = isHours && z < (this.options.outerRadius + this.options.innerRadius) / 2; - let radian = Math.atan2(x, -y), radius = inner ? this.options.innerRadius : this.options.outerRadius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - // Radian should in range [0, 2PI] - if (radian < 0) { - radian = Math.PI * 2 + radian; - } - // Get the round value - let value = Math.round(radian / unit); - // Get the round radian - radian = value * unit; - // Correct the hours or minutes - if (this.options.twelveHour) { - if (isHours) { - if (value === 0) - value = 12; - } - else { - if (roundBy5) - value *= 5; - if (value === 60) - value = 0; - } - } - else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } - else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } - } - } - // Once hours or minutes changed, vibrate the device - if (this[this.currentView] !== value) { - if (this.vibrate && this.options.vibrate) { - // Do not vibrate too frequently - if (!this.vibrateTimer) { - navigator[this.vibrate](10); - this.vibrateTimer = setTimeout(() => { - this.vibrateTimer = null; - }, 100); - } - } - } - this[this.currentView] = value; - if (isHours) { - this.inputHours.value = Timepicker._addLeadingZero(value); - } - else { - this.inputMinutes.value = Timepicker._addLeadingZero(value); - } - // Set clock hand and others' position - this.setClockAttributes(radian, radius); - } - setClockAttributes(radian, radius) { - const cx1 = Math.sin(radian) * (radius - this.options.tickRadius), cy1 = -Math.cos(radian) * (radius - this.options.tickRadius), cx2 = Math.sin(radian) * radius, cy2 = -Math.cos(radian) * radius; - this.hand.setAttribute('x2', cx1.toString()); - this.hand.setAttribute('y2', cy1.toString()); - this.bg.setAttribute('cx', cx2.toString()); - this.bg.setAttribute('cy', cy2.toString()); - } - formatHours() { - if (this.inputHours.value == '') - this.setHoursDefault(); - this.inputHours.value = Timepicker._addLeadingZero(Number(this.inputHours.value)); - } - formatMinutes() { - if (this.inputMinutes.value == '') - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = Timepicker._addLeadingZero(Number(this.inputMinutes.value)); - } - setHoursDefault() { - this.hours = new Date().getHours(); - this.inputHours.value = (this.hours % (this.options.twelveHour ? 12 : 24)).toString(); - } - done = (clearValue = null) => { - // Set input value - const last = this.el.value; - let value = clearValue - ? '' - : Timepicker._addLeadingZero(this.hours) + ':' + Timepicker._addLeadingZero(this.minutes); - this.time = value; - if (!clearValue && this.options.twelveHour) { - value = `${value} ${this.amOrPm}`; - } - this.el.value = value; - // Trigger change event - if (value !== last) { - this.el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - } - }; - confirm = () => { - this.done(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onDone === 'function') - this.options.onDone.call(this); - }; - cancel = () => { - // not logical clearing the input field on cancel, since the end user might want to make use of the previously submitted value - // this.clear(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - clear = () => { - // @todo should clear timepicker hour/minute input elems and reset analog clock, currently clears input el - this.done(true); - }; - // deprecated - open() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Timepicker._template = `
-
-
-
-
- -
-
- : -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
`; - } - } - - const _defaults$7 = { - text: '', - displayLength: 4000, - inDuration: 300, - outDuration: 375, - classes: '', - completeCallback: null, - activationPercent: 0.8 - }; - class Toast { - /** The toast element. */ - el; - /** - * The remaining amount of time in ms that the toast - * will stay before dismissal. - */ - timeRemaining; - /** - * Describes the current pan state of the Toast. - */ - panning; - options; - message; - counterInterval; - wasSwiped; - startingXPos; - xPos; - time; - deltaX; - velocityX; - static _toasts; - static _container; - static _draggedToast; - constructor(options) { - this.options = { - ...Toast.defaults, - ...options - }; - this.message = this.options.text; - this.panning = false; - this.timeRemaining = this.options.displayLength; - if (Toast._toasts.length === 0) { - Toast._createContainer(); - } - // Create new toast - Toast._toasts.push(this); - const toastElement = this._createToast(); - toastElement['M_Toast'] = this; - this.el = toastElement; - this._animateIn(); - this._setTimer(); - } - static get defaults() { - return _defaults$7; - } - static getInstance(el) { - return el['M_Toast']; - } - static _createContainer() { - const container = document.createElement('div'); - container.setAttribute('id', 'toast-container'); - // Add event handler - container.addEventListener('touchstart', Toast._onDragStart); - container.addEventListener('touchmove', Toast._onDragMove); - container.addEventListener('touchend', Toast._onDragEnd); - container.addEventListener('mousedown', Toast._onDragStart); - document.addEventListener('mousemove', Toast._onDragMove); - document.addEventListener('mouseup', Toast._onDragEnd); - document.body.appendChild(container); - Toast._container = container; - } - static _removeContainer() { - document.removeEventListener('mousemove', Toast._onDragMove); - document.removeEventListener('mouseup', Toast._onDragEnd); - if (Toast._container) { - Toast._container.remove(); - Toast._container = null; - } - } - static _onDragStart(e) { - if (e.target && e.target.closest('.toast')) { - const toastElem = e.target.closest('.toast'); - const toast = toastElem['M_Toast']; - toast.panning = true; - Toast._draggedToast = toast; - toast.el.classList.add('panning'); - toast.el.style.transition = ''; - toast.startingXPos = Toast._xPos(e); - toast.time = Date.now(); - toast.xPos = Toast._xPos(e); - } - } - static _onDragMove(e) { - if (!!Toast._draggedToast) { - e.preventDefault(); - const toast = Toast._draggedToast; - toast.deltaX = Math.abs(toast.xPos - Toast._xPos(e)); - toast.xPos = Toast._xPos(e); - toast.velocityX = toast.deltaX / (Date.now() - toast.time); - toast.time = Date.now(); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - toast.el.style.transform = `translateX(${totalDeltaX}px)`; - toast.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString(); - } - } - static _onDragEnd() { - if (!!Toast._draggedToast) { - const toast = Toast._draggedToast; - toast.panning = false; - toast.el.classList.remove('panning'); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || toast.velocityX > 1; - // Remove toast - if (shouldBeDismissed) { - toast.wasSwiped = true; - toast.dismiss(); - // Animate toast back to original position - } - else { - toast.el.style.transition = 'transform .2s, opacity .2s'; - toast.el.style.transform = ''; - toast.el.style.opacity = ''; - } - Toast._draggedToast = null; - } - } - static _xPos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return e.targetTouches[0].clientX; - } - // mouse event - return e.clientX; - } - /** - * dismiss all toasts. - */ - static dismissAll() { - for (const toastIndex in Toast._toasts) { - Toast._toasts[toastIndex].dismiss(); - } - } - _createToast() { - let toast = this.options.toastId - ? document.getElementById(this.options.toastId) - : document.createElement('div'); - if (toast instanceof HTMLTemplateElement) { - const node = toast.content.cloneNode(true); - toast = node.firstElementChild; - } - toast.classList.add('toast'); - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - toast.setAttribute('aria-atomic', 'true'); - // Add custom classes onto toast - if (this.options.classes.length > 0) { - toast.classList.add(...this.options.classes.split(' ')); - } - if (this.message) - toast.innerText = this.message; - if (Toast._container) { - Toast._container.appendChild(toast); - } - return toast; - } - _animateIn() { - // Animate toast in - this.el.style.display = ''; - this.el.style.opacity = '0'; - // easeOutCubic - this.el.style.transition = ` - top ${this.options.inDuration}ms ease, - opacity ${this.options.inDuration}ms ease - `; - setTimeout(() => { - this.el.style.top = '0'; - this.el.style.opacity = '1'; - }, 1); - } - /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. - */ - _setTimer() { - if (this.timeRemaining !== Infinity) { - this.counterInterval = setInterval(() => { - // If toast is not being dragged, decrease its time remaining - if (!this.panning) { - this.timeRemaining -= 20; - } - // Animate toast out - if (this.timeRemaining <= 0) { - this.dismiss(); - } - }, 20); - } - } - /** - * Dismiss toast with animation. - */ - dismiss() { - clearInterval(this.counterInterval); - const activationDistance = this.el.offsetWidth * this.options.activationPercent; - if (this.wasSwiped) { - this.el.style.transition = 'transform .05s, opacity .05s'; - this.el.style.transform = `translateX(${activationDistance}px)`; - this.el.style.opacity = '0'; - } - // easeOutExpo - this.el.style.transition = ` - margin ${this.options.outDuration}ms ease, - opacity ${this.options.outDuration}ms ease`; - setTimeout(() => { - this.el.style.opacity = '0'; - this.el.style.marginTop = '-40px'; - }, 1); - setTimeout(() => { - // Call the optional callback - if (typeof this.options.completeCallback === 'function') { - this.options.completeCallback(); - } - // Remove toast from DOM - if (this.el.id != this.options.toastId) { - this.el.remove(); - Toast._toasts.splice(Toast._toasts.indexOf(this), 1); - if (Toast._toasts.length === 0) { - Toast._removeContainer(); - } - } - }, this.options.outDuration); - } - static { - Toast._toasts = []; - Toast._container = null; - Toast._draggedToast = null; - } - } - - const _defaults$6 = { - exitDelay: 200, - enterDelay: 0, - text: '', - margin: 5, - inDuration: 250, - outDuration: 200, - position: 'bottom', - transitionMovement: 10, - opacity: 1 - }; - class Tooltip extends Component { - /** - * If tooltip is open. - */ - isOpen; - /** - * If tooltip is hovered. - */ - isHovered; - /** - * If tooltip is focused. - */ - isFocused; - tooltipEl; - _exitDelayTimeout; - _enterDelayTimeout; - xMovement; - yMovement; - constructor(el, options) { - super(el, options, Tooltip); - this.el['M_Tooltip'] = this; - this.options = { - ...Tooltip.defaults, - ...this._getAttributeOptions(), - ...options - }; - this.isOpen = false; - this.isHovered = false; - this.isFocused = false; - this._appendTooltipEl(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$6; - } - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tooltip); - } - static getInstance(el) { - return el['M_Tooltip']; - } - destroy() { - this.tooltipEl.remove(); - this._removeEventHandlers(); - this.el['M_Tooltip'] = undefined; - } - _appendTooltipEl() { - this.tooltipEl = document.createElement('div'); - this.tooltipEl.classList.add('material-tooltip'); - const tooltipContentEl = this.options.tooltipId - ? document.getElementById(this.options.tooltipId) - : document.createElement('div'); - this.tooltipEl.append(tooltipContentEl); - tooltipContentEl.style.display = ''; - tooltipContentEl.classList.add('tooltip-content'); - this._setTooltipContent(tooltipContentEl); - this.tooltipEl.appendChild(tooltipContentEl); - document.body.appendChild(this.tooltipEl); - } - _setTooltipContent(tooltipContentEl) { - if (this.options.tooltipId) - return; - tooltipContentEl.innerText = this.options.text; - } - _updateTooltipContent() { - this._setTooltipContent(this.tooltipEl.querySelector('.tooltip-content')); - } - _setupEventHandlers() { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.el.addEventListener('focus', this._handleFocus, true); - this.el.addEventListener('blur', this._handleBlur, true); - } - _removeEventHandlers() { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.el.removeEventListener('focus', this._handleFocus, true); - this.el.removeEventListener('blur', this._handleBlur, true); - } - /** - * Show tooltip. - */ - open = (isManual) => { - if (this.isOpen) - return; - isManual = isManual === undefined ? true : undefined; // Default value true - this.isOpen = true; - // Update tooltip content with HTML attribute options - this.options = { ...this.options, ...this._getAttributeOptions() }; - this._updateTooltipContent(); - this._setEnterDelayTimeout(isManual); - }; - /** - * Hide tooltip. - */ - close = () => { - if (!this.isOpen) - return; - this.isHovered = false; - this.isFocused = false; - this.isOpen = false; - this._setExitDelayTimeout(); - }; - _setExitDelayTimeout() { - clearTimeout(this._exitDelayTimeout); - this._exitDelayTimeout = setTimeout(() => { - if (this.isHovered || this.isFocused) - return; - this._animateOut(); - }, this.options.exitDelay); - } - _setEnterDelayTimeout(isManual) { - clearTimeout(this._enterDelayTimeout); - this._enterDelayTimeout = setTimeout(() => { - if (!this.isHovered && !this.isFocused && !isManual) - return; - this._animateIn(); - }, this.options.enterDelay); - } - _positionTooltip() { - const tooltip = this.tooltipEl; - const origin = this.el, originHeight = origin.offsetHeight, originWidth = origin.offsetWidth, tooltipHeight = tooltip.offsetHeight, tooltipWidth = tooltip.offsetWidth, margin = this.options.margin; - this.xMovement = 0; - this.yMovement = 0; - let targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(); - let targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (this.options.position === 'top') { - targetTop += -tooltipHeight - margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = -this.options.transitionMovement; - } - else if (this.options.position === 'right') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += originWidth + margin; - this.xMovement = this.options.transitionMovement; - } - else if (this.options.position === 'left') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += -tooltipWidth - margin; - this.xMovement = -this.options.transitionMovement; - } - else { - targetTop += originHeight + margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = this.options.transitionMovement; - } - const newCoordinates = this._repositionWithinScreen(targetLeft, targetTop, tooltipWidth, tooltipHeight); - tooltip.style.top = newCoordinates.y + 'px'; - tooltip.style.left = newCoordinates.x + 'px'; - } - _repositionWithinScreen(x, y, width, height) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - const bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - const offset = this.options.margin + this.options.transitionMovement; - const edges = Utils.checkWithinContainer(document.body, bounding, offset); - if (edges.left) { - newX = offset; - } - else if (edges.right) { - newX -= newX + width - window.innerWidth; - } - if (edges.top) { - newY = offset; - } - else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; - } - _animateIn() { - this._positionTooltip(); - this.tooltipEl.style.visibility = 'visible'; - const duration = this.options.inDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(${this.xMovement}px) translateY(${this.yMovement}px)`; - this.tooltipEl.style.opacity = (this.options.opacity || 1).toString(); - }, 1); - } - _animateOut() { - const duration = this.options.outDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(0px) translateY(0px)`; - this.tooltipEl.style.opacity = '0'; - }, 1); - /* - anim.remove(this.tooltipEl); - anim({ - targets: this.tooltipEl, - opacity: 0, - translateX: 0, - translateY: 0, - duration: this.options.outDuration, - easing: 'easeOutCubic' - }); - */ - } - _handleMouseEnter = () => { - this.isHovered = true; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.open(false); - }; - _handleMouseLeave = () => { - this.isHovered = false; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.close(); - }; - _handleFocus = () => { - if (Utils.tabPressed) { - this.isFocused = true; - this.open(false); - } - }; - _handleBlur = () => { - this.isFocused = false; - this.close(); - }; - _getAttributeOptions() { - const attributeOptions = {}; - const tooltipTextOption = this.el.getAttribute('data-tooltip'); - const tooltipId = this.el.getAttribute('data-tooltip-id'); - const positionOption = this.el.getAttribute('data-position'); - if (tooltipTextOption) { - attributeOptions.text = tooltipTextOption; - } - if (positionOption) { - attributeOptions.position = positionOption; - } - if (tooltipId) { - attributeOptions.tooltipId = tooltipId; - } - return attributeOptions; - } - } - - const _defaults$5 = {}; - // TODO: !!!!! - class Range extends Component { - _mousedown; - value; - thumb; - constructor(el, options) { - super(el, options, Range); - this.el['M_Range'] = this; - this.options = { - ...Range.defaults, - ...options - }; - this._mousedown = false; - this._setupThumb(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$5; - } - /** - * Initializes instances of Range. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Range); - } - static getInstance(el) { - return el['M_Range']; - } - destroy() { - this._removeEventHandlers(); - this._removeThumb(); - this.el['M_Range'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('change', this._handleRangeChange); - this.el.addEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.addEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.addEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.addEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.addEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _removeEventHandlers() { - this.el.removeEventListener('change', this._handleRangeChange); - this.el.removeEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.removeEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.removeEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _handleRangeChange = () => { - this.value.innerHTML = this.el.value; - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - }; - _handleRangeMousedownTouchstart = (e) => { - // Set indicator value - this.value.innerHTML = this.el.value; - this._mousedown = true; - this.el.classList.add('active'); - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - if (e.type !== 'input') { - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - } - }; - _handleRangeInputMousemoveTouchmove = () => { - if (this._mousedown) { - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - this.value.innerHTML = this.el.value; - } - }; - _handleRangeMouseupTouchend = () => { - this._mousedown = false; - this.el.classList.remove('active'); - }; - _handleRangeBlurMouseoutTouchleave = () => { - if (!this._mousedown) { - const paddingLeft = parseInt(getComputedStyle(this.el).paddingLeft); - const marginLeftText = 7 + paddingLeft + 'px'; - if (this.thumb.classList.contains('active')) { - const duration = 100; - // from - this.thumb.style.transition = 'none'; - setTimeout(() => { - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '0'; - this.thumb.style.width = '0'; - this.thumb.style.top = '0'; - this.thumb.style.marginLeft = marginLeftText; - }, 1); - } - this.thumb.classList.remove('active'); - } - }; - _setupThumb() { - this.thumb = document.createElement('span'); - this.value = document.createElement('span'); - this.thumb.classList.add('thumb'); - this.value.classList.add('value'); - this.thumb.append(this.value); - this.el.after(this.thumb); - } - _removeThumb() { - this.thumb.remove(); - } - _showRangeBubble() { - const paddingLeft = parseInt(getComputedStyle(this.thumb.parentElement).paddingLeft); - const marginLeftText = -7 + paddingLeft + 'px'; // TODO: fix magic number? - const duration = 300; - // easeOutQuint - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '30px'; - this.thumb.style.width = '30px'; - this.thumb.style.top = '-30px'; - this.thumb.style.marginLeft = marginLeftText; - } - _calcRangeOffset() { - const width = this.el.getBoundingClientRect().width - 15; - const max = parseFloat(this.el.getAttribute('max')) || 100; // Range default max - const min = parseFloat(this.el.getAttribute('min')) || 0; // Range default min - const percent = (parseFloat(this.el.value) - min) / (max - min); - return percent * width; - } - /** - * Initializes every range input in the current document. - */ - static Init() { - if (typeof document !== 'undefined') - Range.init(document?.querySelectorAll('input[type=range]'), {}); - } - } - - const _defaults$4 = { - onOpen: null, - onClose: null - }; - class TapTarget extends Component { - /** - * If the tap target is open. - */ - isOpen; - static _taptargets; - wrapper; - // private _origin: HTMLElement; - originEl; - waveEl; - contentEl; - constructor(el, options) { - super(el, options, TapTarget); - this.el['M_TapTarget'] = this; - this.options = { - ...TapTarget.defaults, - ...options - }; - this.isOpen = false; - // setup - this.originEl = document.querySelector(`#${el.dataset.target}`); - this.originEl.tabIndex = 0; - this._setup(); - this._calculatePositioning(); - this._setupEventHandlers(); - TapTarget._taptargets.push(this); - } - static get defaults() { - return _defaults$4; - } - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, TapTarget); - } - static getInstance(el) { - return el['M_TapTarget']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_TapTarget'] = undefined; - const index = TapTarget._taptargets.indexOf(this); - if (index >= 0) { - TapTarget._taptargets.splice(index, 1); - } - } - _setupEventHandlers() { - this.originEl.addEventListener('click', this._handleTargetToggle); - this.originEl.addEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.addEventListener('click', this._handleOriginClick); - // Resize - window.addEventListener('resize', this._handleThrottledResize); - } - _removeEventHandlers() { - this.originEl.removeEventListener('click', this._handleTargetToggle); - this.originEl.removeEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.removeEventListener('click', this._handleOriginClick); - window.removeEventListener('resize', this._handleThrottledResize); - } - _handleThrottledResize = () => Utils.throttle(this._handleResize, 200).bind(this); - _handleKeyboardInteraction = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleTargetToggle(); - } - }; - _handleTargetToggle = () => { - if (!this.isOpen) - this.open(); - else - this.close(); - }; - /*_handleOriginClick = () => { - this.close(); - }*/ - _handleResize = () => { - this._calculatePositioning(); - }; - _handleDocumentClick = (e) => { - if (e.target.closest(`#${this.el.dataset.target}`) !== this.originEl && - !e.target.closest('.tap-target-wrapper')) { - this.close(); - // e.preventDefault(); - // e.stopPropagation(); - } - }; - _setup() { - // Creating tap target - this.wrapper = this.el.parentElement; - this.waveEl = this.wrapper.querySelector('.tap-target-wave'); - this.el.parentElement.ariaExpanded = 'false'; - this.originEl.style.zIndex = '1002'; - // this.originEl = this.wrapper.querySelector('.tap-target-origin'); - this.contentEl = this.el.querySelector('.tap-target-content'); - // Creating wrapper - if (!this.wrapper.classList.contains('.tap-target-wrapper')) { - this.wrapper = document.createElement('div'); - this.wrapper.classList.add('tap-target-wrapper'); - this.el.before(this.wrapper); - this.wrapper.append(this.el); - } - // Creating content - if (!this.contentEl) { - this.contentEl = document.createElement('div'); - this.contentEl.classList.add('tap-target-content'); - this.el.append(this.contentEl); - } - // Creating foreground wave - if (!this.waveEl) { - this.waveEl = document.createElement('div'); - this.waveEl.classList.add('tap-target-wave'); - // Creating origin - /*if (!this.originEl) { - this.originEl = this._origin.cloneNode(true); // .clone(true, true); - this.originEl.classList.add('tap-target-origin'); - this.originEl.removeAttribute('id'); - this.originEl.removeAttribute('style'); - this.waveEl.append(this.originEl); - }*/ - this.wrapper.append(this.waveEl); - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - _calculatePositioning() { - // Element or parent is fixed position? - let isFixed = getComputedStyle(this.originEl).position === 'fixed'; - if (!isFixed) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let currentElem = this.originEl; - const parents = []; - while ((currentElem = currentElem.parentNode) && currentElem !== document) - parents.push(currentElem); - for (let i = 0; i < parents.length; i++) { - isFixed = getComputedStyle(parents[i]).position === 'fixed'; - if (isFixed) - break; - } - } - // Calculating origin - const originWidth = this.originEl.offsetWidth; - const originHeight = this.originEl.offsetHeight; - const originTop = isFixed - ? this._offset(this.originEl).top - Utils.getDocumentScrollTop() - : this._offset(this.originEl).top; - const originLeft = isFixed - ? this._offset(this.originEl).left - Utils.getDocumentScrollLeft() - : this._offset(this.originEl).left; - // Calculating screen - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - const scrollBarWidth = windowWidth - document.documentElement.clientWidth; - const centerX = windowWidth / 2; - const centerY = windowHeight / 2; - const isLeft = originLeft <= centerX; - const isRight = originLeft > centerX; - const isTop = originTop <= centerY; - const isBottom = originTop > centerY; - const isCenterX = originLeft >= windowWidth * 0.25 && originLeft <= windowWidth * 0.75; - // Calculating tap target - const tapTargetWidth = this.el.offsetWidth; - const tapTargetHeight = this.el.offsetHeight; - const tapTargetTop = originTop + originHeight / 2 - tapTargetHeight / 2; - const tapTargetLeft = originLeft + originWidth / 2 - tapTargetWidth / 2; - const tapTargetPosition = isFixed ? 'fixed' : 'absolute'; - // Calculating content - const tapTargetTextWidth = isCenterX ? tapTargetWidth : tapTargetWidth / 2 + originWidth; - const tapTargetTextHeight = tapTargetHeight / 2; - const tapTargetTextTop = isTop ? tapTargetHeight / 2 : 0; - const tapTargetTextBottom = 0; - const tapTargetTextLeft = isLeft && !isCenterX ? tapTargetWidth / 2 - originWidth : 0; - const tapTargetTextRight = 0; - const tapTargetTextPadding = originWidth; - const tapTargetTextAlign = isBottom ? 'bottom' : 'top'; - // Calculating wave - const tapTargetWaveWidth = originWidth > originHeight ? originWidth * 2 : originWidth * 2; - const tapTargetWaveHeight = tapTargetWaveWidth; - const tapTargetWaveTop = tapTargetHeight / 2 - tapTargetWaveHeight / 2; - const tapTargetWaveLeft = tapTargetWidth / 2 - tapTargetWaveWidth / 2; - // Setting tap target - this.wrapper.style.top = isTop ? tapTargetTop + 'px' : ''; - this.wrapper.style.right = isRight - ? windowWidth - tapTargetLeft - tapTargetWidth - scrollBarWidth + 'px' - : ''; - this.wrapper.style.bottom = isBottom - ? windowHeight - tapTargetTop - tapTargetHeight + 'px' - : ''; - this.wrapper.style.left = isLeft ? tapTargetLeft + 'px' : ''; - this.wrapper.style.position = tapTargetPosition; - // Setting content - this.contentEl.style.width = tapTargetTextWidth + 'px'; - this.contentEl.style.height = tapTargetTextHeight + 'px'; - this.contentEl.style.top = tapTargetTextTop + 'px'; - this.contentEl.style.right = tapTargetTextRight + 'px'; - this.contentEl.style.bottom = tapTargetTextBottom + 'px'; - this.contentEl.style.left = tapTargetTextLeft + 'px'; - this.contentEl.style.padding = tapTargetTextPadding + 'px'; - this.contentEl.style.verticalAlign = tapTargetTextAlign; - // Setting wave - this.waveEl.style.top = tapTargetWaveTop + 'px'; - this.waveEl.style.left = tapTargetWaveLeft + 'px'; - this.waveEl.style.width = tapTargetWaveWidth + 'px'; - this.waveEl.style.height = tapTargetWaveHeight + 'px'; - } - /** - * Open Tap Target. - */ - open = () => { - if (this.isOpen) - return; - // onOpen callback - if (typeof this.options.onOpen === 'function') { - this.options.onOpen.call(this, this.originEl); - } - this.isOpen = true; - this.wrapper.classList.add('open'); - this.wrapper.ariaExpanded = 'true'; - document.body.addEventListener('click', this._handleDocumentClick, true); - document.body.addEventListener('keypress', this._handleDocumentClick, true); - document.body.addEventListener('touchend', this._handleDocumentClick); - }; - /** - * Close Tap Target. - */ - close = () => { - if (!this.isOpen) - return; - // onClose callback - if (typeof this.options.onClose === 'function') { - this.options.onClose.call(this, this.originEl); - } - this.isOpen = false; - this.wrapper.classList.remove('open'); - this.wrapper.ariaExpanded = 'false'; - document.body.removeEventListener('click', this._handleDocumentClick, true); - document.body.removeEventListener('keypress', this._handleDocumentClick, true); - document.body.removeEventListener('touchend', this._handleDocumentClick); - }; - static { - TapTarget._taptargets = []; - } - } - - const _defaults$3 = Object.freeze({}); - class CharacterCounter extends Component { - /** Stores the reference to the counter HTML element. */ - counterEl; - /** Specifies whether the input is valid or not. */ - isInvalid; - /** Specifies whether the input text has valid length or not. */ - isValidLength; - constructor(el, options) { - super(el, {}, CharacterCounter); - this.el['M_CharacterCounter'] = this; - this.options = { - ...CharacterCounter.defaults, - ...options - }; - this.isInvalid = false; - this.isValidLength = false; - this._setupCounter(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$3; - } - /** - * Initializes instances of CharacterCounter. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, CharacterCounter); - } - static getInstance(el) { - return el['M_CharacterCounter']; - } - destroy() { - this._removeEventHandlers(); - this.el['CharacterCounter'] = undefined; - this._removeCounter(); - } - _setupEventHandlers() { - this.el.addEventListener('focus', this.updateCounter, true); - this.el.addEventListener('input', this.updateCounter, true); - } - _removeEventHandlers() { - this.el.removeEventListener('focus', this.updateCounter, true); - this.el.removeEventListener('input', this.updateCounter, true); - } - _setupCounter() { - this.counterEl = document.createElement('span'); - this.counterEl.classList.add('character-counter'); - this.counterEl.style.float = 'right'; - this.counterEl.style.fontSize = '12px'; - this.counterEl.style.height = '1'; - this.el.parentElement.appendChild(this.counterEl); - } - _removeCounter() { - this.counterEl.remove(); - } - updateCounter = () => { - const maxLength = parseInt(this.el.getAttribute('maxlength')), actualLength = this.el.value.length; - this.isValidLength = actualLength <= maxLength; - let counterString = actualLength.toString(); - if (maxLength) { - counterString += '/' + maxLength; - this._validateInput(); - } - this.counterEl.innerHTML = counterString; - }; - _validateInput() { - if (this.isValidLength && this.isInvalid) { - this.isInvalid = false; - this.el.classList.remove('invalid'); - } - else if (!this.isValidLength && !this.isInvalid) { - this.isInvalid = true; - this.el.classList.remove('valid'); - this.el.classList.add('invalid'); - } - } - } - - const _defaults$2 = { - responsiveThreshold: 0 // breakpoint for swipeable - }; - class Parallax extends Component { - _enabled; - _img; - static _parallaxes = []; - static _handleScrollThrottled; - static _handleWindowResizeThrottled; - constructor(el, options) { - super(el, options, Parallax); - this.el['M_Parallax'] = this; - this.options = { - ...Parallax.defaults, - ...options - }; - this._enabled = window.innerWidth > this.options.responsiveThreshold; - this._img = this.el.querySelector('img'); - this._updateParallax(); - this._setupEventHandlers(); - this._setupStyles(); - Parallax._parallaxes.push(this); - } - static get defaults() { - return _defaults$2; - } - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Parallax); - } - static getInstance(el) { - return el['M_Parallax']; - } - destroy() { - Parallax._parallaxes.splice(Parallax._parallaxes.indexOf(this), 1); - this._img.style.transform = ''; - this._removeEventHandlers(); - this.el['M_Parallax'] = undefined; - } - static _handleScroll() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._updateParallax.call(parallaxInstance); - } - } - static _handleWindowResize() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._enabled = window.innerWidth > parallaxInstance.options.responsiveThreshold; - } - } - _setupEventHandlers() { - this._img.addEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - if (!Parallax._handleScrollThrottled) { - Parallax._handleScrollThrottled = Utils.throttle(Parallax._handleScroll, 5); - } - if (!Parallax._handleWindowResizeThrottled) { - Parallax._handleWindowResizeThrottled = Utils.throttle(Parallax._handleWindowResize, 5); - } - window.addEventListener('scroll', Parallax._handleScrollThrottled); - window.addEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _removeEventHandlers() { - this._img.removeEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - window.removeEventListener('scroll', Parallax._handleScrollThrottled); - window.removeEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _setupStyles() { - this._img.style.opacity = '1'; - } - _handleImageLoad = () => { - this._updateParallax(); - }; - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateParallax() { - const containerHeight = this.el.getBoundingClientRect().height > 0 ? this.el.parentElement.offsetHeight : 500; - const imgHeight = this._img.offsetHeight; - const parallaxDist = imgHeight - containerHeight; - const bottom = this._offset(this.el).top + containerHeight; - const top = this._offset(this.el).top; - const scrollTop = Utils.getDocumentScrollTop(); - const windowHeight = window.innerHeight; - const windowBottom = scrollTop + windowHeight; - const percentScrolled = (windowBottom - top) / (containerHeight + windowHeight); - const parallax = parallaxDist * percentScrolled; - if (!this._enabled) { - this._img.style.transform = ''; - } - else if (bottom > scrollTop && top < scrollTop + windowHeight) { - this._img.style.transform = `translate3D(-50%, ${parallax}px, 0)`; - } - } - } - - const _defaults$1 = { - top: 0, - bottom: Infinity, - offset: 0, - onPositionChange: null - }; - class Pushpin extends Component { - static _pushpins; - originalOffset; - constructor(el, options) { - super(el, options, Pushpin); - this.el['M_Pushpin'] = this; - this.options = { - ...Pushpin.defaults, - ...options - }; - this.originalOffset = this.el.offsetTop; - Pushpin._pushpins.push(this); - this._setupEventHandlers(); - this._updatePosition(); - } - static get defaults() { - return _defaults$1; - } - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Pushpin); - } - static getInstance(el) { - return el['M_Pushpin']; - } - destroy() { - this.el.style.top = null; - this._removePinClasses(); - // Remove pushpin Inst - const index = Pushpin._pushpins.indexOf(this); - Pushpin._pushpins.splice(index, 1); - if (Pushpin._pushpins.length === 0) { - this._removeEventHandlers(); - } - this.el['M_Pushpin'] = undefined; - } - static _updateElements() { - for (const elIndex in Pushpin._pushpins) { - const pInstance = Pushpin._pushpins[elIndex]; - pInstance._updatePosition(); - } - } - _setupEventHandlers() { - document.addEventListener('scroll', Pushpin._updateElements); - } - _removeEventHandlers() { - document.removeEventListener('scroll', Pushpin._updateElements); - } - _updatePosition() { - const scrolled = Utils.getDocumentScrollTop() + this.options.offset; - if (this.options.top <= scrolled && - this.options.bottom >= scrolled && - !this.el.classList.contains('pinned')) { - this._removePinClasses(); - this.el.style.top = `${this.options.offset}px`; - this.el.classList.add('pinned'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pinned'); - } - } - // Add pin-top (when scrolled position is above top) - if (scrolled < this.options.top && !this.el.classList.contains('pin-top')) { - this._removePinClasses(); - this.el.style.top = '0'; - this.el.classList.add('pin-top'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-top'); - } - } - // Add pin-bottom (when scrolled position is below bottom) - if (scrolled > this.options.bottom && !this.el.classList.contains('pin-bottom')) { - this._removePinClasses(); - this.el.classList.add('pin-bottom'); - this.el.style.top = `${this.options.bottom - this.originalOffset}px`; - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-bottom'); - } - } - } - _removePinClasses() { - // IE 11 bug (can't remove multiple classes in one line) - this.el.classList.remove('pin-top'); - this.el.classList.remove('pinned'); - this.el.classList.remove('pin-bottom'); - } - static { - Pushpin._pushpins = []; - } - } - - const _defaults = { - throttle: 100, - scrollOffset: 200, // offset - 200 allows elements near bottom of page to scroll - activeClass: 'active', - getActiveElement: (id) => { - return 'a[href="#' + id + '"]'; - }, - keepTopElementActive: false, - animationDuration: null - }; - class ScrollSpy extends Component { - static _elements; - static _count; - static _increment; - static _elementsInView; - static _visibleElements; - static _ticks; - static _keptTopActiveElement = null; - tickId; - id; - constructor(el, options) { - super(el, options, ScrollSpy); - this.el['M_ScrollSpy'] = this; - this.options = { - ...ScrollSpy.defaults, - ...options - }; - ScrollSpy._elements.push(this); - ScrollSpy._count++; - ScrollSpy._increment++; - this.tickId = -1; - this.id = ScrollSpy._increment.toString(); - this._setupEventHandlers(); - this._handleWindowScroll(); - } - static get defaults() { - return _defaults; - } - /** - * Initializes instances of ScrollSpy. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, ScrollSpy); - } - static getInstance(el) { - return el['M_ScrollSpy']; - } - destroy() { - ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1); - ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1); - ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.el), 1); - ScrollSpy._count--; - this._removeEventHandlers(); - const actElem = document.querySelector(this.options.getActiveElement(this.el.id)); - actElem.classList.remove(this.options.activeClass); - this.el['M_ScrollSpy'] = undefined; - } - _setupEventHandlers() { - if (ScrollSpy._count === 1) { - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleThrottledResize); - document.body.addEventListener('click', this._handleTriggerClick); - } - } - _removeEventHandlers() { - if (ScrollSpy._count === 0) { - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleThrottledResize); - document.body.removeEventListener('click', this._handleTriggerClick); - } - } - _handleThrottledResize = () => Utils.throttle(this._handleWindowScroll, 200).bind(this); - _handleTriggerClick = (e) => { - const trigger = e.target; - for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) { - const scrollspy = ScrollSpy._elements[i]; - const x = document.querySelector('a[href="#' + scrollspy.el.id + '"]'); - if (trigger === x) { - e.preventDefault(); - if (scrollspy.el['M_ScrollSpy'].options.animationDuration) { - ScrollSpy._smoothScrollIntoView(scrollspy.el, scrollspy.el['M_ScrollSpy'].options.animationDuration); - } - else { - scrollspy.el.scrollIntoView({ behavior: 'smooth' }); - } - break; - } - } - }; - _handleWindowScroll = () => { - // unique tick id - ScrollSpy._ticks++; - // viewport rectangle - const top = Utils.getDocumentScrollTop(), left = Utils.getDocumentScrollLeft(), right = left + window.innerWidth, bottom = top + window.innerHeight; - // determine which elements are in view - const intersections = ScrollSpy._findElements(top, right, bottom, left); - for (let i = 0; i < intersections.length; i++) { - const scrollspy = intersections[i]; - const lastTick = scrollspy.tickId; - if (lastTick < 0) { - // entered into view - scrollspy._enter(); - } - // update tick id - scrollspy.tickId = ScrollSpy._ticks; - } - for (let i = 0; i < ScrollSpy._elementsInView.length; i++) { - const scrollspy = ScrollSpy._elementsInView[i]; - const lastTick = scrollspy.tickId; - if (lastTick >= 0 && lastTick !== ScrollSpy._ticks) { - // exited from view - scrollspy._exit(); - scrollspy.tickId = -1; - } - } - // remember elements in view for next tick - ScrollSpy._elementsInView = intersections; - if (ScrollSpy._elements.length) { - const options = ScrollSpy._elements[0].el['M_ScrollSpy'].options; - if (options.keepTopElementActive && ScrollSpy._visibleElements.length === 0) { - this._resetKeptTopActiveElementIfNeeded(); - const topElements = ScrollSpy._elements - .filter((value) => ScrollSpy._getDistanceToViewport(value.el) <= 0) - .sort((a, b) => { - const distanceA = ScrollSpy._getDistanceToViewport(a.el); - const distanceB = ScrollSpy._getDistanceToViewport(b.el); - if (distanceA < distanceB) - return -1; - if (distanceA > distanceB) - return 1; - return 0; - }); - const nearestTopElement = topElements.length - ? topElements[topElements.length - 1] - : ScrollSpy._elements[0]; - const actElem = document.querySelector(options.getActiveElement(nearestTopElement.el.id)); - actElem?.classList.add(options.activeClass); - ScrollSpy._keptTopActiveElement = actElem; - } - } - }; - static _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - static _findElements(top, right, bottom, left) { - const hits = []; - for (let i = 0; i < ScrollSpy._elements.length; i++) { - const scrollspy = ScrollSpy._elements[i]; - const currTop = top + scrollspy.options.scrollOffset || 200; - if (scrollspy.el.getBoundingClientRect().height > 0) { - const elTop = ScrollSpy._offset(scrollspy.el).top, elLeft = ScrollSpy._offset(scrollspy.el).left, elRight = elLeft + scrollspy.el.getBoundingClientRect().width, elBottom = elTop + scrollspy.el.getBoundingClientRect().height; - const isIntersect = !(elLeft > right || - elRight < left || - elTop > bottom || - elBottom < currTop); - if (isIntersect) { - hits.push(scrollspy); - } - } - } - return hits; - } - _enter() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - if (ScrollSpy._visibleElements[0]['M_ScrollSpy'] && - this.id < ScrollSpy._visibleElements[0]['M_ScrollSpy'].id) { - ScrollSpy._visibleElements.unshift(this.el); - } - else { - ScrollSpy._visibleElements.push(this.el); - } - } - else { - ScrollSpy._visibleElements.push(this.el); - } - this._resetKeptTopActiveElementIfNeeded(); - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - } - _exit() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((x) => x.id != this.el.id); - if (ScrollSpy._visibleElements[0]) { - // Check if empty - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - this._resetKeptTopActiveElementIfNeeded(); - } - } - } - _resetKeptTopActiveElementIfNeeded() { - if (ScrollSpy._keptTopActiveElement) { - ScrollSpy._keptTopActiveElement.classList.remove(this.options.activeClass); - ScrollSpy._keptTopActiveElement = null; - } - } - static _getDistanceToViewport(element) { - const rect = element.getBoundingClientRect(); - const distance = rect.top; - return distance; - } - static _smoothScrollIntoView(element, duration = 300) { - const targetPosition = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset); - const startPosition = window.scrollY || window.pageYOffset; - const distance = targetPosition - startPosition; - const startTime = performance.now(); - function scrollStep(currentTime) { - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - const scrollY = startPosition + distance * progress; - if (progress < 1) { - window.scrollTo(0, scrollY); - requestAnimationFrame(scrollStep); - } - else { - window.scrollTo(0, targetPosition); - } - } - requestAnimationFrame(scrollStep); - } - static { - ScrollSpy._elements = []; - ScrollSpy._elementsInView = []; - ScrollSpy._visibleElements = []; // Array. - ScrollSpy._count = 0; - ScrollSpy._increment = 0; - ScrollSpy._ticks = 0; - } - } - - // type RGBColor = { - // r: number; - // g: number; - // b: number; - // }; - - // type Position = { - // x: number; - // y: number; - // }; - - class Waves { - /** - * - * @param {HTMLElement} el - * @returns - */ - static #offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - - // https://phoenix-dx.com/css-techniques-for-material-ripple-effect/ - - /** - * - * @param {HTMLElement} targetElement - * @param position - * @param color - */ - static renderWaveEffect(targetElement, position = null, color = null) { - const isCentered = position === null; - const duration = 500; - let animationFrame, animationStart; - - const animationStep = (timestamp) => { - if (!animationStart) animationStart = timestamp; - const frame = timestamp - animationStart; - if (frame < duration) { - const easing = (frame / duration) * (2 - frame / duration); - const circle = isCentered - ? 'circle at 50% 50%' - : `circle at ${position.x}px ${position.y}px`; - const waveColor = `rgba(${color?.r || 0}, ${color?.g || 0}, ${color?.b || 0}, ${0.3 * (1 - easing)})`; - const stop = 90 * easing + '%'; - targetElement.style.backgroundImage = - 'radial-gradient(' + - circle + - ', ' + - waveColor + - ' ' + - stop + - ', transparent ' + - stop + - ')'; - animationFrame = window.requestAnimationFrame(animationStep); - } else { - targetElement.style.backgroundImage = 'none'; - window.cancelAnimationFrame(animationFrame); - } - }; - - animationFrame = window.requestAnimationFrame(animationStep); - } - - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.body.addEventListener('click', (e) => { - const trigger = e.target; - const el = trigger.closest('.waves-effect'); - if (el && el.contains(trigger)) { - const isCircular = el.classList.contains('waves-circle'); - const x = e.pageX - this.#offset(el).left; - const y = e.pageY - this.#offset(el).top; - let color = null; - if (el.classList.contains('waves-light')) color = { r: 255, g: 255, b: 255 }; - this.renderWaveEffect(el, isCircular ? null : { x, y }, color); - } - }); - }); - } - } - - /* eslint-disable @typescript-eslint/no-unused-vars */ - const version = '2.3.3'; - /** - * Automatically initialize components. - * @param context Root element to initialize. Defaults to `document.body`. - * @param options Options for each component. - */ - function AutoInit(context = document.body, options) { - const registry = { - Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'), - Cards: context.querySelectorAll('.cards:not(.no-autoinit)'), - Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'), - Chips: context.querySelectorAll('.chips:not(.no-autoinit)'), - Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'), - Datepicker: context.querySelectorAll('.datepicker:not(.no-autoinit)'), - Dropdown: context.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'), - Materialbox: context.querySelectorAll('.materialboxed:not(.no-autoinit)'), - Modal: context.querySelectorAll('.modal:not(.no-autoinit)'), - Parallax: context.querySelectorAll('.parallax:not(.no-autoinit)'), - Pushpin: context.querySelectorAll('.pushpin:not(.no-autoinit)'), - ScrollSpy: context.querySelectorAll('.scrollspy:not(.no-autoinit)'), - FormSelect: context.querySelectorAll('select:not(.no-autoinit)'), - Sidenav: context.querySelectorAll('.sidenav:not(.no-autoinit)'), - Tabs: context.querySelectorAll('.tabs:not(.no-autoinit)'), - TapTarget: context.querySelectorAll('.tap-target:not(.no-autoinit)'), - Timepicker: context.querySelectorAll('.timepicker:not(.no-autoinit)'), - Tooltip: context.querySelectorAll('.tooltipped:not(.no-autoinit)'), - FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)') - }; - Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {}); - Cards.init(registry.Cards, options?.Cards ?? {}); - Carousel.init(registry.Carousel, options?.Carousel ?? {}); - Chips.init(registry.Chips, options?.Chips ?? {}); - Collapsible.init(registry.Collapsible, options?.Collapsible ?? {}); - Datepicker.init(registry.Datepicker, options?.Datepicker ?? {}); - Dropdown.init(registry.Dropdown, options?.Dropdown ?? {}); - Materialbox.init(registry.Materialbox, options?.Materialbox ?? {}); - Modal.init(registry.Modal, options?.Modal ?? {}); - Parallax.init(registry.Parallax, options?.Parallax ?? {}); - Pushpin.init(registry.Pushpin, options?.Pushpin ?? {}); - ScrollSpy.init(registry.ScrollSpy, options?.ScrollSpy ?? {}); - FormSelect.init(registry.FormSelect, options?.FormSelect ?? {}); - Sidenav.init(registry.Sidenav, options?.Sidenav ?? {}); - Tabs.init(registry.Tabs, options?.Tabs ?? {}); - TapTarget.init(registry.TapTarget, options?.TapTarget ?? {}); - Timepicker.init(registry.Timepicker, options?.Timepicker ?? {}); - Tooltip.init(registry.Tooltip, options?.Tooltip ?? {}); - FloatingActionButton.init(registry.FloatingActionButton, options?.FloatingActionButton ?? {}); - } - // Init - if (typeof document !== 'undefined') { - document.addEventListener('keydown', Utils.docHandleKeydown, true); - document.addEventListener('keyup', Utils.docHandleKeyup, true); - document.addEventListener('focus', Utils.docHandleFocus, true); - document.addEventListener('blur', Utils.docHandleBlur, true); - } - Forms.Init(); - Chips.Init(); - Waves.Init(); - Range.Init(); - Cards.Init(); - - exports.AutoInit = AutoInit; - exports.Autocomplete = Autocomplete; - exports.Cards = Cards; - exports.Carousel = Carousel; - exports.CharacterCounter = CharacterCounter; - exports.Chips = Chips; - exports.Collapsible = Collapsible; - exports.Datepicker = Datepicker; - exports.Dropdown = Dropdown; - exports.FloatingActionButton = FloatingActionButton; - exports.FormSelect = FormSelect; - exports.Forms = Forms; - exports.Materialbox = Materialbox; - exports.Modal = Modal; - exports.Parallax = Parallax; - exports.Pushpin = Pushpin; - exports.Range = Range; - exports.ScrollSpy = ScrollSpy; - exports.Sidenav = Sidenav; - exports.Slider = Slider; - exports.Tabs = Tabs; - exports.TapTarget = TapTarget; - exports.Timepicker = Timepicker; - exports.Toast = Toast; - exports.Tooltip = Tooltip; - exports.Waves = Waves; - exports.version = version; - - return exports; - -})({}); diff --git a/dist/js/materialize.min.js b/dist/js/materialize.min.js deleted file mode 100644 index aede528a0c..0000000000 --- a/dist/js/materialize.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! -* Materialize v2.3.3 (https://materializeweb.com) -* Copyright 2014-2026 Materialize -* MIT License (https://raw.githubusercontent.com/materializecss/materialize/master/LICENSE) -*/ -var M=function(t){"use strict";class e{static tabPressed=!1;static keyDown=!1;static keys={TAB:["Tab"],ENTER:["Enter"],ESC:["Escape","Esc"],BACKSPACE:["Backspace"],ARROW_UP:["ArrowUp","Up"],ARROW_DOWN:["ArrowDown","Down"],ARROW_LEFT:["ArrowLeft","Left"],ARROW_RIGHT:["ArrowRight","Right"],DELETE:["Delete","Del"]};static docHandleKeydown(t){e.keyDown=!0,[...e.keys.TAB,...e.keys.ARROW_DOWN,...e.keys.ARROW_UP].includes(t.key)&&(e.tabPressed=!0)}static docHandleKeyup(t){e.keyDown=!1,[...e.keys.TAB,...e.keys.ARROW_DOWN,...e.keys.ARROW_UP].includes(t.key)&&(e.tabPressed=!1)}static docHandleFocus(t){e.keyDown&&document.body.classList.add("keyboard-focused")}static docHandleBlur(t){document.body.classList.remove("keyboard-focused")}static guid(){const t=()=>Math.floor(65536*(1+Math.random())).toString(16).substring(1);return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}static checkWithinContainer(t,e,i){const s={top:!1,right:!1,bottom:!1,left:!1},n=t.getBoundingClientRect(),o=t===document.body?Math.max(n.bottom,window.innerHeight):n.bottom,a=t.scrollLeft,l=t.scrollTop,r=e.left-a,h=e.top-l;return(rn.right-i||r+e.width>window.innerWidth-i)&&(s.right=!0),(ho-i||h+e.height>window.innerHeight-i)&&(s.bottom=!0),s}static checkPossibleAlignments(t,e,i,s){const n={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),l=Math.min(a.height,window.innerHeight),r=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,c=e.scrollTop,p=i.left-d,u=i.top-c,m=i.top+h.height-c;return n.spaceOnRight=o?window.innerWidth-(h.left+i.width):r-(p+i.width),n.spaceOnRight<0&&(n.left=!1),n.spaceOnLeft=o?h.right-i.width:p-i.width+h.width,n.spaceOnLeft<0&&(n.right=!1),n.spaceOnBottom=o?window.innerHeight-(h.top+i.height+s):l-(u+i.height+s),n.spaceOnBottom<0&&(n.top=!1),n.spaceOnTop=o?h.bottom-(i.height+s):m-(i.height-s),n.spaceOnTop<0&&(n.bottom=!1),n}static getIdFromTrigger(t){let e=t.dataset.target;return e||(e=t.getAttribute("href"),e?e.slice(1):"")}static getDocumentScrollTop(){return window.scrollY||document.documentElement.scrollTop||document.body.scrollTop||0}static getDocumentScrollLeft(){return window.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft||0}static throttle(t,e,i={}){let s,n,o,a=null,l=0;const r=()=>{l=!1===i.leading?0:(new Date).getTime(),a=null,o=t.apply(s,n),s=n=null};return(...s)=>{const n=(new Date).getTime();l||!1!==i.leading||(l=n);const h=e-(n-l);return h<=0?(clearTimeout(a),a=null,l=n,o=t.apply(this,s)):a||!1===i.trailing||(a=setTimeout(r,h)),o}}static createConfirmationContainer(t,e,i,s,n){const o=document.createElement("div");o.classList.add("confirmation-btns"),t.append(o),this.createButton(o,i,["btn-cancel"],!0,n),this.createButton(o,e,["btn-confirm"],!0,s)}static createButton(t,i,s=[],n=!0,o=null){s=s.concat(["btn","waves-effect","text"]);const a=document.createElement("button");a.className=s.join(" "),a.style.visibility=n?"visible":"hidden",a.type="button",a.tabIndex=n?0:-1,a.innerText=i,a.addEventListener("click",o),a.addEventListener("keypress",t=>{e.keys.ENTER.includes(t.key)&&o(t)}),t.append(a)}static _setAbsolutePosition(t,i,s,n,o,a="center"){const l=t.offsetHeight,r=t.offsetWidth,h=i.offsetHeight,d=i.offsetWidth;let c=0,p=0,u=t.getBoundingClientRect().top+e.getDocumentScrollTop(),m=t.getBoundingClientRect().left+e.getDocumentScrollLeft();"top"===s?(u+=-h-n,"center"===a&&(m+=r/2-d/2),p=-o):"right"===s?(u+=l/2-h/2,m=r+n,c=o):"left"===s?(u+=l/2-h/2,m=-d-n,c=-o):(u+=l+n,"center"===a&&(m+=r/2-d/2),p=o),"right"===a&&(m+=r-d-n);const v=e._repositionWithinScreen(m,u,d,h,n,o,a);return i.style.top=v.y+"px",i.style.left=v.x+"px",{x:c,y:p}}static _repositionWithinScreen(t,i,s,n,o,a,l){const r=e.getDocumentScrollLeft(),h=e.getDocumentScrollTop();let d=t-r,c=i-h;const p={left:d,top:c,width:s,height:n};let u;"left"===l||"center"==l?u=o+a:"right"===l&&(u=o-a);const m=e.checkWithinContainer(document.body,p,u);return m.left?d=u:m.right&&(d-=d+s-window.innerWidth),m.top?c=u:m.bottom&&(c-=c+n-window.innerHeight),{x:d+r,y:c+h}}}class i{el;options;constructor(t,e,i){t instanceof HTMLElement||console.error(Error(t+" is not an HTML Element"));const s=i.getInstance(t);s&&s.destroy(),this.el=t}static init(t,e,i){let s=null;if(t instanceof Element)s=new i(t,e);else if(t&&t.length){s=[];for(let n=0;n{t.preventDefault(),this.isOpen?this.close():this.open()};_handleMouseEnter=()=>{this.open()};_handleMouseLeave=t=>{const e=t.relatedTarget,i=!!e.closest(".dropdown-content");let s=!1;const n=e.closest(".dropdown-trigger");n&&n.M_Dropdown&&n.M_Dropdown.isOpen&&(s=!0),s||i||this.close()};_handleDocumentClick=t=>{const e=t.target;this.options.closeOnClick&&e.closest(".dropdown-content")&&!this.isTouchMoving?this.close():e.closest(".dropdown-content")||setTimeout(()=>{this.isOpen&&this.close()},0),this.isTouchMoving=!1};_handleTriggerKeydown=t=>{(e.keys.ARROW_DOWN.includes(t.key)||e.keys.ENTER.includes(t.key))&&!this.isOpen&&(t.preventDefault(),this.open())};_handleDocumentTouchmove=t=>{t.target.closest(".dropdown-content")&&(this.isTouchMoving=!0)};_handleDropdownClick=t=>{if("function"==typeof this.options.onItemClick){const e=t.target.closest("li");this.options.onItemClick.call(this,e)}};_handleDropdownKeydown=t=>{const i=e.keys.ARROW_DOWN.includes(t.key)||e.keys.ARROW_UP.includes(t.key);if(e.keys.TAB.includes(t.key))t.preventDefault(),this.close();else if(i&&this.isOpen){t.preventDefault();const i=e.keys.ARROW_DOWN.includes(t.key)?1:-1;let s=this.focusedIndex,n=!1;do{if(s+=i,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){n=!0;break}}while(s=0);n&&(this.focusedIndex>=0&&this.dropdownEl.children[this.focusedIndex].classList.remove("active"),this.focusedIndex=s,this._focusFocusedItem())}else if(e.keys.ENTER.includes(t.key)&&this.isOpen){const t=this.dropdownEl.children[this.focusedIndex],e=t?.querySelector("a, button");e?e.click():t&&t instanceof HTMLElement&&t.click()}else e.keys.ESC.includes(t.key)&&this.isOpen&&(t.preventDefault(),this.close());const s=t.key.toLowerCase(),n=/[a-zA-Z0-9-_]/.test(s),o=[...e.keys.ARROW_DOWN,...e.keys.ARROW_UP,...e.keys.ENTER,...e.keys.ESC,...e.keys.TAB];if(n&&!o.includes(t.key)){this.filterQuery.push(s);const t=this.filterQuery.join(""),e=Array.from(this.dropdownEl.querySelectorAll("li")).find(e=>0===e.innerText.toLowerCase().indexOf(t));e&&(this.focusedIndex=[...e.parentNode.children].indexOf(e),this._focusFocusedItem())}this.filterTimeout=setTimeout(this._resetFilterQuery,1e3)};_handleWindowResize=()=>{this.el.offsetParent&&this.recalculateDimensions()};_resetFilterQuery=()=>{this.filterQuery=[]};_resetDropdownStyles(){this.dropdownEl.style.display="",this._resetDropdownPositioningStyles(),this.dropdownEl.style.transform="",this.dropdownEl.style.opacity=""}_resetDropdownPositioningStyles(){this.dropdownEl.style.width="",this.dropdownEl.style.height="",this.dropdownEl.style.left="",this.dropdownEl.style.top="",this.dropdownEl.style.transformOrigin=""}_moveDropdownToElement(t=null){this.options.container?this.options.container.append(this.dropdownEl):t?t.contains(this.dropdownEl)||t.append(this.dropdownEl):this.el.after(this.dropdownEl)}_makeDropdownFocusable(){this.dropdownEl&&(this.dropdownEl.popover="",this.dropdownEl.tabIndex=0,Array.from(this.dropdownEl.children).forEach(t=>{t.getAttribute("tabindex")||t.setAttribute("tabindex","0")}))}_focusFocusedItem(){this.focusedIndex>=0&&this.focusedIndexh.spaceOnBottom?(d="bottom",n+=h.spaceOnTop,l-=this.options.coverTrigger?h.spaceOnTop-20:h.spaceOnTop-20+i.height):n+=h.spaceOnBottom)),!h[c]){const t="left"===c?"right":"left";h[t]?c=t:h.spaceOnLeft>h.spaceOnRight?(c="right",o+=h.spaceOnLeft,a-=h.spaceOnLeft):(c="left",o+=h.spaceOnRight)}return"bottom"===d&&(l=l-s.height+(this.options.coverTrigger?i.height:0)),"right"===c&&(a=a-s.width+i.width),{x:a,y:l,verticalAlignment:d,horizontalAlignment:c,height:n,width:o}}_animateIn(){const t=this.options.inDuration;this.dropdownEl.style.transition="none",this.dropdownEl.style.opacity="0",this.dropdownEl.style.transform="scale(0.3, 0.3)",setTimeout(()=>{this.dropdownEl.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`,this.dropdownEl.style.opacity="1",this.dropdownEl.style.transform="scale(1, 1)"},1),setTimeout(()=>{this.options.autoFocus&&this.dropdownEl.focus(),"function"==typeof this.options.onOpenEnd&&this.options.onOpenEnd.call(this,this.el)},t)}_animateOut(){const t=this.options.outDuration;this.dropdownEl.style.transition=`opacity ${t}ms ease, transform ${t}ms ease`,this.dropdownEl.style.opacity="0",this.dropdownEl.style.transform="scale(0.3, 0.3)",setTimeout(()=>{this._resetDropdownStyles(),"function"==typeof this.options.onCloseEnd&&this.options.onCloseEnd.call(this,this.el)},t)}_getClosestAncestor(t,e){let i=t.parentNode;for(;null!==i&&i!==document;){if(e(i))return i;i=i.parentElement}return null}_placeDropdown(){let t=this._getClosestAncestor(this.dropdownEl,t=>!["HTML","BODY"].includes(t.tagName)&&"visible"!==getComputedStyle(t).overflow);t||(t=this.dropdownEl.offsetParent?this.dropdownEl.offsetParent:this.dropdownEl.parentNode),"static"===getComputedStyle(t).position&&(t.style.position="relative");const e=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=e+"px";const i=this._getDropdownPosition(t);this.dropdownEl.style.left=i.x+"px",this.dropdownEl.style.top=i.y+"px",this.dropdownEl.style.height=i.height+"px",this.dropdownEl.style.width=i.width+"px",this.dropdownEl.style.transformOrigin=`${"left"===i.horizontalAlignment?"0":"100%"} ${"top"===i.verticalAlignment?"0":"100%"}`}open=()=>{this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),setTimeout(()=>this._setupTemporaryEventHandlers(),0),this.el.ariaExpanded="true")};close=()=>{this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus(),this.el.ariaExpanded="false")};recalculateDimensions=()=>{this.isOpen&&(this._resetDropdownPositioningStyles(),this._placeDropdown())}}const o={data:[],onAutocomplete:null,dropdownOptions:{autoFocus:!1,closeOnClick:!1,coverTrigger:!1},minLength:1,isMultiSelect:!1,onSearch:(t,e)=>{const i=t.toLocaleLowerCase();e.setMenuItems(e.data.filter(t=>t.id.toString().toLocaleLowerCase().includes(i)||t.text?.toLocaleLowerCase().includes(i)))},maxDropDownHeight:"300px",allowUnsafeHTML:!1,selected:[]};class a extends i{isOpen;count;activeIndex;oldVal;$active;_mousedown;container;dropdown;static _keydown;selectedValues;menuItems;data;constructor(t,e){super(t,e,a),this.el.M_Autocomplete=this,this.options={...a.defaults,...e},this.isOpen=!1,this.count=0,this.activeIndex=-1,this.oldVal="",this.selectedValues=this.selectedValues||this.options.selected.map(t=>({id:t}))||[],this.menuItems=this.options.data||[],this.data=this.options.data||[],this.$active=null,this._mousedown=!1,this._setupDropdown(),this._setupEventHandlers()}static get defaults(){return o}static init(t,e={}){return super.init(t,e,a)}static getInstance(t){return t.M_Autocomplete}destroy(){this._removeEventHandlers(),this._removeDropdown(),this.el.M_Autocomplete=void 0}_setupEventHandlers(){this.el.addEventListener("blur",this._handleInputBlur),this.el.addEventListener("keyup",this._handleInputKeyup),this.el.addEventListener("focus",this._handleInputFocus),this.el.addEventListener("keydown",this._handleInputKeydown),this.el.addEventListener("click",this._handleInputClick),this.container.addEventListener("mousedown",this._handleContainerMousedownAndTouchstart),this.container.addEventListener("mouseup",this._handleContainerMouseupAndTouchend),void 0!==window.ontouchstart&&(this.container.addEventListener("touchstart",this._handleContainerMousedownAndTouchstart),this.container.addEventListener("touchend",this._handleContainerMouseupAndTouchend))}_removeEventHandlers(){this.el.removeEventListener("blur",this._handleInputBlur),this.el.removeEventListener("keyup",this._handleInputKeyup),this.el.removeEventListener("focus",this._handleInputFocus),this.el.removeEventListener("keydown",this._handleInputKeydown),this.el.removeEventListener("click",this._handleInputClick),this.container.removeEventListener("mousedown",this._handleContainerMousedownAndTouchstart),this.container.removeEventListener("mouseup",this._handleContainerMouseupAndTouchend),void 0!==window.ontouchstart&&(this.container.removeEventListener("touchstart",this._handleContainerMousedownAndTouchstart),this.container.removeEventListener("touchend",this._handleContainerMouseupAndTouchend))}_setupDropdown(){this.container=document.createElement("ul"),this.container.style.maxHeight=this.options.maxDropDownHeight,this.container.id=`autocomplete-options-${e.guid()}`,this.container.classList.add("autocomplete-content","dropdown-content"),this.container.ariaExpanded="true",this.el.setAttribute("data-target",this.container.id),this.menuItems.forEach(t=>{const e=this._createDropdownItem(t);this.container.append(e)}),this.el.parentElement.appendChild(this.container);const t={...a.defaults.dropdownOptions,...this.options.dropdownOptions},i=t.onItemClick;t.onItemClick=t=>{if(!t)return;const e=t.getAttribute("data-id");this.selectOption(e),i&&"function"==typeof i&&i.call(this.dropdown,this.el)},this.dropdown=n.init(this.el,t);const s=this.el.parentElement.querySelector("label");if(s&&this.el.after(s),this.el.removeEventListener("click",this.dropdown._handleClick),!this.options.isMultiSelect&&0!==this.options.selected.length){const t=this.menuItems.filter(t=>t.id===this.selectedValues[0].id);this.el.value=t[0].text}this.el.value&&this.selectOption(this.el.value);const o=document.createElement("div");o.classList.add("status-info"),o.setAttribute("style","position: absolute;right:0;top:0;"),this.el.parentElement.appendChild(o),this._updateSelectedInfo()}_removeDropdown(){this.container.ariaExpanded="false",this.container.parentNode.removeChild(this.container)}_handleInputBlur=()=>{this._mousedown||(this.close(),this._resetAutocomplete())};_handleInputKeyup=t=>{"keyup"===t.type&&(a._keydown=!1),this.count=0;const i=this.el.value.toLocaleLowerCase();e.keys.ENTER.includes(t.key)||e.keys.ARROW_UP.includes(t.key)||e.keys.ARROW_DOWN.includes(t.key)||(this.oldVal!==i&&e.tabPressed&&this.open(),this._inputChangeDetection(i))};_handleInputFocus=()=>{this.count=0;const t=this.el.value.toLocaleLowerCase();this._inputChangeDetection(t)};_inputChangeDetection=t=>{this.oldVal!==t&&(this._setStatusLoading(),this.options.onSearch(this.el.value,this)),this.options.isMultiSelect||0!==this.el.value.length||(this.selectedValues=[],this._triggerChanged()),this.oldVal=t};_handleInputKeydown=t=>{a._keydown=!0;const i=this.container.querySelectorAll("li").length;if(e.keys.ENTER.includes(t.key)&&this.activeIndex>=0){const e=this.container.querySelectorAll("li")[this.activeIndex];return void(e&&(this.selectOption(e.getAttribute("data-id")),t.preventDefault()))}(e.keys.ARROW_UP.includes(t.key)||e.keys.ARROW_DOWN.includes(t.key))&&(t.preventDefault(),e.keys.ARROW_UP.includes(t.key)&&this.activeIndex>0&&this.activeIndex--,e.keys.ARROW_DOWN.includes(t.key)&&this.activeIndex=0&&(this.$active=this.container.querySelectorAll("li")[this.activeIndex],this.$active?.classList.add("active"),this.container.children[this.activeIndex].scrollIntoView({behavior:"smooth",block:"nearest",inline:"nearest"})))};_handleInputClick=()=>{this.open()};_handleContainerMousedownAndTouchstart=()=>{this._mousedown=!0};_handleContainerMouseupAndTouchend=()=>{this._mousedown=!1};_resetCurrentElementPosition(){this.activeIndex=-1,this.$active?.classList.remove("active")}_resetAutocomplete(){this.container.replaceChildren(),this._resetCurrentElementPosition(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}_highlightPartialText(t,e){const i=e.toLocaleLowerCase().indexOf(""+t.toLocaleLowerCase()),s=i+t.length-1;return-1==i||-1==s?[e,"",""]:[e.slice(0,i),e.slice(i,s+1),e.slice(s+1)]}_createDropdownItem(t){const e=document.createElement("li");if(e.setAttribute("data-id",t.id),e.setAttribute("style","display:grid; grid-auto-flow: column; user-select: none; align-items: center;"),e.tabIndex=0,this.options.isMultiSelect&&(e.innerHTML=`\n
\n e.id===t.id)?' checked="checked"':""}>\n
`),t.image){const i=document.createElement("img");i.classList.add("circle"),i.src=t.image,e.appendChild(i)}const i=this.el.value.toLocaleLowerCase(),s=this._highlightPartialText(i,(t.text||t.id).toString()),n=document.createElement("div");if(n.setAttribute("style","line-height:1.2;font-weight:500;"),this.options.allowUnsafeHTML)n.innerHTML=s[0]+''+s[1]+""+s[2];else if(n.appendChild(document.createTextNode(s[0])),s[1]){const t=document.createElement("span");t.textContent=s[1],t.classList.add("highlight"),n.appendChild(t),n.appendChild(document.createTextNode(s[2]))}const o=document.createElement("div");if(o.classList.add("item-text"),o.setAttribute("style","padding:5px;overflow:hidden;"),e.appendChild(o),e.querySelector(".item-text").appendChild(n),"string"==typeof t.description||"number"==typeof t.description&&!isNaN(t.description)){const i=document.createElement("small");i.setAttribute("style","line-height:1.3;color:grey;white-space:nowrap;text-overflow:ellipsis;display:block;width:90%;overflow:hidden;"),i.innerText=t.description,e.querySelector(".item-text").appendChild(i)}return e.style.gridTemplateColumns=(()=>this.options.isMultiSelect?t.image?"40px min-content auto":"40px auto":t.image?"min-content auto":"auto")(),e}_renderDropdown(){this._resetAutocomplete(),0===this.menuItems.length&&(this.menuItems=this.selectedValues);for(let t=0;t\n \n \n \n '}_updateSelectedInfo(){const t=this.el.parentElement.querySelector(".status-info");t&&(this.options.isMultiSelect?t.innerHTML=this.selectedValues.length.toString():t.innerHTML="")}_refreshInputText(){if(1===this.selectedValues.length){const t=this.selectedValues[0];this.el.value=t.text||t.id}}_triggerChanged(){this.el.dispatchEvent(new Event("change")),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,this.selectedValues)}open=()=>{const t=this.el.value.toLocaleLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown()),this.dropdown.isOpen?this.dropdown.recalculateDimensions():setTimeout(()=>{this.dropdown.open()},0)};close=()=>{this.dropdown.close()};setMenuItems(t,e=null,i=!0,s=!1){this.menuItems=t,this.options.data=t,s&&(this.data=t),e&&(this.selectedValues=this.menuItems.filter(t=>!(-1===e.indexOf(t.id)))),this.options.isMultiSelect&&this._renderDropdown(),i&&this.open(),this._updateSelectedInfo(),this._triggerChanged()}setValues(t){this.selectedValues=t,this._updateSelectedInfo(),this.options.isMultiSelect||this._refreshInputText(),this._triggerChanged()}selectOption(t){const e=this.menuItems.find(e=>e.id==t);e&&(this.options.isMultiSelect?(this.selectedValues.filter(t=>t.id===e.id).length>=1?this.selectedValues=this.selectedValues.filter(t=>t.id!==e.id):this.selectedValues.push(e),this._renderDropdown(),this.el.focus()):(this.selectedValues=[e],this._refreshInputText(),this._resetAutocomplete(),this.close()),this._updateSelectedInfo(),this._triggerChanged())}selectOptions(t){const e=this.menuItems.filter(e=>!(-1===t.indexOf(e.id)));e&&(this.selectedValues=e,this._renderDropdown())}}const l={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};class r extends i{#t;#e;#i;#s;isOpen;offsetY;offsetX;btnBottom;btnLeft;btnWidth;constructor(t,e){super(t,e,r),this.el.M_FloatingActionButton=this,this.options={...r.defaults,...e},this.isOpen=!1,this.#t=this.el.querySelector("a"),this.#e=this.el.querySelector("ul"),this.#i=Array.from(this.el.querySelectorAll("ul .btn-floating")),this.#s=this.#i.reverse(),this.offsetY=0,this.offsetX=0,this.el.classList.add(`direction-${this.options.direction}`),this.#t.tabIndex=0,this.#e.ariaExpanded="false","top"===this.options.direction?this.offsetY=40:"right"===this.options.direction?this.offsetX=-40:"bottom"===this.options.direction?this.offsetY=-40:this.offsetX=40,this.#n()}static get defaults(){return l}static init(t,e={}){return super.init(t,e,r)}static getInstance(t){return t.M_FloatingActionButton}destroy(){this.#o(),this.el.M_FloatingActionButton=void 0}#n(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this.open),this.el.addEventListener("mouseleave",this.close)):this.el.addEventListener("click",this.#a),this.el.addEventListener("keypress",this.#l)}#o(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this.open),this.el.removeEventListener("mouseleave",this.close)):this.el.removeEventListener("click",this.#a),this.el.removeEventListener("keypress",this.#l)}#a=()=>{this.#r()};#l=t=>{e.keys.ENTER.includes(t.key)&&this.#r()};#r=()=>{this.isOpen?this.close():this.open()};#h=t=>{t.target!==this.#e&&this.close()};open=()=>{this.isOpen||(this.options.toolbarEnabled?this.#d():this.#c(),this.isOpen=!0)};close=()=>{this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this.close,!0),document.body.removeEventListener("click",this.#h,!0)):this.#p(),this.isOpen=!1)};#c(){this.el.classList.add("active"),this.#e.ariaExpanded="true";this.#s.forEach((t,e)=>{const i=40*e;t.style.transition="none",t.style.opacity="0",t.style.transform=`translate(${this.offsetX}px, ${this.offsetY}px) scale(0.4)`,setTimeout(()=>{t.style.opacity="0.4",setTimeout(()=>{t.style.transition="opacity 275ms ease, transform 275ms ease",t.style.opacity="1",t.style.transform="translate(0, 0) scale(1)",t.tabIndex=0},1)},i)})}#p(){setTimeout(()=>{this.el.classList.remove("active"),this.#e.ariaExpanded="false"},175),this.#s.forEach(t=>{t.style.transition="opacity 175ms ease, transform 175ms ease",t.style.opacity="0",t.style.transform=`translate(${this.offsetX}px, ${this.offsetY}px) scale(0.4)`,t.tabIndex=-1})}#d(){const t=window.innerWidth,e=window.innerHeight,i=this.el.getBoundingClientRect(),s=document.createElement("div"),n=t/s[0].clientWidth,o=getComputedStyle(this.#t).backgroundColor;s.classList.add("fab-backdrop"),s.style.backgroundColor=o,this.#t.append(s),this.offsetX=i.left-t/2+i.width/2,this.offsetY=e-i.bottom,this.btnBottom=i.bottom,this.btnLeft=i.left,this.btnWidth=i.width,this.el.classList.add("active"),this.el.style.textAlign="center",this.el.style.width="100%",this.el.style.bottom="0",this.el.style.left="0",this.el.style.transform="translateX("+this.offsetX+"px)",this.el.style.transition="none",this.#e.ariaExpanded="true",this.#t.style.transform=`translateY(${this.offsetY}px`,this.#t.style.transition="none",setTimeout(()=>{this.el.style.transform="",this.el.style.transition="transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s",this.#t.style.overflow="visible",this.#t.style.transform="",this.#t.style.transition="transform .2s",setTimeout(()=>{this.el.style.overflow="hidden",this.el.style.backgroundColor=o,s.style.transform="scale("+n+")",s.style.transition="transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)",this.#e.querySelectorAll("li > a").forEach(t=>{t.style.opacity="1",t.tabIndex=0}),window.addEventListener("scroll",this.close,!0),document.body.addEventListener("click",this.#h,!0)},100)},0)}}const h={onOpen:null,onClose:null,inDuration:225,outDuration:300};class d extends i{#u;#m;#v;#g;isOpen=!1;constructor(t,e){super(t,e,d),this.el.M_Cards=this,this.options={...d.defaults,...e},this.#v=[],this.#u=this.el.querySelector(".card-reveal"),this.#u&&(this.#m=getComputedStyle(this.el).overflow,this.#v=Array.from(this.el.querySelectorAll(".activator")),this.#v.forEach(t=>{t&&(t.tabIndex=0)}),this.#g=this.#u?.querySelector(".card-title"),this.#g&&(this.#g.tabIndex=-1),this.#u.ariaExpanded="false",this.#n())}static get defaults(){return h}static init(t,e){return super.init(t,e,d)}static getInstance(t){return t.M_Cards}destroy(){this.#o(),this.#v=[]}#n=()=>{this.#v.forEach(t=>{t.addEventListener("click",this.#_),t.addEventListener("keypress",this.#y)})};#o=()=>{this.#v.forEach(t=>{t.removeEventListener("click",this.#_),t.removeEventListener("keypress",this.#y)})};#_=()=>{this.#f()};#y=t=>{e.keys.ENTER.includes(t.key)&&this.#f()};#f=()=>{this.#v.forEach(t=>t.tabIndex=-1),this.open()};#E=()=>{this.#g.addEventListener("click",this.close),this.#g.addEventListener("keypress",this.#w)};#b=()=>{this.#g.addEventListener("click",this.close),this.#g.addEventListener("keypress",this.#w)};#w=t=>{e.keys.ENTER.includes(t.key)&&this.close()};open=()=>{this.isOpen||(this.isOpen=!0,this.el.style.overflow="hidden",this.#u.style.display="block",this.#u.ariaExpanded="true",this.#g.tabIndex=0,setTimeout(()=>{this.#u.style.transition=`transform ${this.options.outDuration}ms ease`,this.#u.style.transform="translateY(-100%)"},1),"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.#E())};close=()=>{this.isOpen&&(this.isOpen=!1,this.#u.style.transition=`transform ${this.options.inDuration}ms ease`,this.#u.style.transform="translateY(0)",setTimeout(()=>{this.#u.style.display="none",this.#u.ariaExpanded="false",this.#v.forEach(t=>t.tabIndex=0),this.#g.tabIndex=-1,this.el.style.overflow=this.#m},this.options.inDuration),"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.#b())};static Init(){"undefined"!=typeof document&&document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".card").forEach(t=>{t&&null==t.M_Card&&this.init(t)})})}}const c={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null};class p extends i{hasMultipleSlides;showIndicators;noWrap;pressed;dragged;offset;target;images;itemWidth;itemHeight;dim;_indicators;count;xform;verticalDragged;reference;referenceY;velocity;frame;timestamp;ticker;amplitude;center=0;imageHeight;scrollingTimeout;oneTimeCallback;constructor(t,e){super(t,e,p),this.el.M_Carousel=this,this.options={...p.defaults,...e},this.hasMultipleSlides=this.el.querySelectorAll(".carousel-item").length>1,this.showIndicators=this.options.indicators&&this.hasMultipleSlides,this.noWrap=this.options.noWrap||!this.hasMultipleSlides,this.pressed=!1,this.dragged=!1,this.offset=this.target=0,this.images=[],this.itemWidth=this.el.querySelector(".carousel-item").clientWidth,this.itemHeight=this.el.querySelector(".carousel-item").clientHeight,this.dim=2*this.itemWidth+this.options.padding||1,this.options.fullWidth&&(this.options.dist=0,this._setCarouselHeight(),this.showIndicators&&this.el.querySelector(".carousel-fixed-item")?.classList.add("with-indicators")),this._indicators=document.createElement("ul"),this._indicators.classList.add("indicators"),this.el.querySelectorAll(".carousel-item").forEach((t,e)=>{if(this.images.push(t),this.showIndicators){const t=document.createElement("li");t.classList.add("indicator-item"),t.tabIndex=0,0===e&&t.classList.add("active"),this._indicators.appendChild(t)}}),this.showIndicators&&this.el.appendChild(this._indicators),this.count=this.images.length,this.options.numVisible=Math.min(this.count,this.options.numVisible),this.xform="transform",["webkit","Moz","O","ms"].every(t=>{const e=t+"Transform";return void 0===document.body.style[e]||(this.xform=e,!1)}),this._setupEventHandlers(),this._scroll(this.offset)}static get defaults(){return c}static init(t,e={}){return super.init(t,e,p)}static getInstance(t){return t.M_Carousel}destroy(){this._removeEventHandlers(),this.el.M_Carousel=void 0}_setupEventHandlers(){void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTap),this.el.addEventListener("touchmove",this._handleCarouselDrag),this.el.addEventListener("touchend",this._handleCarouselRelease)),this.el.addEventListener("mousedown",this._handleCarouselTap),this.el.addEventListener("mousemove",this._handleCarouselDrag),this.el.addEventListener("mouseup",this._handleCarouselRelease),this.el.addEventListener("mouseleave",this._handleCarouselRelease),this.el.addEventListener("click",this._handleCarouselClick),this.showIndicators&&this._indicators&&this._indicators.querySelectorAll(".indicator-item").forEach(t=>{t.addEventListener("click",this._handleIndicatorClick),t.addEventListener("keypress",this._handleIndicatorKeyPress)}),window.addEventListener("resize",this._handleThrottledResize)}_removeEventHandlers(){void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTap),this.el.removeEventListener("touchmove",this._handleCarouselDrag),this.el.removeEventListener("touchend",this._handleCarouselRelease)),this.el.removeEventListener("mousedown",this._handleCarouselTap),this.el.removeEventListener("mousemove",this._handleCarouselDrag),this.el.removeEventListener("mouseup",this._handleCarouselRelease),this.el.removeEventListener("mouseleave",this._handleCarouselRelease),this.el.removeEventListener("click",this._handleCarouselClick),this.showIndicators&&this._indicators&&this._indicators.querySelectorAll(".indicator-item").forEach(t=>{t.removeEventListener("click",this._handleIndicatorClick)}),window.removeEventListener("resize",this._handleThrottledResize)}_handleThrottledResize=()=>e.throttle(this._handleResize,200,null).bind(this);_handleCarouselTap=t=>{"mousedown"===t.type&&"IMG"===t.target.tagName&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._track,100)};_handleCarouselDrag=t=>{let e,i,s,n;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),s=this.reference-e,n=Math.abs(this.referenceY-i),n<30&&!this.verticalDragged)(s>2||s<-2)&&(this.dragged=!0,this.reference=e,this._scroll(this.offset+s));else{if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;this.verticalDragged=!0}if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1};_handleCarouselRelease=t=>{if(this.pressed)return this.pressed=!1,clearInterval(this.ticker),this.target=this.offset,(this.velocity>10||this.velocity<-10)&&(this.amplitude=.9*this.velocity,this.target=this.offset+this.amplitude),this.target=Math.round(this.target/this.dim)*this.dim,this.noWrap&&(this.target>=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScroll),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1};_handleCarouselClick=t=>{if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){const e=t.target.closest(".carousel-item");if(!e)return;const i=[...e.parentNode.children].indexOf(e);0!==this._wrap(this.center)-i&&(t.preventDefault(),t.stopPropagation()),i<0?t.clientX-t.target.getBoundingClientRect().left>this.el.clientWidth/2?this.next():this.prev():this._cycleTo(i)}};_handleIndicatorClick=t=>{t.stopPropagation(),this._handleIndicatorInteraction(t)};_handleIndicatorKeyPress=t=>{t.stopPropagation(),e.keys.ENTER.includes(t.key)&&this._handleIndicatorInteraction(t)};_handleIndicatorInteraction=t=>{const e=t.target.closest(".indicator-item");if(e){const t=[...e.parentNode.children].indexOf(e);this._cycleTo(t)}};_handleResize=()=>{this.options.fullWidth?(this.itemWidth=this.el.querySelector(".carousel-item").clientWidth,this.imageHeight=this.el.querySelector(".carousel-item.active").clientHeight,this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()};_setCarouselHeight(t=!1){const e=this.el.querySelector(".carousel-item.active")?this.el.querySelector(".carousel-item.active"):this.el.querySelector(".carousel-item"),i=e.querySelector("img");if(i)if(i.complete){const t=i.clientHeight;if(t>0)this.el.style.height=t+"px";else{const t=i.naturalWidth,e=i.naturalHeight,s=this.el.clientWidth/t*e;this.el.style.height=s+"px"}}else i.addEventListener("load",()=>{this.el.style.height=i.offsetHeight+"px"});else if(!t){const t=e.clientHeight;this.el.style.height=t+"px"}}_xpos(t){return t.type.startsWith("touch")&&t.targetTouches.length>=1?t.targetTouches[0].clientX:t.clientX}_ypos(t){return t.type.startsWith("touch")&&t.targetTouches.length>=1?t.targetTouches[0].clientY:t.clientY}_wrap(t){return t>=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}_track=()=>{const t=Date.now(),e=t-this.timestamp,i=1e3*(this.offset-this.frame)/(1+e);this.timestamp=t,this.frame=this.offset,this.velocity=.8*i+.2*this.velocity};_autoScroll=()=>{let t,e;this.amplitude&&(t=Date.now()-this.timestamp,e=this.amplitude*Math.exp(-t/this.options.duration),e>2||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScroll)):this._scroll(this.target))};_scroll(t=0){this.el.classList.contains("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&clearTimeout(this.scrollingTimeout),this.scrollingTimeout=setTimeout(()=>{this.el.classList.remove("scrolling")},this.options.duration);const e=this.center;this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim);const i=this.count>>1,s=this.offset-this.center*this.dim,n=s<0?1:-1,o=-n*s*2/this.dim;let a,l,r,h,d,c;const p=1/this.options.numVisible;if(this.options.fullWidth?(r="translateX(0)",c=1):(r="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",r+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",c=1-p*o),this.showIndicators){const t=this.center%this.count,e=this._indicators.querySelector(".indicator-item.active");if([...e.parentNode.children].indexOf(e)!==t){e.classList.remove("active");const i=t<0?this.count+t:t;this._indicators.querySelectorAll(".indicator-item")[i].classList.add("active")}}if(!this.noWrap||this.center>=0&&this.center0?1-o:1):(h=this.options.dist*(2*a-o*n),d=1-p*(2*a-o*n)),!this.noWrap||this.center-a>=0){l=this.images[this._wrap(this.center-a)];const t=`${r} translateX(${-this.options.shift+(-this.dim*a-s)/2}px) translateZ(${h}px)`;this._updateItemStyle(l,d,-a,t)}}if(!this.noWrap||this.center>=0&&this.center0&&Math.abs(i-this.count)0&&(this.target-=this.dim*i),"function"==typeof e&&(this.oneTimeCallback=e),this.offset!==this.target&&(this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScroll))}next(t=1){(void 0===t||isNaN(t))&&(t=1);let e=this.center+t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}prev(t=1){(void 0===t||isNaN(t))&&(t=1);let e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}set(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}var u;const m={data:[],placeholder:"",secondaryPlaceholder:"",closeIconClass:"material-icons",autocompleteOptions:{},autocompleteOnly:!1,limit:1/0,allowUserInput:!1,onChipAdd:null,onChipSelect:null,onChipDelete:null};function v(t){return[...t.parentNode.children].indexOf(t)}class g extends i{chipsData;hasAutocomplete;autocomplete;#L;#C;#k;static#T;#D;constructor(t,e){super(t,e,u),this.el.M_Chips=this,this.options={...u.defaults,...e},this.el.classList.add("chips"),this.chipsData=[],this.#k=[],this.options.data.length&&(this.chipsData=this.options.data,this.#x()),this.options.allowUserInput&&(this.#S(),this.el.classList.add("input-field"),this.#I(),this.#n(),this.el.append(this.#L))}static get defaults(){return m}static init(t,e={}){return super.init(t,e,u)}static getInstance(t){return t.M_Chips}getData(){return this.chipsData}destroy(){this.options.allowUserInput&&this.#o(),this.#k.forEach(t=>t.remove()),this.#k=[],this.el.M_Chips=void 0}#n(){this.el.addEventListener("click",this.#A),document.addEventListener("keydown",u.#M),document.addEventListener("keyup",u.#O),this.el.addEventListener("blur",u.#R,!0),this.#L.addEventListener("focus",this.#H),this.#L.addEventListener("blur",this.#P),this.#L.addEventListener("keydown",this.#W)}#o(){this.el.removeEventListener("click",this.#A),document.removeEventListener("keydown",u.#M),document.removeEventListener("keyup",u.#O),this.el.removeEventListener("blur",u.#R,!0),this.#L.removeEventListener("focus",this.#H),this.#L.removeEventListener("blur",this.#P),this.#L.removeEventListener("keydown",this.#W)}#A=t=>{const e=t.target.closest(".chip"),i=t.target.classList.contains("close");if(e){const t=[...e.parentNode.children].indexOf(e);i?(this.deleteChip(t),this.#L.focus()):this.selectChip(t)}else this.#L.focus()};static#M(t){u.#T=!0;const i=t.target.closest(".chips"),s=t.target&&i,n=t.target.tagName;if("INPUT"===n||"TEXTAREA"===n||!s)return;const o=i.M_Chips;if(e.keys.BACKSPACE.includes(t.key)||e.keys.DELETE.includes(t.key)){t.preventDefault();let e=o.chipsData.length;if(o.#D){const t=v(o.#D);o.deleteChip(t),o.#D=null,e=Math.max(t-1,0)}o.chipsData.length?o.selectChip(e):o.#L.focus()}else if(e.keys.ARROW_LEFT.includes(t.key)){if(o.#D){const t=v(o.#D)-1;if(t<0)return;o.selectChip(t)}}else if(e.keys.ARROW_RIGHT.includes(t.key)&&o.#D){const t=v(o.#D)+1;t>=o.chipsData.length?o.#L.focus():o.selectChip(t)}}static#O(){u.#T=!1}static#R(t){if(!u.#T&&document.hidden){t.target.closest(".chips").M_Chips.#D=null}}#H=()=>{this.el.classList.add("focus")};#P=()=>{this.el.classList.remove("focus")};#W=t=>{if(u.#T=!0,e.keys.ENTER.includes(t.key)){if(this.hasAutocomplete&&this.autocomplete&&this.autocomplete.isOpen)return;t.preventDefault(),(!this.hasAutocomplete||this.hasAutocomplete&&!this.options.autocompleteOnly)&&this.addChip({id:this.#L.value}),this.#L.value=""}else(e.keys.BACKSPACE.includes(t.key)||e.keys.ARROW_LEFT.includes(t.key))&&""===this.#L.value&&this.chipsData.length&&(t.preventDefault(),this.selectChip(this.chipsData.length-1))};#B(t){if(!t.id)return;const e=document.createElement("li");if(e.classList.add("chip"),e.innerText=t.text||t.id,t.image){const i=document.createElement("img");i.setAttribute("src",t.image),e.insertBefore(i,e.firstChild)}if(this.options.allowUserInput){const t=document.createElement("button");t.classList.add(this.options.closeIconClass,"close"),t.innerText="close",e.appendChild(t)}return e}#x(){this.#k=[];for(let t=0;t{t.length>0&&this.addChip({id:t[0].id,text:t[0].text,image:t[0].image}),this.#L.value="",this.#L.focus()},this.autocomplete=a.init(this.#L,this.options.autocompleteOptions)}#I(){this.#L=this.el.querySelector("input"),this.#L||(this.#L=document.createElement("input"),this.el.append(this.#L)),this.#L.classList.add("input"),this.hasAutocomplete=Object.keys(this.options.autocompleteOptions).length>0,this.hasAutocomplete&&this.#q(),this.#F(),this.#L.getAttribute("id")||this.#L.setAttribute("id",e.guid())}#S(){this.#C=this.el.querySelector("label"),this.#C&&this.#C.setAttribute("for",this.#L.getAttribute("id"))}#F(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?this.#L.placeholder=this.options.placeholder:(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&(this.#L.placeholder=this.options.secondaryPlaceholder)}#V(t){const e=!!t.id,i=!this.chipsData.some(e=>e.id==t.id);return e&&i}addChip(t){if(!this.#V(t)||this.chipsData.length>=this.options.limit)return;const e=this.#B(t);this.#k.push(e),this.chipsData.push(t),this.#L.before(e),this.#F(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd(this.el,e)}deleteChip(t){const e=this.#k[t];this.#k[t].remove(),this.#k.splice(t,1),this.chipsData.splice(t,1),this.#F(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete(this.el,e)}selectChip(t){const e=this.#k[t];this.#D=e,e.focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect(this.el,e)}static Init(){"undefined"!=typeof document&&document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".chips").forEach(t=>{t.addEventListener("click",t=>{if(t.target.classList.contains("close")){const e=t.target.closest(".chip");e&&e.remove()}})})})}static{this.#T=!1}}u=g;const _={accordion:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,inDuration:300,outDuration:300};class y extends i{_headers;constructor(t,e){super(t,e,y),this.el.M_Collapsible=this,this.options={...y.defaults,...e},this._headers=Array.from(this.el.querySelectorAll("li > .collapsible-header")),this._headers.forEach(t=>t.tabIndex=0),this._setupEventHandlers();const i=Array.from(this.el.querySelectorAll("li.active > .collapsible-body"));this.options.accordion?i.length>0&&this._setExpanded(i[0]):i.forEach(t=>this._setExpanded(t))}static get defaults(){return _}static init(t,e={}){return super.init(t,e,y)}static getInstance(t){return t.M_Collapsible}destroy(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}_setupEventHandlers(){this.el.addEventListener("click",this._handleCollapsibleClick),this._headers.forEach(t=>t.addEventListener("keydown",this._handleCollapsibleKeydown))}_removeEventHandlers(){this.el.removeEventListener("click",this._handleCollapsibleClick),this._headers.forEach(t=>t.removeEventListener("keydown",this._handleCollapsibleKeydown))}_handleCollapsibleClick=t=>{const e=t.target.closest(".collapsible-header");if(t.target&&e){if(e.closest(".collapsible")!==this.el)return;const t=e.closest("li"),i=t.classList.contains("active"),s=[...t.parentNode.children].indexOf(t);i?this.close(s):this.open(s)}};_handleCollapsibleKeydown=t=>{e.keys.ENTER.includes(t.key)&&this._handleCollapsibleClick(t)};_setExpanded(t){t.style.maxHeight=t.scrollHeight+"px"}_animateIn(t){const e=this.el.children[t];if(!e)return;const i=e.querySelector(".collapsible-body"),s=this.options.inDuration;i.style.transition=`max-height ${s}ms ease-out`,this._setExpanded(i),setTimeout(()=>{"function"==typeof this.options.onOpenEnd&&this.options.onOpenEnd.call(this,e)},s)}_animateOut(t){const e=this.el.children[t];if(!e)return;const i=e.querySelector(".collapsible-body"),s=this.options.outDuration;i.style.transition=`max-height ${s}ms ease-out`,i.style.maxHeight="0",setTimeout(()=>{"function"==typeof this.options.onCloseEnd&&this.options.onCloseEnd.call(this,e)},s)}open=t=>{const e=Array.from(this.el.children).filter(t=>"LI"===t.tagName),i=e[t];if(i&&!i.classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,i),this.options.accordion){const t=e.filter(t=>t.classList.contains("active"));t.forEach(t=>{const i=e.indexOf(t);this.close(i)})}i.classList.add("active"),this._animateIn(t)}};close=t=>{const e=Array.from(this.el.children).filter(t=>"LI"===t.tagName)[t];e&&e.classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e),e.classList.remove("active"),this._animateOut(t))}}const f={classes:"",dropdownOptions:{}};class E extends i{isMultiple;labelEl;dropdownOptions;input;dropdown;wrapper;selectOptions;_values;nativeTabIndex;constructor(t,e){super(t,e,E),this.el.classList.contains("browser-default")||(this.el.M_FormSelect=this,this.options={...E.defaults,...e},this.isMultiple=this.el.multiple,this.nativeTabIndex=this.el.tabIndex??-1,this.el.tabIndex=-1,this._values=[],this._setupDropdown(),this._setupEventHandlers())}static get defaults(){return f}static init(t,e={}){return super.init(t,e,E)}static getInstance(t){return t.M_FormSelect}destroy(){this._removeEventHandlers(),this._removeDropdown(),this.el.M_FormSelect=void 0}_setupEventHandlers(){this.dropdownOptions.querySelectorAll("li:not(.optgroup)").forEach(t=>{t.addEventListener("click",this._handleOptionClick),t.addEventListener("keydown",t=>{" "!==t.key&&"Enter"!==t.key||this._handleOptionClick(t)})}),this.el.addEventListener("change",this._handleSelectChange),this.input.addEventListener("click",this._handleInputClick)}_removeEventHandlers(){this.dropdownOptions.querySelectorAll("li:not(.optgroup)").forEach(t=>{t.removeEventListener("click",this._handleOptionClick)}),this.el.removeEventListener("change",this._handleSelectChange),this.input.removeEventListener("click",this._handleInputClick)}_handleSelectChange=()=>{this._setValueToInput()};_handleOptionClick=t=>{t.preventDefault();const e=t.target.closest("li");this._selectOptionElement(e),t.stopPropagation()};_arraysEqual(t,e){if(t===e)return!0;if(null==t||null==e)return!1;if(t.length!==e.length)return!1;for(let i=0;ie.optionEl===t),i=this.getSelectedValues();this.isMultiple?this._toggleEntryFromArray(e):(this._deselectAll(),this._selectValue(e)),this._setValueToInput();const s=this.getSelectedValues();!this._arraysEqual(i,s)&&this.el.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0,composed:!0}))}this.isMultiple||this.dropdown.close()}_handleInputClick=()=>{this.dropdown&&this.dropdown.isOpen&&(this._setValueToInput(),this._setSelectedStates())};_setupDropdown(){this.labelEl=document.querySelector('[for="'+this.el.id+'"]'),this.wrapper=document.createElement("div"),this.wrapper.classList.add("select-wrapper","input-field"),this.options.classes.length>0&&this.wrapper.classList.add(...this.options.classes.split(" ")),this.el.before(this.wrapper);const t=document.createElement("div");t.classList.add("hide-select"),this.wrapper.append(t),t.appendChild(this.el),this.el.disabled&&this.wrapper.classList.add("disabled"),this.selectOptions=Array.from(this.el.children).filter(t=>["OPTION","OPTGROUP"].includes(t.tagName)),this.dropdownOptions=document.createElement("ul"),this.dropdownOptions.id=`select-options-${e.guid()}`,this.dropdownOptions.setAttribute("popover","auto"),this.dropdownOptions.classList.add("dropdown-content","select-dropdown"),this.dropdownOptions.setAttribute("role","listbox"),this.dropdownOptions.ariaMultiSelectable=this.isMultiple.toString(),this.isMultiple&&this.dropdownOptions.classList.add("multiple-select-dropdown"),this.selectOptions.length>0&&this.selectOptions.forEach(t=>{if("OPTION"===t.tagName){const e=this._createAndAppendOptionWithIcon(t,this.isMultiple?"multiple":void 0);this._addOptionToValues(t,e)}else if("OPTGROUP"===t.tagName){const i="opt-group-"+e.guid(),s=document.createElement("li");s.classList.add("optgroup"),s.tabIndex=-1,s.setAttribute("role","group"),s.setAttribute("aria-labelledby",i),s.innerHTML=`${t.getAttribute("label")}`,this.dropdownOptions.append(s);const n=[];Array.from(t.children).filter(t=>"OPTION"===t.tagName).forEach(t=>{const i=this._createAndAppendOptionWithIcon(t,"optgroup-option"),s="opt-child-"+e.guid();i.id=s,n.push(s),this._addOptionToValues(t,i)}),s.setAttribute("aria-owns",n.join(" "))}}),this.wrapper.append(this.dropdownOptions),this.input=document.createElement("input"),this.input.id="m_select-input-"+e.guid(),this.input.classList.add("select-dropdown","dropdown-trigger"),this.input.type="text",this.input.readOnly=!0,this.input.setAttribute("data-target",this.dropdownOptions.id),this.input.ariaReadOnly="true",this.input.ariaRequired=this.el.hasAttribute("required").toString(),this.el.disabled&&(this.input.disabled=!0),this.input.setAttribute("tabindex",this.nativeTabIndex.toString());const i=this.el.attributes;for(let t=0;t{const t=this.dropdownOptions.querySelector(".selected");if(t&&(e.keyDown=!0,this.dropdown.focusedIndex=[...t.parentNode.children].indexOf(t),this.dropdown._focusFocusedItem(),e.keyDown=!1,this.dropdown.isScrollable)){let e=t.getBoundingClientRect().top-this.dropdownOptions.getBoundingClientRect().top;e-=this.dropdownOptions.clientHeight/2,this.dropdownOptions.scrollTop=e}this.input.ariaExpanded="true",i&&"function"==typeof i&&i.call(this.dropdown,this.el)},t.onCloseEnd=()=>{this.input.ariaExpanded="false",s&&"function"==typeof s&&s.call(this.dropdown,this.el)},t.closeOnClick=!1,this.dropdown=n.init(this.input,t)}this._setSelectedStates(),this.labelEl&&this.input.after(this.labelEl)}_addOptionToValues(t,e){this._values.push({el:t,optionEl:e})}_removeDropdown(){this.wrapper.querySelector(".caret").remove(),this.input.remove(),this.dropdownOptions.remove(),this.wrapper.before(this.el),this.wrapper.remove()}_createAndAppendOptionWithIcon(t,e){const i=document.createElement("li");i.setAttribute("role","option"),t.disabled&&(i.classList.add("disabled"),i.ariaDisabled="true"),"optgroup-option"===e&&i.classList.add(e);const s=document.createElement("span");s.innerHTML=t.innerHTML,this.isMultiple&&!t.disabled&&(s.innerHTML=``),i.appendChild(s);const n=t.getAttribute("data-icon"),o=t.getAttribute("class")?.split(" ");if(n){const t=document.createElement("img");o&&t.classList.add(...o),t.src=n,t.ariaHidden="true",i.prepend(t)}return this.dropdownOptions.append(i),i}_selectValue(t){t.el.selected=!0,t.optionEl.classList.add("selected"),t.optionEl.ariaSelected="true";const e=t.optionEl.querySelector('input[type="checkbox"]');e&&(e.checked=!0)}_deselectValue(t){t.el.selected=!1,t.optionEl.classList.remove("selected"),t.optionEl.ariaSelected="false";const e=t.optionEl.querySelector('input[type="checkbox"]');e&&(e.checked=!1)}_deselectAll(){this._values.forEach(t=>this._deselectValue(t))}_isValueSelected(t){return this.getSelectedValues().some(e=>e===t.el.value)}_toggleEntryFromArray(t){this._isValueSelected(t)?this._deselectValue(t):this._selectValue(t)}_getSelectedOptions(){return Array.prototype.filter.call(this.el.selectedOptions,t=>t)}_setValueToInput(){const t=this._getSelectedOptions(),e=this._values.filter(e=>t.indexOf(e.el)>=0).filter(t=>!t.el.disabled).map(t=>t.optionEl.querySelector("span").innerText.trim());if(0===e.length){const t=this.el.querySelector("option:disabled");if(t&&""===t.value)return void(this.input.value=t.innerText)}this.input.value=e.join(", ")}_setSelectedStates(){this._values.forEach(t=>{const e=t.el.selected,i=t.optionEl.querySelector('input[type="checkbox"]');i&&(i.checked=e),e?this._activateOption(this.dropdownOptions,t.optionEl):(t.optionEl.classList.remove("selected"),t.optionEl.ariaSelected="false")})}_activateOption(t,e){e&&(this.isMultiple||t.querySelectorAll("li.selected").forEach(t=>t.classList.remove("selected")),e.classList.add("selected"),e.ariaSelected="true")}getSelectedValues(){return this._getSelectedOptions().map(t=>t.value)}}const w={margin:5,transition:10,duration:250,align:"left",title:null,onOpen:null,onClose:null};class b{el;container;options;visible;constructor(t,e,i){this.el=t,this.options={...w,...i},this.container=document.createElement("div"),this.container.classList.add("display-docked"),this.container.append(e),t.parentElement.append(this.container),document.addEventListener("click",t=>{this.visible&&this.el!==t.target&&!t.target.closest(".display-docked")&&this.hide()},!0)}static init(t,e,i){return new b(t,e,i)}show=()=>{if(this.visible)return;this.visible=!0;const t=e._setAbsolutePosition(this.el,this.container,"bottom",this.options.margin,this.options.transition,this.options.align);this.container.style.visibility="visible",this.container.style.transition=`\n transform ${this.options.duration}ms ease-out,\n opacity ${this.options.duration}ms ease-out`,setTimeout(()=>{this.container.style.transform=`translateX(${t.x}px) translateY(${t.y}px)`,this.container.style.opacity=1..toString(),"function"==typeof this.options.onOpen&&this.options.onOpen.call(this)},100)};hide=()=>{this.visible&&(this.visible=!1,this.container.removeAttribute("style"),this.container.style.transition=`\n transform ${this.options.duration}ms ease-out,\n opacity ${this.options.duration}ms ease-out`,setTimeout(()=>{this.container.style.transform="translateX(0px) translateY(0px)",this.container.style.opacity="0","function"==typeof this.options.onClose&&this.options.onClose.call(this)},100))}}const L={classList:["modal"],title:null,onOpen:null,onClose:null};class C{el;container;options;visible;footer;constructor(t,e,i){if(this.el=t,this.options={...L,...i},this.container=document.createElement("dialog"),this.container.classList.add("modal","display-modal",this.options.classList.join(" ")),i.title){const t=document.createElement("div");t.classList.add("modal-header"),t.append(i.title),this.container.append(t)}const s=document.createElement("div");s.classList.add("modal-content"),s.append(e),this.container.append(s),this.footer=document.createElement("div"),this.footer.classList.add("modal-footer"),this.container.append(this.footer),document.body.append(this.container),document.addEventListener("click",t=>{this.visible&&this.el!==t.target&&!t.target.closest(".display-modal")&&this.hide()},!0)}static init(t,e,i){return new C(t,e,i)}show=()=>{this.visible||(this.visible=!0,this.container.setAttribute("open","true"),setTimeout(()=>{"function"==typeof this.options.onOpen&&this.options.onOpen.call(this)},10))};hide=()=>{this.visible&&(this.visible=!1,this.container.removeAttribute("open"),setTimeout(()=>{"function"==typeof this.options.onClose&&this.options.onClose.call(this)},10))}}const k={format:"mmm dd, yyyy",parse:null,isDateRange:!1,dateRangeEndEl:null,isMultipleSelection:!1,defaultDate:null,defaultEndDate:null,setDefaultDate:!1,setDefaultEndDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,yearRangeReverse:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,openByDefault:!1,container:null,showClearBtn:!1,autoSubmit:!0,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onDraw:null,onInputInteraction:null,displayPlugin:null,displayPluginOptions:null,onConfirm:null,onCancel:null};class T extends i{id;multiple=!1;calendarEl;containerEl;yearTextEl;dateTextEl;endDateEl;dateEls;date;endDate;dates;formats;calendars;_y;_m;displayPlugin;footer;static _template;constructor(t,i){super(t,i,T),this.el.M_Datepicker=this,this.options={...T.defaults,...i},i&&i.hasOwnProperty("i18n")&&"object"==typeof i.i18n&&(this.options.i18n={...T.defaults.i18n,...i.i18n}),this.options.minDate&&this.options.minDate.setHours(0,0,0,0),this.options.maxDate&&this.options.maxDate.setHours(0,0,0,0),this.id=e.guid(),this._setupVariables(),this._insertHTMLIntoDOM(),this._setupEventHandlers(),this.options.displayPlugin&&this._setupDisplayPlugin(),this._pickerSetup()}static get defaults(){return k}static init(t,e={}){return super.init(t,e,T)}static _isDate(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}static _isWeekend(t){const e=t.getDay();return 0===e||6===e}static _setToStartOfDay(t){T._isDate(t)&&t.setHours(0,0,0,0)}static _getDaysInMonth(t,e){return[31,T._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}static _isLeapYear(t){return t%4==0&&t%100!=0||t%400==0}static _compareDates(t,e){return t.getTime()===e.getTime()}static _compareWithinRange(t,e,i){return t.getTime()>e.getTime()&&t.getTime()this.formats[e]?this.formats[e](t):e).join("")}setDateFromInput(t){const e=new Date(Date.parse(t.value));this.setDate(e,!1,t==this.endDateEl,!0)}setDate(t=null,e=!1,i=!1,s=!1){const n=this.validateDate(t);n&&(this.options.isMultipleSelection?s||this.setMultiDate(n):this.setSingleDate(n,i),T._setToStartOfDay(n),this.gotoDate(n),e||"function"!=typeof this.options.onSelect||this.options.onSelect.call(this,n))}validateDate(t){if(!t)return this._renderDateDisplay(t),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),!T._isDate(t))return;const e=this.options.minDate,i=this.options.maxDate;return T._isDate(e)&&ti&&(t=i),new Date(t.getTime())}setSingleDate(t,e){e?e&&(this.endDate=t):this.date=t}setMultiDate(t){const e=this.dates?.find(e=>e.getTime()===t.getTime()&&e);e?this.dates.splice(this.dates.indexOf(e),1):this.dates.push(t),this.dates.sort((t,e)=>t.getTime(){if(e>this.dates.length-1)return t}).forEach(t=>{t.remove()}),this.dates.forEach((t,e)=>{if(Array.from(this.dateEls)[e])return void this.setInputValue(this.dateEls[e],t);const i=this.createDateInput();this.setInputValue(i,t),this.dateEls.push(i)})}setInputValue(t,e){"date"==t.type?(this.setDataDate(t,e),t.value=this.formatDate(e,"yyyy-mm-dd")):t.value=this.toString(e),this.el.dispatchEvent(new CustomEvent("change",{bubbles:!0,cancelable:!0,composed:!0,detail:{firedBy:this}}))}_setupDisplayPlugin(){const t={...this.options.displayPluginOptions,onOpen:()=>{document.querySelectorAll(".select-dropdown").forEach(t=>{t.tabIndex=0})},onClose:()=>{document.querySelectorAll(".select-dropdown").forEach(t=>{t.tabIndex=-1})}};"docked"===this.options.displayPlugin&&(this.displayPlugin=b.init(this.el,this.containerEl,t)),"modal"===this.options.displayPlugin&&(this.displayPlugin=C.init(this.el,this.containerEl,{...t,classList:["datepicker-modal"]}),this.footer.remove(),this.footer=this.displayPlugin.footer),this.options.openByDefault&&this.displayPlugin.show()}_renderDateDisplay(t,e=null){const i=T._isDate(t)?t:new Date;if(this.options.isDateRange){const t=T._isDate(e)?e:new Date;this.dateTextEl.innerHTML=`${this.formatDate(i,"mmm d")} - ${this.formatDate(t,"mmm d")}`}else this.dateTextEl.innerHTML=this.formatDate(i,"ddd, mmm d")}gotoDate(t){let e=!0;if(T._isDate(t)){if(this.calendars){const i=new Date(this.calendars[0].year,this.calendars[0].month,1),s=new Date(this.calendars[this.calendars.length-1].year,this.calendars[this.calendars.length-1].month,1),n=t.getTime();s.setMonth(s.getMonth()+1),s.setDate(s.getDate()-1),e=n11&&(t.year+=Math.floor(Math.abs(t.month)/12),t.month-=12),t}nextMonth(){this.calendars[0].month++,this.adjustCalendars()}prevMonth(){this.calendars[0].month--,this.adjustCalendars()}render(t,e,i){const s=new Date,n=T._getDaysInMonth(t,e),o=[];let a=new Date(t,e,1).getDay(),l=[];T._setToStartOfDay(s),this.options.firstDay>0&&(a-=this.options.firstDay,a<0&&(a+=7));const r=0===e?11:e-1,h=11===e?0:e+1,d=0===e?t-1:t,c=11===e?t+1:t,p=T._getDaysInMonth(d,r);let u=n+a,m=u;for(;m>7;)m-=7;u+=7-m;let v=!1;for(let i=0,m=0;i=n+a,f=this.options.startRange&&T._compareDates(this.options.startRange,u),E=this.options.endRange&&T._compareDates(this.options.endRange,u),w=this.options.startRange&&this.options.endRange&&this.options.startRangethis.options.maxDate||this.options.disableWeekends&&T._isWeekend(u)||this.options.disableDayFn&&this.options.disableDayFn(u),L=this.options.isDateRange&&this.date&&this.endDate&&T._compareDates(this.date,u),C=this.options.isDateRange&&this.endDate&&T._compareDates(this.endDate,u),k=this.options.isDateRange&&T._isDate(this.endDate)&&T._compareWithinRange(u,this.date,this.endDate);let D=i-a+1,x=e,S=t,I=!1;T._isDate(this.date)&&(I=T._compareDates(u,this.date)),!I&&T._isDate(this.endDate)&&(I=T._compareDates(u,this.endDate)),this.options.isMultipleSelection&&this.dates?.some(t=>t.getTime()===u.getTime())&&(I=!0),y&&(i!(!t.hasOwnProperty(e)||!0!==t[e])).length),s=["datepicker-day"];if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';s.push("is-outside-current-month"),s.push("is-selection-disabled")}for(const[i,n]of Object.entries(e))t.hasOwnProperty(i)&&t[i]&&s.push(n);return``}renderRow(t,e,i){return''+(e?t.reverse():t).join("")+""}renderTable(t,e,i){return'
'+this.renderHead(t)+this.renderBody(e)+"
"}renderHead(t){const e=[];let i;for(i=0;i<7;i++)e.push(`${this.renderDayName(t,i,!0)}`);return""+(t.isRTL?e.reverse():e).join("")+""}renderBody(t){return""+t.join("")+""}renderTitle(t,e,i,s,n,o){const a=this.options,l=i===a.minYear,r=i===a.maxYear;let h,d,c=[],p='
',u=!0,m=!0;for(h=0;h<12;h++)c.push('");const v='";for(Array.isArray(a.yearRange)?(h=a.yearRange[0],d=a.yearRange[1]+1):(h=i-a.yearRange,d=1+i+a.yearRange),c=[];h=a.minYear&&c.push(``);a.yearRangeReverse&&c.reverse();const g=``;p+=``,p+='
',a.showMonthAfterYear?p+=g+v:p+=v+g,p+="
",l&&(0===s||a.minMonth>=s)&&(u=!1),r&&(11===s||a.maxMonth<=s)&&(m=!1);return p+=``,p+"
"}draw(){const t=this.options,e=t.minYear,i=t.maxYear,s=t.minMonth,n=t.maxMonth;let o="";this._y<=e&&(this._y=e,!isNaN(s)&&this._m=i&&(this._y=i,!isNaN(n)&&this._m>n&&(this._m=n));const a="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(let t=0;t<1;t++)this.options.isDateRange?this._renderDateDisplay(this.date,this.endDate):this._renderDateDisplay(this.date),o+=this.renderTitle(this,t,this.calendars[t].year,this.calendars[t].month,this.calendars[0].year,a)+this.render(this.calendars[t].year,this.calendars[t].month,a);this.destroySelects(),this.calendarEl.innerHTML=o;const l=this.calendarEl.querySelector(".orig-select-year"),r=this.calendarEl.querySelector(".orig-select-month");E.init(l,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),E.init(r,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),l.addEventListener("change",this._handleYearChange),r.addEventListener("change",this._handleMonthChange),"function"==typeof this.options.onDraw&&this.options.onDraw.call(this)}_setupEventHandlers(){this.el.addEventListener("click",this._handleInputClick),this.el.addEventListener("keydown",this._handleInputKeydown),this.el.addEventListener("change",this._handleInputChange),this.calendarEl.addEventListener("click",this._handleCalendarClick)}_setupVariables(){const t=document.createElement("template");t.innerHTML=T._template.trim(),this.containerEl=t.content.firstChild,this.calendarEl=this.containerEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.containerEl.querySelector(".year-text"),this.dateTextEl=this.containerEl.querySelector(".date-text"),this.footer=this.containerEl.querySelector(".datepicker-footer"),this.formats={d:t=>t.getDate(),dd:t=>{const e=t.getDate();return(e<10?"0":"")+e},ddd:t=>this.options.i18n.weekdaysShort[t.getDay()],dddd:t=>this.options.i18n.weekdays[t.getDay()],m:t=>t.getMonth()+1,mm:t=>{const e=t.getMonth()+1;return(e<10?"0":"")+e},mmm:t=>this.options.i18n.monthsShort[t.getMonth()],mmmm:t=>this.options.i18n.months[t.getMonth()],yy:t=>(""+t.getFullYear()).slice(2),yyyy:t=>t.getFullYear()}}_pickerSetup(){this.options.defaultDate||(this.options.defaultDate=new Date(Date.parse(this.el.value)));const t=this.options.defaultDate;if(T._isDate(t)?this.options.setDefaultDate?(this.setDate(t,!0),this.setInputValue(this.el,t)):this.gotoDate(t):this.gotoDate(new Date),this.options.isDateRange){this.multiple=!0;const t=this.options.defaultEndDate;T._isDate(t)&&this.options.setDefaultEndDate&&(this.setDate(t,!0,!0),this.setInputValue(this.endDateEl,t))}this.options.isMultipleSelection&&(this.multiple=!0,this.dates=[],this.dateEls=[],this.dateEls.push(this.el))}_removeEventHandlers(){this.el.removeEventListener("click",this._handleInputClick),this.el.removeEventListener("keydown",this._handleInputKeydown),this.el.removeEventListener("change",this._handleInputChange),this.calendarEl.removeEventListener("click",this._handleCalendarClick),this.options.isDateRange&&(this.endDateEl.removeEventListener("click",this._handleInputClick),this.endDateEl.removeEventListener("keypress",this._handleInputKeydown),this.endDateEl.removeEventListener("change",this._handleInputChange))}_handleInputClick=t=>{"date"==t.type&&t.preventDefault(),this.setDateFromInput(t.target),this.draw(),this.gotoDate(t.target===this.el?this.date:this.endDate),this.displayPlugin&&this.displayPlugin.show(),this.options.onInputInteraction&&this.options.onInputInteraction.call(this)};_handleInputKeydown=t=>{e.keys.ENTER.includes(t.key)&&(t.preventDefault(),this.setDateFromInput(t.target),this.draw(),this.displayPlugin&&this.displayPlugin.show(),this.options.onInputInteraction&&this.options.onInputInteraction.call(this))};_handleCalendarClick=t=>{const e=t.target;if(!e.classList.contains("is-disabled"))if(!e.classList.contains("datepicker-day-button")||e.classList.contains("is-empty")||e.parentElement.classList.contains("is-disabled"))e.closest(".month-prev")?this.prevMonth():e.closest(".month-next")&&this.nextMonth();else{const e=new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"));if((!this.multiple||this.multiple&&this.options.isMultipleSelection)&&this.setDate(e),this.options.isDateRange){const t=T._isDate(this.date)&&this.options.autoSubmit;this._handleDateRangeCalendarClick(e),t&&this._confirm()}!this.options.isDateRange&&this.options.autoSubmit&&this._confirm()}};_handleDateRangeCalendarClick=t=>{if(null!=this.endDate&&T._compareDates(t,this.endDate))this._clearDates(),this.draw();else{if(T._isDate(this.date)&&T._comparePastDate(t,this.date))return;this.setDate(t,!0,T._isDate(this.date))}};_handleClearClick=()=>{this._clearDates(),this.setInputValues()};_clearDates=()=>{this.date=null,this.endDate=null,this.draw()};_handleMonthChange=t=>{this.gotoMonth(t.target.value)};_handleYearChange=t=>{this.gotoYear(t.target.value)};gotoMonth(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}gotoYear(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}_handleInputChange=t=>{let e;const i=t.target;t.detail?.firedBy!==this&&(i!=this.endDateEl||this.date)&&(e=this.options.parse?this.options.parse(t.target.value,"function"==typeof this.options.format?this.options.format(new Date(this.el.value)):this.options.format):new Date(Date.parse(t.target.value)),T._isDate(e)&&(this.setDate(e,!1,i==this.endDateEl,!0),"date"==t.type&&(this.setDataDate(t,e),this.setInputValues())))};renderDayName(t,e,i=!1){for(e+=t.firstDay;e>=7;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}createDateInput(){const t=this.el.cloneNode(!0);return t.addEventListener("click",this._handleInputClick),t.addEventListener("keypress",this._handleInputKeydown),t.addEventListener("change",this._handleInputChange),this.el.parentElement.appendChild(t),t}_finishSelection=()=>{this.setInputValues()};_confirm=()=>{this._finishSelection(),this.displayPlugin&&this.displayPlugin.hide(),"function"==typeof this.options.onConfirm&&this.options.onConfirm.call(this)};_cancel=()=>{this.displayPlugin&&this.displayPlugin.hide(),"function"==typeof this.options.onCancel&&this.options.onCancel.call(this)};open(){return console.warn("Datepicker.open() is deprecated. Remove this method and wrap in modal yourself."),this}close(){return console.warn("Datepicker.close() is deprecated. Remove this method and wrap in modal yourself."),this}static{T._template='\n
\n
\n \n \n
\n
\n
\n \n
\n
'}}class D{static validateField(t){if(!t)return void console.error("No text field element found");const e=null!==t.getAttribute("data-length"),i=parseInt(t.getAttribute("data-length")),s=t.value.length;0===s&&!1===t.validity.badInput&&!t.required&&t.classList.contains("validate")?t.classList.remove("invalid"):t.classList.contains("validate")&&(t.validity.valid&&e&&s<=i||t.validity.valid&&!e?t.classList.remove("invalid"):t.classList.add("invalid"))}static textareaAutoResize(t){const e=t;let i=document.querySelector(".hiddendiv");i||(i=document.createElement("div"),i.classList.add("hiddendiv","common"),document.body.append(i));const s=getComputedStyle(e),n=s.fontFamily,o=s.fontSize,a=s.lineHeight,l=s.paddingTop,r=s.paddingRight,h=s.paddingBottom,d=s.paddingLeft;o&&(i.style.fontSize=o),n&&(i.style.fontFamily=n),a&&(i.style.lineHeight=a),l&&(i.style.paddingTop=l),r&&(i.style.paddingRight=r),h&&(i.style.paddingBottom=h),d&&(i.style.paddingLeft=d),e.hasAttribute("original-height")||e.setAttribute("original-height",e.getBoundingClientRect().height.toString()),"off"===e.getAttribute("wrap")&&(i.style.overflowWrap="normal",i.style.whiteSpace="pre"),i.innerText=e.value+"\n",i.innerHTML=i.innerHTML.replace(/\n/g,"
"),e.offsetWidth>0&&e.offsetHeight>0?i.style.width=e.getBoundingClientRect().width+"px":i.style.width=window.innerWidth/2+"px";const c=parseInt(e.getAttribute("original-height")),p=parseInt(e.getAttribute("previous-length"));isNaN(c)||(c<=i.clientHeight?e.style.height=i.clientHeight+"px":e.value.length{document.addEventListener("change",t=>{const e=t.target;if(e instanceof HTMLInputElement){if(0!==e.value.length||null!==e.getAttribute("placeholder"))for(const t of e.parentNode.children)"label"==t.tagName&&t.classList.add("active");D.validateField(e)}}),document.addEventListener("keyup",t=>{const i=t.target;i instanceof HTMLInputElement&&["radio","checkbox"].includes(i.type)&&e.keys.TAB.includes(t.key)&&(i.classList.add("tabbed"),i.addEventListener("blur",()=>i.classList.remove("tabbed"),{once:!0}))}),document.querySelectorAll(".materialize-textarea").forEach(t=>{D.InitTextarea(t)}),document.querySelectorAll('.file-field input[type="file"]').forEach(t=>{D.InitFileInputPath(t)})})}static InitTextarea(t){t.setAttribute("original-height",t.getBoundingClientRect().height.toString()),t.setAttribute("previous-length",(t.value||"").length.toString()),D.textareaAutoResize(t),t.addEventListener("keyup",t=>D.textareaAutoResize(t.target)),t.addEventListener("keydown",t=>D.textareaAutoResize(t.target))}static InitFileInputPath(t){t.addEventListener("change",()=>{const e=t.closest(".file-field").querySelector("input.file-path"),i=t.files,s=[];for(let t=0;t{this._handleMaterialboxToggle()};_handleMaterialboxKeypress=t=>{e.keys.ENTER.includes(t.key)&&this._handleMaterialboxToggle()};_handleMaterialboxToggle=()=>{!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()};_handleWindowScroll=()=>{this.overlayActive&&this.close()};_handleWindowResize=()=>{this.overlayActive&&this.close()};_handleWindowEscape=t=>{e.keys.ESC.includes(t.key)&&this.doneAnimating&&this.overlayActive&&this.close()};_makeAncestorsOverflowVisible(){this._changedAncestorList=[];let t=this.placeholder.parentNode;for(;null!==t&&t!==document;){const e=t;"visible"!==e.style.overflow&&(e.style.overflow="visible",this._changedAncestorList.push(e)),t=t.parentNode}}_offset(t){const e=t.getBoundingClientRect(),i=document.documentElement;return{top:e.top+window.scrollY-i.clientTop,left:e.left+window.scrollX-i.clientLeft}}_updateVars(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}_animateImageIn(){this.el.style.maxHeight=this.newHeight.toString()+"px",this.el.style.maxWidth=this.newWidth.toString()+"px";const t=this.options.inDuration;this.el.style.transition="none",this.el.style.height=this.originalHeight+"px",this.el.style.width=this.originalWidth+"px",setTimeout(()=>{this.el.style.transition=`height ${t}ms ease,\n width ${t}ms ease,\n left ${t}ms ease,\n top ${t}ms ease\n `,this.el.style.height=this.newHeight+"px",this.el.style.width=this.newWidth+"px",this.el.style.left=e.getDocumentScrollLeft()+this.windowWidth/2-this._offset(this.placeholder).left-this.newWidth/2+"px",this.el.style.top=e.getDocumentScrollTop()+this.windowHeight/2-this._offset(this.placeholder).top-this.newHeight/2+"px"},1),setTimeout(()=>{this.doneAnimating=!0,"function"==typeof this.options.onOpenEnd&&this.options.onOpenEnd.call(this,this.el)},t)}_animateImageOut(){const t=this.options.outDuration;this.el.style.transition=`height ${t}ms ease,\n width ${t}ms ease,\n left ${t}ms ease,\n top ${t}ms ease\n `,this.el.style.height=this.originalWidth+"px",this.el.style.width=this.originalWidth+"px",this.el.style.left="0",this.el.style.top="0",setTimeout(()=>{this.placeholder.style.height="",this.placeholder.style.width="",this.placeholder.style.position="",this.placeholder.style.top="",this.placeholder.style.left="",this.attrWidth&&this.el.setAttribute("width",this.attrWidth.toString()),this.attrHeight&&this.el.setAttribute("height",this.attrHeight.toString()),this.el.removeAttribute("style"),this.originInlineStyles&&this.el.setAttribute("style",this.originInlineStyles),this.el.classList.remove("active"),this.doneAnimating=!0,this._changedAncestorList.forEach(t=>t.style.overflow=""),"function"==typeof this.options.onCloseEnd&&this.options.onCloseEnd.call(this,this.el)},t)}_addCaption(){this._photoCaption=document.createElement("div"),this._photoCaption.classList.add("materialbox-caption"),this._photoCaption.innerText=this.caption,document.body.append(this._photoCaption),this._photoCaption.style.display="inline",this._photoCaption.style.transition="none",this._photoCaption.style.opacity="0";const t=this.options.inDuration;setTimeout(()=>{this._photoCaption.style.transition=`opacity ${t}ms ease`,this._photoCaption.style.opacity="1"},1)}_removeCaption(){const t=this.options.outDuration;this._photoCaption.style.transition=`opacity ${t}ms ease`,this._photoCaption.style.opacity="0",setTimeout(()=>{this._photoCaption.remove()},t)}_addOverlay(){this._overlay=document.createElement("div"),this._overlay.id="materialbox-overlay",this._overlay.addEventListener("click",()=>{this.doneAnimating&&this.close()},{once:!0}),this.el.before(this._overlay);const t=this._overlay.getBoundingClientRect();this._overlay.style.width=this.windowWidth+"px",this._overlay.style.height=this.windowHeight+"px",this._overlay.style.left=-1*t.left+"px",this._overlay.style.top=-1*t.top+"px",this._overlay.style.transition="none",this._overlay.style.opacity="0";const e=this.options.inDuration;setTimeout(()=>{this._overlay.style.transition=`opacity ${e}ms ease`,this._overlay.style.opacity="1"},1)}_removeOverlay(){const t=this.options.outDuration;this._overlay.style.transition=`opacity ${t}ms ease`,this._overlay.style.opacity="0",setTimeout(()=>{this.overlayActive=!1,this._overlay.remove()},t)}open=()=>{this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.el.classList.add("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.style.width=this.placeholder.getBoundingClientRect().width+"px",this.placeholder.style.height=this.placeholder.getBoundingClientRect().height+"px",this.placeholder.style.position="relative",this.placeholder.style.top="0",this.placeholder.style.left="0",this._makeAncestorsOverflowVisible(),this.el.style.position="absolute",this.el.style.zIndex="1000",this.el.style.willChange="left, top, width, height",this.attrWidth=this.el.getAttribute("width"),this.attrHeight=this.el.getAttribute("height"),this.attrWidth&&(this.el.style.width=this.attrWidth+"px",this.el.removeAttribute("width")),this.attrHeight&&(this.el.style.width=this.attrHeight+"px",this.el.removeAttribute("height")),this._addOverlay(),""!==this.caption&&this._addCaption();const t=this.originalWidth/this.windowWidth,e=this.originalHeight/this.windowHeight;if(this.newWidth=0,this.newHeight=0,t>e){const t=this.originalHeight/this.originalWidth;this.newWidth=.9*this.windowWidth,this.newHeight=.9*this.windowWidth*t}else{const t=this.originalWidth/this.originalHeight;this.newWidth=.9*this.windowHeight*t,this.newHeight=.9*this.windowHeight}this._animateImageIn(),window.addEventListener("scroll",this._handleWindowScroll),window.addEventListener("resize",this._handleWindowResize),window.addEventListener("keyup",this._handleWindowEscape)};close=()=>{this._updateVars(),this.doneAnimating=!1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),window.removeEventListener("scroll",this._handleWindowScroll),window.removeEventListener("resize",this._handleWindowResize),window.removeEventListener("keyup",this._handleWindowEscape),this._removeOverlay(),this._animateImageOut(),""!==this.caption&&this._removeCaption()}}const I={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"};class A extends i{constructor(t,e){super(t,e,A),this.el.M_Modal=this,this.options={...A.defaults,...e},this.el.tabIndex=0,this._setupEventHandlers()}static get defaults(){return I}static init(t,e={}){return super.init(t,e,A)}static getInstance(t){return t.M_Modal}destroy(){}_setupEventHandlers(){}_removeEventHandlers(){}_handleTriggerClick(){}_handleOverlayClick(){}_handleModalCloseClick(){}_handleKeydown(){}_handleFocus(){}open(){return this}close(){return this}static#$(t){return`\n ${t.header?'":""}\n \n ${t.header?'":""}\n `}static#N(t){const e=document.createElement("dialog");return e.id=t.id,e}static create(t){return this.#N(t)}}const M={edge:"left",draggable:!0,dragTargetWidth:"10px",inDuration:250,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0};class O extends i{id;isOpen;isFixed;isDragged;lastWindowWidth;lastWindowHeight;static _sidenavs;_overlay;dragTarget;_startingXpos;_xPos;_time;_width;_initialScrollTop;_verticallyScrolling;deltaX;velocityX;percentOpen;constructor(t,e){super(t,e,O),this.el.M_Sidenav=this,this.options={...O.defaults,...e},this.id=this.el.id,this.isOpen=!1,this.isFixed=this.el.classList.contains("sidenav-fixed"),this.isDragged=!1,this.lastWindowWidth=window.innerWidth,this.lastWindowHeight=window.innerHeight,this._createOverlay(),this._createDragTarget(),this._setupEventHandlers(),this._setupClasses(),this._setupFixed(),O._sidenavs.push(this)}static get defaults(){return M}static init(t,e={}){return super.init(t,e,O)}static getInstance(t){return t.M_Sidenav}destroy(){this._removeEventHandlers(),this._enableBodyScrolling(),this._overlay.parentNode.removeChild(this._overlay),this.dragTarget.parentNode.removeChild(this.dragTarget),this.el.M_Sidenav=void 0,this.el.style.transform="";const t=O._sidenavs.indexOf(this);t>=0&&O._sidenavs.splice(t,1)}_createOverlay(){this._overlay=document.createElement("div"),this._overlay.classList.add("sidenav-overlay"),this._overlay.addEventListener("click",this.close),document.body.appendChild(this._overlay)}_setupEventHandlers(){0===O._sidenavs.length&&document.body.addEventListener("click",this._handleTriggerClick);this.dragTarget.addEventListener("touchmove",this._handleDragTargetDrag,null),this.dragTarget.addEventListener("touchend",this._handleDragTargetRelease),this._overlay.addEventListener("touchmove",this._handleCloseDrag,null),this._overlay.addEventListener("touchend",this._handleCloseRelease),this.el.addEventListener("touchmove",this._handleCloseDrag),this.el.addEventListener("touchend",this._handleCloseRelease),this.el.addEventListener("click",this._handleCloseTriggerClick),this.isFixed&&window.addEventListener("resize",this._handleWindowResize),this._setAriaHidden(),this._setTabIndex()}_removeEventHandlers(){1===O._sidenavs.length&&document.body.removeEventListener("click",this._handleTriggerClick),this.dragTarget.removeEventListener("touchmove",this._handleDragTargetDrag),this.dragTarget.removeEventListener("touchend",this._handleDragTargetRelease),this._overlay.removeEventListener("touchmove",this._handleCloseDrag),this._overlay.removeEventListener("touchend",this._handleCloseRelease),this.el.removeEventListener("touchmove",this._handleCloseDrag),this.el.removeEventListener("touchend",this._handleCloseRelease),this.el.removeEventListener("click",this._handleCloseTriggerClick),this.isFixed&&window.removeEventListener("resize",this._handleWindowResize)}_handleTriggerClick(t){const i=t.target.closest(".sidenav-trigger");if(t.target&&i){const s=e.getIdFromTrigger(i),n=document.getElementById(s).M_Sidenav;n&&n.open(),t.preventDefault()}}_startDrag(t){const i=t.targetTouches[0].clientX;this.isDragged=!0,this._startingXpos=i,this._xPos=this._startingXpos,this._time=Date.now(),this._width=this.el.getBoundingClientRect().width,this._overlay.style.display="block",this._initialScrollTop=this.isOpen?this.el.scrollTop:e.getDocumentScrollTop(),this._verticallyScrolling=!1}_dragMoveUpdate(t){const i=t.targetTouches[0].clientX,s=this.isOpen?this.el.scrollTop:e.getDocumentScrollTop();this.deltaX=Math.abs(this._xPos-i),this._xPos=i,this.velocityX=this.deltaX/(Date.now()-this._time),this._time=Date.now(),this._initialScrollTop!==s&&(this._verticallyScrolling=!0)}_handleDragTargetDrag=t=>{if(!this._isDraggable())return;let e=this._calculateDelta(t);const i=e>0?"right":"left";e=Math.min(this._width,Math.abs(e)),this.options.edge===i&&(e=0);let s=e,n="translateX(-100%)";"right"===this.options.edge&&(n="translateX(100%)",s=-s),this.percentOpen=Math.min(1,e/this._width),this.el.style.transform=`${n} translateX(${s}px)`,this._overlay.style.opacity=this.percentOpen.toString()};_handleDragTargetRelease=()=>{this.isDragged&&(this.percentOpen>.2?this.open():this._animateOut(),this.isDragged=!1,this._verticallyScrolling=!1)};_handleCloseDrag=t=>{if(!this.isOpen||!this._isDraggable())return;let e=this._calculateDelta(t);const i=e>0?"right":"left";e=Math.min(this._width,Math.abs(e)),this.options.edge!==i&&(e=0);let s=-e;"right"===this.options.edge&&(s=-s),this.percentOpen=Math.min(1,1-e/this._width),this.el.style.transform=`translateX(${s}px)`,this._overlay.style.opacity=this.percentOpen.toString()};_calculateDelta=t=>(this.isDragged||this._startDrag(t),this._dragMoveUpdate(t),this._xPos-this._startingXpos);_handleCloseRelease=()=>{this.isOpen&&this.isDragged&&(this.percentOpen>.8?this._animateIn():this.close(),this.isDragged=!1,this._verticallyScrolling=!1)};_handleCloseTriggerClick=t=>{t.target.closest(".sidenav-close")&&!this._isCurrentlyFixed()&&this.close()};_handleWindowResize=()=>{this.lastWindowWidth!==window.innerWidth&&(window.innerWidth>992?this.open():this.close()),this.lastWindowWidth=window.innerWidth,this.lastWindowHeight=window.innerHeight};_setupClasses(){"right"===this.options.edge&&(this.el.classList.add("right-aligned"),this.dragTarget.classList.add("right-aligned"))}_removeClasses(){this.el.classList.remove("right-aligned"),this.dragTarget.classList.remove("right-aligned")}_setupFixed(){this._isCurrentlyFixed()&&this.open()}_isDraggable(){return this.options.draggable&&!this._isCurrentlyFixed()&&!this._verticallyScrolling}_isCurrentlyFixed(){return this.isFixed&&window.innerWidth>992}_createDragTarget(){const t=document.createElement("div");t.classList.add("drag-target"),t.style.width=this.options.dragTargetWidth,document.body.appendChild(t),this.dragTarget=t}_preventBodyScrolling(){document.body.style.overflow="hidden"}_enableBodyScrolling(){document.body.style.overflow=""}open=()=>{!0!==this.isOpen&&(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._isCurrentlyFixed()?(this.el.style.transform="translateX(0)",this._enableBodyScrolling(),this._overlay.style.display="none"):(this.options.preventScrolling&&this._preventBodyScrolling(),this.isDragged&&1==this.percentOpen||this._animateIn(),this._setAriaHidden(),this._setTabIndex()))};close=()=>{if(!1!==this.isOpen)if(this.isOpen=!1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._isCurrentlyFixed()){const t="left"===this.options.edge?"-105%":"105%";this.el.style.transform=`translateX(${t})`}else this._enableBodyScrolling(),this.isDragged&&0==this.percentOpen?this._overlay.style.display="none":this._animateOut(),this._setAriaHidden(),this._setTabIndex()};_animateIn(){this._animateSidenavIn(),this._animateOverlayIn()}_animateOut(){this._animateSidenavOut(),this._animateOverlayOut()}_animateSidenavIn(){let t="left"===this.options.edge?-1:1;this.isDragged&&(t="left"===this.options.edge?t+this.percentOpen:t-this.percentOpen);const e=this.options.inDuration;this.el.style.transition="none",this.el.style.transform="translateX("+100*t+"%)",setTimeout(()=>{this.el.style.transition=`transform ${e}ms ease`,this.el.style.transform="translateX(0)"},1),setTimeout(()=>{"function"==typeof this.options.onOpenEnd&&this.options.onOpenEnd.call(this,this.el)},e)}_animateSidenavOut(){const t="left"===this.options.edge?-1:1,e=this.options.outDuration;this.el.style.transition=`transform ${e}ms ease`,this.el.style.transform="translateX("+100*t+"%)",setTimeout(()=>{"function"==typeof this.options.onCloseEnd&&this.options.onCloseEnd.call(this,this.el)},e)}_animateOverlayIn(){let t=0;this.isDragged?t=this.percentOpen:this._overlay.style.display="block";const e=this.options.inDuration;this._overlay.style.transition="none",this._overlay.style.opacity=t.toString(),setTimeout(()=>{this._overlay.style.transition=`opacity ${e}ms ease`,this._overlay.style.opacity="1"},1)}_animateOverlayOut(){const t=this.options.outDuration;this._overlay.style.transition=`opacity ${t}ms ease`,this._overlay.style.opacity="0",setTimeout(()=>{this._overlay.style.display="none"},t)}_setAriaHidden=()=>{this.el.ariaHidden=this.isOpen?"false":"true";const t=document.querySelector(".nav-wrapper ul");t&&(t.ariaHidden=this.isOpen.toString())};_setTabIndex=()=>{const t=document.querySelectorAll(".nav-wrapper ul li a"),e=document.querySelectorAll(".sidenav li a");t&&t.forEach(t=>{t.tabIndex=this.isOpen?-1:0}),e&&e.forEach(t=>{t.tabIndex=this.isOpen?0:-1})};static{O._sidenavs=[]}}const R={indicators:!0,height:400,duration:500,interval:6e3,pauseOnFocus:!0,pauseOnHover:!0,indicatorLabelFunc:null};class H extends i{activeIndex;interval;eventPause;_slider;_slides;_activeSlide;_indicators;_hovered;_focused;_focusCurrent;_sliderId;constructor(t,i){super(t,i,H),this.el.M_Slider=this,this.options={...H.defaults,...i},this.interval=null,this.eventPause=!1,this._hovered=!1,this._focused=!1,this._focusCurrent=!1,this._slider=this.el.querySelector(".slides"),this._slides=Array.from(this._slider.querySelectorAll("li")),this.activeIndex=this._slides.findIndex(t=>t.classList.contains("active")),-1!==this.activeIndex&&(this._activeSlide=this._slides[this.activeIndex]),this._setSliderHeight(),this._slider.hasAttribute("id")?this._sliderId=this._slider.getAttribute("id"):(this._sliderId="slider-"+e.guid(),this._slider.setAttribute("id",this._sliderId));const s="data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";this._slides.forEach(t=>{const e=t.querySelector("img");e&&e.src!==s&&(e.style.backgroundImage="url("+e.src+")",e.src=s),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.style.visibility="hidden"}),this._setupIndicators(),this._activeSlide?(this._activeSlide.style.display="block",this._activeSlide.style.visibility="visible"):(this.activeIndex=0,this._slides[0].classList.add("active"),this._slides[0].style.visibility="visible",this._activeSlide=this._slides[0],this._animateSlide(this._slides[0],!0),this.options.indicators&&this._indicators[this.activeIndex].children[0].classList.add("active")),this._setupEventHandlers(),this.start()}static get defaults(){return R}static init(t,e={}){return super.init(t,e,H)}static getInstance(t){return t.M_Slider}destroy(){this.pause(),this._removeIndicators(),this._removeEventHandlers(),this.el.M_Slider=void 0}_setupEventHandlers(){this.options.pauseOnFocus&&(this.el.addEventListener("focusin",this._handleAutoPauseFocus),this.el.addEventListener("focusout",this._handleAutoStartFocus)),this.options.pauseOnHover&&(this.el.addEventListener("mouseenter",this._handleAutoPauseHover),this.el.addEventListener("mouseleave",this._handleAutoStartHover)),this.options.indicators&&this._indicators.forEach(t=>{t.addEventListener("click",this._handleIndicatorClick)})}_removeEventHandlers(){this.options.pauseOnFocus&&(this.el.removeEventListener("focusin",this._handleAutoPauseFocus),this.el.removeEventListener("focusout",this._handleAutoStartFocus)),this.options.pauseOnHover&&(this.el.removeEventListener("mouseenter",this._handleAutoPauseHover),this.el.removeEventListener("mouseleave",this._handleAutoStartHover)),this.options.indicators&&this._indicators.forEach(t=>{t.removeEventListener("click",this._handleIndicatorClick)})}_handleIndicatorClick=t=>{const e=t.target.parentElement,i=[...e.parentNode.children].indexOf(e);this._focusCurrent=!0,this.set(i)};_handleAutoPauseHover=()=>{this._hovered=!0,null!=this.interval&&this._pause(!0)};_handleAutoPauseFocus=()=>{this._focused=!0,null!=this.interval&&this._pause(!0)};_handleAutoStartHover=()=>{this._hovered=!1,this.options.pauseOnFocus&&this._focused||!this.eventPause||this.start()};_handleAutoStartFocus=()=>{this._focused=!1,this.options.pauseOnHover&&this._hovered||!this.eventPause||this.start()};_handleInterval=()=>{const t=this._slider.querySelector(".active");let e=[...t.parentNode.children].indexOf(t);this._slides.length===e+1?e=0:e+=1,this.set(e)};_animateSlide(t,e){let i=0,s=0;t.style.opacity=e?"0":"1",setTimeout(()=>{t.style.transition=`opacity ${this.options.duration}ms ease`,t.style.opacity=e?"1":"0"},1);const n=t.querySelector(".caption");n&&(n.classList.contains("center-align")?s=-100:n.classList.contains("right-align")?i=100:n.classList.contains("left-align")&&(i=-100),n.style.opacity=e?"0":"1",n.style.transform=e?`translate(${i}px, ${s}px)`:"translate(0, 0)",setTimeout(()=>{n.style.transition=`opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`,n.style.opacity=e?"1":"0",n.style.transform=e?"translate(0, 0)":`translate(${i}px, ${s}px)`},this.options.duration))}_setSliderHeight(){this.el.classList.contains("fullscreen")||(this.options.indicators?this.el.style.height=this.options.height+40+"px":this.el.style.height=this.options.height+"px",this._slider.style.height=this.options.height+"px")}_setupIndicators(){if(this.options.indicators){const t=document.createElement("ul");t.classList.add("indicators");const e=[];this._slides.forEach((i,s)=>{const n=this.options.indicatorLabelFunc?this.options.indicatorLabelFunc.call(this,s+1,0===s):`${s+1}`,o=document.createElement("li");o.classList.add("indicator-item"),o.innerHTML=``,e.push(o),t.append(o)}),this.el.append(t),this._indicators=e}}_removeIndicators(){this.el.querySelector("ul.indicators").remove()}set(t){if(t>=this._slides.length?t=0:t<0&&(t=this._slides.length-1),this.activeIndex===t)return;this._activeSlide=this._slides[this.activeIndex];const e=this._activeSlide.querySelector(".caption");if(this._activeSlide.classList.remove("active"),this._slides.forEach(t=>t.style.visibility="visible"),this._activeSlide.style.opacity="0",setTimeout(()=>{this._slides.forEach(t=>{t.classList.contains("active")||(t.style.opacity="0",t.style.transform="translate(0, 0)",t.style.visibility="hidden")})},this.options.duration),e.style.opacity="0",this.options.indicators){const e=this._indicators[this.activeIndex].children[0],i=this._indicators[t].children[0];e.classList.remove("active"),i.classList.add("active"),"function"==typeof this.options.indicatorLabelFunc&&(e.ariaLabel=this.options.indicatorLabelFunc.call(this,this.activeIndex,!1),i.ariaLabel=this.options.indicatorLabelFunc.call(this,t,!0))}this._animateSlide(this._slides[t],!0),this._slides[t].classList.add("active"),this.activeIndex=t,null!=this.interval&&this.start()}_pause(t){clearInterval(this.interval),this.eventPause=t,this.interval=null}pause=()=>{this._pause(!1)};start=()=>{clearInterval(this.interval),this.interval=setInterval(this._handleInterval,this.options.duration+this.options.interval),this.eventPause=!1};next=()=>{let t=this.activeIndex+1;t>=this._slides.length?t=0:t<0&&(t=this._slides.length-1),this.set(t)};prev=()=>{let t=this.activeIndex-1;t>=this._slides.length?t=0:t<0&&(t=this._slides.length-1),this.set(t)}}const P={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0};class W extends i{_tabLinks;_index;_indicator;_tabWidth;_tabsWidth;_tabsCarousel;_activeTabLink;_content;constructor(t,e){super(t,e,W),this.el.M_Tabs=this,this.options={...W.defaults,...e},this._tabLinks=this.el.querySelectorAll("li.tab > a"),this._index=0,this._setupActiveTabLink(),this.options.swipeable?this._setupSwipeableTabs():this._setupNormalTabs(),this._setTabsAndTabWidth(),this._createIndicator(),this._setupEventHandlers()}static get defaults(){return P}static init(t,e={}){return super.init(t,e,W)}static getInstance(t){return t.M_Tabs}destroy(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.el.M_Tabs=void 0}get index(){return this._index}_setupEventHandlers(){window.addEventListener("resize",this._handleWindowResize),this.el.addEventListener("click",this._handleTabClick),this.el.addEventListener("wheel",t=>{0!==t.deltaY&&(t.preventDefault(),this.el.scrollLeft+=1*t.deltaY)})}_removeEventHandlers(){window.removeEventListener("resize",this._handleWindowResize),this.el.removeEventListener("click",this._handleTabClick)}_handleWindowResize=()=>{this._setTabsAndTabWidth(),0!==this._tabWidth&&0!==this._tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this._activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this._activeTabLink)+"px")};_handleTabClick=t=>{let e=t.target;if(!e)return;let i=e.parentElement;for(;i&&!i.classList.contains("tab");)e=e.parentElement,i=i.parentElement;if(!e||!i.classList.contains("tab"))return;if(i.classList.contains("disabled"))return void t.preventDefault();if(e.hasAttribute("target"))return;this._activeTabLink.classList.remove("active");const s=this._content;this._activeTabLink=e,e.hash&&(this._content=document.querySelector(e.hash)),this._tabLinks=this.el.querySelectorAll("li.tab > a"),this._activeTabLink.classList.add("active");const n=this._index;this._index=Math.max(Array.from(this._tabLinks).indexOf(e),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this._index,()=>{"function"==typeof this.options.onShow&&this.options.onShow.call(this,this._content)}):this._content&&(this._content.style.display="block",this._content.classList.add("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this._content),s&&s!==this._content&&(s.style.display="none",s.classList.remove("active"))),this._setTabsAndTabWidth(),this._animateIndicator(n),t.preventDefault()};_createIndicator(){const t=document.createElement("li");t.classList.add("indicator"),this.el.appendChild(t),this._indicator=t,this._indicator.style.left=this._calcLeftPos(this._activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this._activeTabLink)+"px"}_setupActiveTabLink(){if(this._activeTabLink=Array.from(this._tabLinks).find(t=>t.getAttribute("href")===location.hash),!this._activeTabLink){let t=this.el.querySelector("li.tab a.active");t||(t=this.el.querySelector("li.tab a")),this._activeTabLink=t}Array.from(this._tabLinks).forEach(t=>t.classList.remove("active")),this._activeTabLink.classList.add("active"),this._index=Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink),0),this._activeTabLink&&this._activeTabLink.hash&&(this._content=document.querySelector(this._activeTabLink.hash),this._content&&this._content.classList.add("active"))}_setupSwipeableTabs(){window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);const t=[];this._tabLinks.forEach(e=>{if(e.hash){const i=document.querySelector(e.hash);i.classList.add("carousel-item"),t.push(i)}});const e=document.createElement("div");e.classList.add("tabs-content","carousel","carousel-slider"),t[0].parentElement.insertBefore(e,t[0]),t.forEach(t=>{e.appendChild(t),t.style.display=""});const i=this._activeTabLink.parentElement,s=Array.from(i.parentNode.children).indexOf(i);this._tabsCarousel=p.init(e,{fullWidth:!0,noWrap:!0,onCycleTo:t=>{const e=this._index;this._index=Array.from(t.parentNode.children).indexOf(t),this._activeTabLink.classList.remove("active"),this._activeTabLink=Array.from(this._tabLinks)[this._index],this._activeTabLink.classList.add("active"),this._animateIndicator(e),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this._content)}}),this._tabsCarousel.set(s)}_teardownSwipeableTabs(){const t=this._tabsCarousel.el;this._tabsCarousel.destroy(),t.append(t.parentElement),t.remove()}_setupNormalTabs(){Array.from(this._tabLinks).forEach(t=>{if(t!==this._activeTabLink&&t.hash){const e=document.querySelector(t.hash);e&&(e.style.display="none")}})}_teardownNormalTabs(){this._tabLinks.forEach(t=>{if(t.hash){const e=document.querySelector(t.hash);e&&(e.style.display="")}})}_setTabsAndTabWidth(){this._tabsWidth=this.el.getBoundingClientRect().width,this._tabWidth=Math.max(this._tabsWidth,this.el.scrollWidth)/this._tabLinks.length}_calcRightPos(t){return Math.ceil(this._tabsWidth-t.offsetLeft-t.getBoundingClientRect().width)}_calcLeftPos(t){return Math.floor(t.offsetLeft)}updateTabIndicator(){this._setTabsAndTabWidth(),this._animateIndicator(this._index)}_animateIndicator(t){let e=0,i=0;this._index-t>=0?e=90:i=90,this._indicator.style.transition=`\n left ${this.options.duration}ms ease-out ${e}ms,\n right ${this.options.duration}ms ease-out ${i}ms`,this._indicator.style.left=this._calcLeftPos(this._activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this._activeTabLink)+"px"}select(t){const e=Array.from(this._tabLinks).find(e=>e.getAttribute("href")==="#"+t);e&&e.click()}}const B={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,autoSubmit:!0,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},twelveHour:!0,vibrate:!0,onSelect:null,onInputInteraction:null,onDone:null,onCancel:null,displayPlugin:null,displayPluginOptions:null};class q extends i{id;containerEl;plate;digitalClock;inputHours;inputMinutes;x0;y0;moved;dx;dy;currentView;hand;minutesView;hours;minutes;time;amOrPm;static _template;vibrate;_canvas;hoursView;spanAmPm;footer;_amBtn;_pmBtn;bg;bearing;g;toggleViewTimer;vibrateTimer;displayPlugin;constructor(t,i){super(t,i,q),this.el.M_Timepicker=this,this.options={...q.defaults,...i},this.id=e.guid(),this._insertHTMLIntoDOM(),this._setupVariables(),this._setupEventHandlers(),this._clockSetup(),this.options.displayPlugin&&this._setupDisplayPlugin(),this._pickerSetup()}static get defaults(){return B}static init(t,e={}){return super.init(t,e,q)}static _addLeadingZero(t){return(t<10?"0":"")+t}static _createSVGEl(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}static _Pos(t){return t.type.startsWith("touch")&&t.targetTouches.length>=1?{x:t.targetTouches[0].clientX,y:t.targetTouches[0].clientY}:{x:t.clientX,y:t.clientY}}static getInstance(t){return t.M_Timepicker}destroy(){this._removeEventHandlers(),this.containerEl.remove(),this.el.M_Timepicker=void 0}_setupEventHandlers(){this.el.addEventListener("click",this._handleInputClick),this.el.addEventListener("keydown",this._handleInputKeydown),this.plate.addEventListener("mousedown",this._handleClockClickStart),this.plate.addEventListener("touchstart",this._handleClockClickStart),this.digitalClock.addEventListener("keyup",this._inputFromTextField),this.inputHours.addEventListener("focus",()=>this.showView("hours")),this.inputHours.addEventListener("focusout",()=>this.formatHours()),this.inputMinutes.addEventListener("focus",()=>this.showView("minutes")),this.inputMinutes.addEventListener("focusout",()=>this.formatMinutes())}_removeEventHandlers(){this.el.removeEventListener("click",this._handleInputClick),this.el.removeEventListener("keydown",this._handleInputKeydown)}_handleInputClick=()=>{this.inputHours.focus(),"function"==typeof this.options.onInputInteraction&&this.options.onInputInteraction.call(this),this.displayPlugin&&this.displayPlugin.show()};_handleInputKeydown=t=>{e.keys.ENTER.includes(t.key)&&(t.preventDefault(),this.inputHours.focus(),"function"==typeof this.options.onInputInteraction&&this.options.onInputInteraction.call(this),this.displayPlugin&&this.displayPlugin.show())};_handleTimeInputEnterKey=t=>{e.keys.ENTER.includes(t.key)&&(t.preventDefault(),this._inputFromTextField())};_handleClockClickStart=t=>{t.preventDefault();const e=this.plate.getBoundingClientRect(),i=e.left,s=e.top;this.x0=i+this.options.dialRadius,this.y0=s+this.options.dialRadius,this.moved=!1;const n=q._Pos(t);this.dx=n.x-this.x0,this.dy=n.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMove),document.addEventListener("touchmove",this._handleDocumentClickMove),document.addEventListener("mouseup",this._handleDocumentClickEnd),document.addEventListener("touchend",this._handleDocumentClickEnd)};_handleDocumentClickMove=t=>{t.preventDefault();const e=q._Pos(t),i=e.x-this.x0,s=e.y-this.y0;this.moved=!0,this.setHand(i,s,!1)};_handleDocumentClickEnd=t=>{t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEnd),document.removeEventListener("touchend",this._handleDocumentClickEnd);const e=q._Pos(t),i=e.x-this.x0,s=e.y-this.y0;this.moved&&i===this.dx&&s===this.dy&&this.setHand(i,s),"hours"===this.currentView?(this.inputMinutes.focus(),this.showView("minutes",this.options.duration/2)):setTimeout(()=>{this.options.autoSubmit&&this.done()},this.options.duration/2),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMove),document.removeEventListener("touchmove",this._handleDocumentClickMove)};_insertHTMLIntoDOM(){const t=document.createElement("template");t.innerHTML=q._template.trim(),this.containerEl=t.content.firstChild,this.containerEl.id="container-"+this.id;const e=this.options.container,i=e instanceof HTMLElement?e:document.querySelector(e);this.options.container&&i?i.append(this.containerEl):this.el.parentElement.appendChild(this.containerEl)}_setupVariables(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.containerEl.querySelector(".timepicker-canvas"),this.plate=this.containerEl.querySelector(".timepicker-plate"),this.digitalClock=this.containerEl.querySelector(".timepicker-display-column"),this.hoursView=this.containerEl.querySelector(".timepicker-hours"),this.minutesView=this.containerEl.querySelector(".timepicker-minutes"),this.inputHours=this.containerEl.querySelector(".timepicker-input-hours"),this.inputMinutes=this.containerEl.querySelector(".timepicker-input-minutes"),this.spanAmPm=this.containerEl.querySelector(".timepicker-span-am-pm"),this.footer=this.containerEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}_pickerSetup(){this.options.showClearBtn&&e.createButton(this.footer,this.options.i18n.clear,["timepicker-clear"],!0,this.clear),this.options.autoSubmit||e.createConfirmationContainer(this.footer,this.options.i18n.done,this.options.i18n.cancel,this.confirm,this.cancel),this._updateTimeFromInput(),this.showView("hours")}_setupDisplayPlugin(){"docked"===this.options.displayPlugin&&(this.displayPlugin=b.init(this.el,this.containerEl,this.options.displayPluginOptions)),"modal"===this.options.displayPlugin&&(this.displayPlugin=C.init(this.el,this.containerEl,{...this.options.displayPluginOptions,classList:["timepicker-modal"]}),this.footer.remove(),this.footer=this.displayPlugin.footer)}_clockSetup(){this.options.twelveHour&&(this._amBtn=document.createElement("div"),this._amBtn.classList.add("am-btn","btn"),this._amBtn.innerText="AM",this._amBtn.tabIndex=0,this._amBtn.addEventListener("click",this._handleAmPmClick),this._amBtn.addEventListener("keypress",this._handleAmPmKeypress),this.spanAmPm.appendChild(this._amBtn),this._pmBtn=document.createElement("div"),this._pmBtn.classList.add("pm-btn","btn"),this._pmBtn.innerText="PM",this._pmBtn.tabIndex=0,this._pmBtn.addEventListener("click",this._handleAmPmClick),this._pmBtn.addEventListener("keypress",this._handleAmPmKeypress),this.spanAmPm.appendChild(this._pmBtn)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}_buildSVGClock(){const t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,s=q._createSVGEl("svg");s.setAttribute("class","timepicker-svg"),s.setAttribute("width",i.toString()),s.setAttribute("height",i.toString());const n=q._createSVGEl("g");n.setAttribute("transform","translate("+t+","+t+")");const o=q._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx","0"),o.setAttribute("cy","0"),o.setAttribute("r","4");const a=q._createSVGEl("line");a.setAttribute("x1","0"),a.setAttribute("y1","0");const l=q._createSVGEl("circle");l.setAttribute("class","timepicker-canvas-bg"),l.setAttribute("r",e.toString()),n.appendChild(a),n.appendChild(l),n.appendChild(o),s.appendChild(n),this._canvas.appendChild(s),this.hand=a,this.bg=l,this.bearing=o,this.g=n}_buildHoursView(){if(this.options.twelveHour)for(let t=1;t<13;t+=1){const e=t/6*Math.PI,i=this.options.outerRadius;this._buildHoursTick(t,e,i)}else for(let t=0;t<24;t+=1){const e=t/6*Math.PI,i=t>0&&t<13?this.options.innerRadius:this.options.outerRadius;this._buildHoursTick(t,e,i)}}_buildHoursTick(t,e,i){const s=document.createElement("div");s.classList.add("timepicker-tick"),s.style.left=this.options.dialRadius+Math.sin(e)*i-this.options.tickRadius+"px",s.style.top=this.options.dialRadius-Math.cos(e)*i-this.options.tickRadius+"px",s.innerHTML=0===t?"00":t.toString(),this.hoursView.appendChild(s)}_buildMinutesView(){const t=document.createElement("div");t.classList.add("timepicker-tick");for(let e=0;e<60;e+=5){const i=t.cloneNode(!0),s=e/30*Math.PI;i.style.left=this.options.dialRadius+Math.sin(s)*this.options.outerRadius-this.options.tickRadius+"px",i.style.top=this.options.dialRadius-Math.cos(s)*this.options.outerRadius-this.options.tickRadius+"px",i.innerHTML=q._addLeadingZero(e),this.minutesView.appendChild(i)}}_handleAmPmClick=t=>{this._handleAmPmInteraction(t.target)};_handleAmPmKeypress=t=>{e.keys.ENTER.includes(t.key)&&this._handleAmPmInteraction(t.target)};_handleAmPmInteraction=t=>{this.amOrPm=t.classList.contains("am-btn")?"AM":"PM",this._updateAmPmView()};_updateAmPmView(){this.options.twelveHour&&("PM"===this.amOrPm?(this._amBtn.classList.remove("filled"),this._pmBtn.classList.add("filled")):"AM"===this.amOrPm&&(this._amBtn.classList.add("filled"),this._pmBtn.classList.remove("filled")))}_updateTimeFromInput(){let t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(t[1].toUpperCase().indexOf("AM")>0?this.amOrPm="AM":this.amOrPm="PM",t[1]=t[1].replace("AM","").replace("PM","")),"now"===t[0]){const e=new Date(+new Date+this.options.fromNow);t=[e.getHours().toString(),e.getMinutes().toString()],this.options.twelveHour&&(this.amOrPm=parseInt(t[0])>=12&&parseInt(t[0])<24?"PM":"AM")}this.hours=+t[0]||0,this.minutes=+t[1]||0,this.inputHours.value=q._addLeadingZero(this.hours),this.inputMinutes.value=q._addLeadingZero(this.minutes),this._updateAmPmView()}showView=(t,e=null)=>{"minutes"===t&&getComputedStyle(this.hoursView).visibility;const i="hours"===t,s=i?this.hoursView:this.minutesView,n=i?this.minutesView:this.hoursView;this.currentView=t,n.classList.add("timepicker-dial-out"),s.style.visibility="visible",s.classList.remove("timepicker-dial-out"),this.resetClock(e),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(()=>{n.style.visibility="hidden"},this.options.duration)};resetClock(t){const e=this.currentView,i=this[e],s="hours"===e,n=i*(Math.PI/(s?6:30)),o=s&&i>0&&i<13?this.options.innerRadius:this.options.outerRadius,a=Math.sin(n)*o,l=-Math.cos(n)*o;t?(this._canvas?.classList.add("timepicker-canvas-out"),setTimeout(()=>{this._canvas?.classList.remove("timepicker-canvas-out"),this.setHand(a,l)},t)):this.setHand(a,l)}_inputFromTextField=()=>{const t="hours"===this.currentView;if(t&&""!==this.inputHours.value){const e=parseInt(this.inputHours.value);e>0&&e<(this.options.twelveHour?13:24)?this.hours=e:this.setHoursDefault(),this.drawClockFromTimeInput(this.hours,t)}else if(!t&&""!==this.inputMinutes.value){const e=parseInt(this.inputMinutes.value);e>=0&&e<60?this.minutes=e:(this.minutes=(new Date).getMinutes(),this.inputMinutes.value=this.minutes.toString()),this.drawClockFromTimeInput(this.minutes,t)}};drawClockFromTimeInput(t,e){const i=t*(Math.PI/(e?6:30));let s;s=this.options.twelveHour?this.options.outerRadius:e&&t>0&&t<13?this.options.innerRadius:this.options.outerRadius,this.setClockAttributes(i,s)}setHand(t,e,i=!1){const s="hours"===this.currentView,n=Math.PI/(s||i?6:30),o=Math.sqrt(t*t+e*e),a=s&&o<(this.options.outerRadius+this.options.innerRadius)/2;let l=Math.atan2(t,-e),r=a?this.options.innerRadius:this.options.outerRadius;this.options.twelveHour&&(r=this.options.outerRadius),l<0&&(l=2*Math.PI+l);let h=Math.round(l/n);l=h*n,this.options.twelveHour?s?0===h&&(h=12):(i&&(h*=5),60===h&&(h=0)):s?(12===h&&(h=0),h=a?0===h?12:h:0===h?0:h+12):(i&&(h*=5),60===h&&(h=0)),this[this.currentView]!==h&&this.vibrate&&this.options.vibrate&&(this.vibrateTimer||(navigator[this.vibrate](10),this.vibrateTimer=setTimeout(()=>{this.vibrateTimer=null},100))),this[this.currentView]=h,s?this.inputHours.value=q._addLeadingZero(h):this.inputMinutes.value=q._addLeadingZero(h),this.setClockAttributes(l,r)}setClockAttributes(t,e){const i=Math.sin(t)*(e-this.options.tickRadius),s=-Math.cos(t)*(e-this.options.tickRadius),n=Math.sin(t)*e,o=-Math.cos(t)*e;this.hand.setAttribute("x2",i.toString()),this.hand.setAttribute("y2",s.toString()),this.bg.setAttribute("cx",n.toString()),this.bg.setAttribute("cy",o.toString())}formatHours(){""==this.inputHours.value&&this.setHoursDefault(),this.inputHours.value=q._addLeadingZero(Number(this.inputHours.value))}formatMinutes(){""==this.inputMinutes.value&&(this.minutes=(new Date).getMinutes()),this.inputMinutes.value=q._addLeadingZero(Number(this.inputMinutes.value))}setHoursDefault(){this.hours=(new Date).getHours(),this.inputHours.value=(this.hours%(this.options.twelveHour?12:24)).toString()}done=(t=null)=>{const e=this.el.value;let i=t?"":q._addLeadingZero(this.hours)+":"+q._addLeadingZero(this.minutes);this.time=i,!t&&this.options.twelveHour&&(i=`${i} ${this.amOrPm}`),this.el.value=i,i!==e&&this.el.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0,composed:!0}))};confirm=()=>{this.done(),this.displayPlugin&&this.displayPlugin.hide(),"function"==typeof this.options.onDone&&this.options.onDone.call(this)};cancel=()=>{this.displayPlugin&&this.displayPlugin.hide(),"function"==typeof this.options.onCancel&&this.options.onCancel.call(this)};clear=()=>{this.done(!0)};open(){return console.warn("Timepicker.close() is deprecated. Remove this method and wrap in modal yourself."),this}close(){return console.warn("Timepicker.close() is deprecated. Remove this method and wrap in modal yourself."),this}static{q._template='
\n
\n
\n
\n
\n \n
\n
\n :\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
'}}const F={text:"",displayLength:4e3,inDuration:300,outDuration:375,classes:"",completeCallback:null,activationPercent:.8};class V{el;timeRemaining;panning;options;message;counterInterval;wasSwiped;startingXPos;xPos;time;deltaX;velocityX;static _toasts;static _container;static _draggedToast;constructor(t){this.options={...V.defaults,...t},this.message=this.options.text,this.panning=!1,this.timeRemaining=this.options.displayLength,0===V._toasts.length&&V._createContainer(),V._toasts.push(this);const e=this._createToast();e.M_Toast=this,this.el=e,this._animateIn(),this._setTimer()}static get defaults(){return F}static getInstance(t){return t.M_Toast}static _createContainer(){const t=document.createElement("div");t.setAttribute("id","toast-container"),t.addEventListener("touchstart",V._onDragStart),t.addEventListener("touchmove",V._onDragMove),t.addEventListener("touchend",V._onDragEnd),t.addEventListener("mousedown",V._onDragStart),document.addEventListener("mousemove",V._onDragMove),document.addEventListener("mouseup",V._onDragEnd),document.body.appendChild(t),V._container=t}static _removeContainer(){document.removeEventListener("mousemove",V._onDragMove),document.removeEventListener("mouseup",V._onDragEnd),V._container&&(V._container.remove(),V._container=null)}static _onDragStart(t){if(t.target&&t.target.closest(".toast")){const e=t.target.closest(".toast").M_Toast;e.panning=!0,V._draggedToast=e,e.el.classList.add("panning"),e.el.style.transition="",e.startingXPos=V._xPos(t),e.time=Date.now(),e.xPos=V._xPos(t)}}static _onDragMove(t){if(V._draggedToast){t.preventDefault();const e=V._draggedToast;e.deltaX=Math.abs(e.xPos-V._xPos(t)),e.xPos=V._xPos(t),e.velocityX=e.deltaX/(Date.now()-e.time),e.time=Date.now();const i=e.xPos-e.startingXPos,s=e.el.offsetWidth*e.options.activationPercent;e.el.style.transform=`translateX(${i}px)`,e.el.style.opacity=(1-Math.abs(i/s)).toString()}}static _onDragEnd(){if(V._draggedToast){const t=V._draggedToast;t.panning=!1,t.el.classList.remove("panning");const e=t.xPos-t.startingXPos,i=t.el.offsetWidth*t.options.activationPercent;Math.abs(e)>i||t.velocityX>1?(t.wasSwiped=!0,t.dismiss()):(t.el.style.transition="transform .2s, opacity .2s",t.el.style.transform="",t.el.style.opacity=""),V._draggedToast=null}}static _xPos(t){return t.type.startsWith("touch")&&t.targetTouches.length>=1?t.targetTouches[0].clientX:t.clientX}static dismissAll(){for(const t in V._toasts)V._toasts[t].dismiss()}_createToast(){let t=this.options.toastId?document.getElementById(this.options.toastId):document.createElement("div");if(t instanceof HTMLTemplateElement){const e=t.content.cloneNode(!0);t=e.firstElementChild}return t.classList.add("toast"),t.setAttribute("role","alert"),t.setAttribute("aria-live","assertive"),t.setAttribute("aria-atomic","true"),this.options.classes.length>0&&t.classList.add(...this.options.classes.split(" ")),this.message&&(t.innerText=this.message),V._container&&V._container.appendChild(t),t}_animateIn(){this.el.style.display="",this.el.style.opacity="0",this.el.style.transition=`\n top ${this.options.inDuration}ms ease,\n opacity ${this.options.inDuration}ms ease\n `,setTimeout(()=>{this.el.style.top="0",this.el.style.opacity="1"},1)}_setTimer(){this.timeRemaining!==1/0&&(this.counterInterval=setInterval(()=>{this.panning||(this.timeRemaining-=20),this.timeRemaining<=0&&this.dismiss()},20))}dismiss(){clearInterval(this.counterInterval);const t=this.el.offsetWidth*this.options.activationPercent;this.wasSwiped&&(this.el.style.transition="transform .05s, opacity .05s",this.el.style.transform=`translateX(${t}px)`,this.el.style.opacity="0"),this.el.style.transition=`\n margin ${this.options.outDuration}ms ease,\n opacity ${this.options.outDuration}ms ease`,setTimeout(()=>{this.el.style.opacity="0",this.el.style.marginTop="-40px"},1),setTimeout(()=>{"function"==typeof this.options.completeCallback&&this.options.completeCallback(),this.el.id!=this.options.toastId&&(this.el.remove(),V._toasts.splice(V._toasts.indexOf(this),1),0===V._toasts.length&&V._removeContainer())},this.options.outDuration)}static{V._toasts=[],V._container=null,V._draggedToast=null}}const $={exitDelay:200,enterDelay:0,text:"",margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10,opacity:1};class N extends i{isOpen;isHovered;isFocused;tooltipEl;_exitDelayTimeout;_enterDelayTimeout;xMovement;yMovement;constructor(t,e){super(t,e,N),this.el.M_Tooltip=this,this.options={...N.defaults,...this._getAttributeOptions(),...e},this.isOpen=!1,this.isHovered=!1,this.isFocused=!1,this._appendTooltipEl(),this._setupEventHandlers()}static get defaults(){return $}static init(t,e={}){return super.init(t,e,N)}static getInstance(t){return t.M_Tooltip}destroy(){this.tooltipEl.remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}_appendTooltipEl(){this.tooltipEl=document.createElement("div"),this.tooltipEl.classList.add("material-tooltip");const t=this.options.tooltipId?document.getElementById(this.options.tooltipId):document.createElement("div");this.tooltipEl.append(t),t.style.display="",t.classList.add("tooltip-content"),this._setTooltipContent(t),this.tooltipEl.appendChild(t),document.body.appendChild(this.tooltipEl)}_setTooltipContent(t){this.options.tooltipId||(t.innerText=this.options.text)}_updateTooltipContent(){this._setTooltipContent(this.tooltipEl.querySelector(".tooltip-content"))}_setupEventHandlers(){this.el.addEventListener("mouseenter",this._handleMouseEnter),this.el.addEventListener("mouseleave",this._handleMouseLeave),this.el.addEventListener("focus",this._handleFocus,!0),this.el.addEventListener("blur",this._handleBlur,!0)}_removeEventHandlers(){this.el.removeEventListener("mouseenter",this._handleMouseEnter),this.el.removeEventListener("mouseleave",this._handleMouseLeave),this.el.removeEventListener("focus",this._handleFocus,!0),this.el.removeEventListener("blur",this._handleBlur,!0)}open=t=>{this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options={...this.options,...this._getAttributeOptions()},this._updateTooltipContent(),this._setEnterDelayTimeout(t))};close=()=>{this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())};_setExitDelayTimeout(){clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(()=>{this.isHovered||this.isFocused||this._animateOut()},this.options.exitDelay)}_setEnterDelayTimeout(t){clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(()=>{(this.isHovered||this.isFocused||t)&&this._animateIn()},this.options.enterDelay)}_positionTooltip(){const t=this.tooltipEl,i=this.el,s=i.offsetHeight,n=i.offsetWidth,o=t.offsetHeight,a=t.offsetWidth,l=this.options.margin;this.xMovement=0,this.yMovement=0;let r=i.getBoundingClientRect().top+e.getDocumentScrollTop(),h=i.getBoundingClientRect().left+e.getDocumentScrollLeft();"top"===this.options.position?(r+=-o-l,h+=n/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(r+=s/2-o/2,h+=n+l,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(r+=s/2-o/2,h+=-a-l,this.xMovement=-this.options.transitionMovement):(r+=s+l,h+=n/2-a/2,this.yMovement=this.options.transitionMovement);const d=this._repositionWithinScreen(h,r,a,o);t.style.top=d.y+"px",t.style.left=d.x+"px"}_repositionWithinScreen(t,i,s,n){const o=e.getDocumentScrollLeft(),a=e.getDocumentScrollTop();let l=t-o,r=i-a;const h={left:l,top:r,width:s,height:n},d=this.options.margin+this.options.transitionMovement,c=e.checkWithinContainer(document.body,h,d);return c.left?l=d:c.right&&(l-=l+s-window.innerWidth),c.top?r=d:c.bottom&&(r-=r+n-window.innerHeight),{x:l+o,y:r+a}}_animateIn(){this._positionTooltip(),this.tooltipEl.style.visibility="visible";const t=this.options.inDuration;this.tooltipEl.style.transition=`\n transform ${t}ms ease-out,\n opacity ${t}ms ease-out`,setTimeout(()=>{this.tooltipEl.style.transform=`translateX(${this.xMovement}px) translateY(${this.yMovement}px)`,this.tooltipEl.style.opacity=(this.options.opacity||1).toString()},1)}_animateOut(){const t=this.options.outDuration;this.tooltipEl.style.transition=`\n transform ${t}ms ease-out,\n opacity ${t}ms ease-out`,setTimeout(()=>{this.tooltipEl.style.transform="translateX(0px) translateY(0px)",this.tooltipEl.style.opacity="0"},1)}_handleMouseEnter=()=>{this.isHovered=!0,this.isFocused=!1,this.open(!1)};_handleMouseLeave=()=>{this.isHovered=!1,this.isFocused=!1,this.close()};_handleFocus=()=>{e.tabPressed&&(this.isFocused=!0,this.open(!1))};_handleBlur=()=>{this.isFocused=!1,this.close()};_getAttributeOptions(){const t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-tooltip-id"),s=this.el.getAttribute("data-position");return e&&(t.text=e),s&&(t.position=s),i&&(t.tooltipId=i),t}}const z={};class X extends i{_mousedown;value;thumb;constructor(t,e){super(t,e,X),this.el.M_Range=this,this.options={...X.defaults,...e},this._mousedown=!1,this._setupThumb(),this._setupEventHandlers()}static get defaults(){return z}static init(t,e={}){return super.init(t,e,X)}static getInstance(t){return t.M_Range}destroy(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}_setupEventHandlers(){this.el.addEventListener("change",this._handleRangeChange),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstart),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstart),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmove),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmove),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmove),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchend),this.el.addEventListener("touchend",this._handleRangeMouseupTouchend),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleave),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleave),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleave)}_removeEventHandlers(){this.el.removeEventListener("change",this._handleRangeChange),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstart),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstart),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmove),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmove),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmove),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchend),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchend),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleave),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleave),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleave)}_handleRangeChange=()=>{this.value.innerHTML=this.el.value,this.thumb.classList.contains("active")||this._showRangeBubble();const t=this._calcRangeOffset();this.thumb.classList.add("active"),this.thumb.style.left=t+"px"};_handleRangeMousedownTouchstart=t=>{if(this.value.innerHTML=this.el.value,this._mousedown=!0,this.el.classList.add("active"),this.thumb.classList.contains("active")||this._showRangeBubble(),"input"!==t.type){const t=this._calcRangeOffset();this.thumb.classList.add("active"),this.thumb.style.left=t+"px"}};_handleRangeInputMousemoveTouchmove=()=>{if(this._mousedown){this.thumb.classList.contains("active")||this._showRangeBubble();const t=this._calcRangeOffset();this.thumb.classList.add("active"),this.thumb.style.left=t+"px",this.value.innerHTML=this.el.value}};_handleRangeMouseupTouchend=()=>{this._mousedown=!1,this.el.classList.remove("active")};_handleRangeBlurMouseoutTouchleave=()=>{if(!this._mousedown){const t=7+parseInt(getComputedStyle(this.el).paddingLeft)+"px";if(this.thumb.classList.contains("active")){const e=100;this.thumb.style.transition="none",setTimeout(()=>{this.thumb.style.transition=`\n height ${e}ms ease,\n width ${e}ms ease,\n top ${e}ms ease,\n margin ${e}ms ease\n `,this.thumb.style.height="0",this.thumb.style.width="0",this.thumb.style.top="0",this.thumb.style.marginLeft=t},1)}this.thumb.classList.remove("active")}};_setupThumb(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),this.thumb.classList.add("thumb"),this.value.classList.add("value"),this.thumb.append(this.value),this.el.after(this.thumb)}_removeThumb(){this.thumb.remove()}_showRangeBubble(){const t=-7+parseInt(getComputedStyle(this.thumb.parentElement).paddingLeft)+"px";this.thumb.style.transition="\n height 300ms ease,\n width 300ms ease,\n top 300ms ease,\n margin 300ms ease\n ",this.thumb.style.height="30px",this.thumb.style.width="30px",this.thumb.style.top="-30px",this.thumb.style.marginLeft=t}_calcRangeOffset(){const t=this.el.getBoundingClientRect().width-15,e=parseFloat(this.el.getAttribute("max"))||100,i=parseFloat(this.el.getAttribute("min"))||0;return(parseFloat(this.el.value)-i)/(e-i)*t}static Init(){"undefined"!=typeof document&&X.init(document?.querySelectorAll("input[type=range]"),{})}}const K={onOpen:null,onClose:null};class Y extends i{isOpen;static _taptargets;wrapper;originEl;waveEl;contentEl;constructor(t,e){super(t,e,Y),this.el.M_TapTarget=this,this.options={...Y.defaults,...e},this.isOpen=!1,this.originEl=document.querySelector(`#${t.dataset.target}`),this.originEl.tabIndex=0,this._setup(),this._calculatePositioning(),this._setupEventHandlers(),Y._taptargets.push(this)}static get defaults(){return K}static init(t,e={}){return super.init(t,e,Y)}static getInstance(t){return t.M_TapTarget}destroy(){this._removeEventHandlers(),this.el.M_TapTarget=void 0;const t=Y._taptargets.indexOf(this);t>=0&&Y._taptargets.splice(t,1)}_setupEventHandlers(){this.originEl.addEventListener("click",this._handleTargetToggle),this.originEl.addEventListener("keypress",this._handleKeyboardInteraction,!0),window.addEventListener("resize",this._handleThrottledResize)}_removeEventHandlers(){this.originEl.removeEventListener("click",this._handleTargetToggle),this.originEl.removeEventListener("keypress",this._handleKeyboardInteraction,!0),window.removeEventListener("resize",this._handleThrottledResize)}_handleThrottledResize=()=>e.throttle(this._handleResize,200).bind(this);_handleKeyboardInteraction=t=>{e.keys.ENTER.includes(t.key)&&this._handleTargetToggle()};_handleTargetToggle=()=>{this.isOpen?this.close():this.open()};_handleResize=()=>{this._calculatePositioning()};_handleDocumentClick=t=>{t.target.closest(`#${this.el.dataset.target}`)===this.originEl||t.target.closest(".tap-target-wrapper")||this.close()};_setup(){this.wrapper=this.el.parentElement,this.waveEl=this.wrapper.querySelector(".tap-target-wave"),this.el.parentElement.ariaExpanded="false",this.originEl.style.zIndex="1002",this.contentEl=this.el.querySelector(".tap-target-content"),this.wrapper.classList.contains(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.el.before(this.wrapper),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.wrapper.append(this.waveEl))}_offset(t){const e=t.getBoundingClientRect(),i=document.documentElement;return{top:e.top+window.pageYOffset-i.clientTop,left:e.left+window.pageXOffset-i.clientLeft}}_calculatePositioning(){let t="fixed"===getComputedStyle(this.originEl).position;if(!t){let e=this.originEl;const i=[];for(;(e=e.parentNode)&&e!==document;)i.push(e);for(let e=0;eh,u=n<=d,m=n>d,v=o>=.25*a&&o<=.75*a,g=this.el.offsetWidth,_=this.el.offsetHeight,y=n+s/2-_/2,f=o+i/2-g/2,E=t?"fixed":"absolute",w=v?g:g/2+i,b=_/2,L=u?_/2:0,C=c&&!v?g/2-i:0,k=i,T=m?"bottom":"top",D=2*i,x=D,S=_/2-x/2,I=g/2-D/2;this.wrapper.style.top=u?y+"px":"",this.wrapper.style.right=p?a-f-g-r+"px":"",this.wrapper.style.bottom=m?l-y-_+"px":"",this.wrapper.style.left=c?f+"px":"",this.wrapper.style.position=E,this.contentEl.style.width=w+"px",this.contentEl.style.height=b+"px",this.contentEl.style.top=L+"px",this.contentEl.style.right="0px",this.contentEl.style.bottom="0px",this.contentEl.style.left=C+"px",this.contentEl.style.padding=k+"px",this.contentEl.style.verticalAlign=T,this.waveEl.style.top=S+"px",this.waveEl.style.left=I+"px",this.waveEl.style.width=D+"px",this.waveEl.style.height=x+"px"}open=()=>{this.isOpen||("function"==typeof this.options.onOpen&&this.options.onOpen.call(this,this.originEl),this.isOpen=!0,this.wrapper.classList.add("open"),this.wrapper.ariaExpanded="true",document.body.addEventListener("click",this._handleDocumentClick,!0),document.body.addEventListener("keypress",this._handleDocumentClick,!0),document.body.addEventListener("touchend",this._handleDocumentClick))};close=()=>{this.isOpen&&("function"==typeof this.options.onClose&&this.options.onClose.call(this,this.originEl),this.isOpen=!1,this.wrapper.classList.remove("open"),this.wrapper.ariaExpanded="false",document.body.removeEventListener("click",this._handleDocumentClick,!0),document.body.removeEventListener("keypress",this._handleDocumentClick,!0),document.body.removeEventListener("touchend",this._handleDocumentClick))};static{Y._taptargets=[]}}const j=Object.freeze({});class U extends i{counterEl;isInvalid;isValidLength;constructor(t,e){super(t,{},U),this.el.M_CharacterCounter=this,this.options={...U.defaults,...e},this.isInvalid=!1,this.isValidLength=!1,this._setupCounter(),this._setupEventHandlers()}static get defaults(){return j}static init(t,e={}){return super.init(t,e,U)}static getInstance(t){return t.M_CharacterCounter}destroy(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}_setupEventHandlers(){this.el.addEventListener("focus",this.updateCounter,!0),this.el.addEventListener("input",this.updateCounter,!0)}_removeEventHandlers(){this.el.removeEventListener("focus",this.updateCounter,!0),this.el.removeEventListener("input",this.updateCounter,!0)}_setupCounter(){this.counterEl=document.createElement("span"),this.counterEl.classList.add("character-counter"),this.counterEl.style.float="right",this.counterEl.style.fontSize="12px",this.counterEl.style.height="1",this.el.parentElement.appendChild(this.counterEl)}_removeCounter(){this.counterEl.remove()}updateCounter=()=>{const t=parseInt(this.el.getAttribute("maxlength")),e=this.el.value.length;this.isValidLength=e<=t;let i=e.toString();t&&(i+="/"+t,this._validateInput()),this.counterEl.innerHTML=i};_validateInput(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.el.classList.remove("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.el.classList.remove("valid"),this.el.classList.add("invalid"))}}const Z={responsiveThreshold:0};class G extends i{_enabled;_img;static _parallaxes=[];static _handleScrollThrottled;static _handleWindowResizeThrottled;constructor(t,e){super(t,e,G),this.el.M_Parallax=this,this.options={...G.defaults,...e},this._enabled=window.innerWidth>this.options.responsiveThreshold,this._img=this.el.querySelector("img"),this._updateParallax(),this._setupEventHandlers(),this._setupStyles(),G._parallaxes.push(this)}static get defaults(){return Z}static init(t,e={}){return super.init(t,e,G)}static getInstance(t){return t.M_Parallax}destroy(){G._parallaxes.splice(G._parallaxes.indexOf(this),1),this._img.style.transform="",this._removeEventHandlers(),this.el.M_Parallax=void 0}static _handleScroll(){for(let t=0;te.options.responsiveThreshold}}_setupEventHandlers(){this._img.addEventListener("load",this._handleImageLoad),0===G._parallaxes.length&&(G._handleScrollThrottled||(G._handleScrollThrottled=e.throttle(G._handleScroll,5)),G._handleWindowResizeThrottled||(G._handleWindowResizeThrottled=e.throttle(G._handleWindowResize,5)),window.addEventListener("scroll",G._handleScrollThrottled),window.addEventListener("resize",G._handleWindowResizeThrottled))}_removeEventHandlers(){this._img.removeEventListener("load",this._handleImageLoad),0===G._parallaxes.length&&(window.removeEventListener("scroll",G._handleScrollThrottled),window.removeEventListener("resize",G._handleWindowResizeThrottled))}_setupStyles(){this._img.style.opacity="1"}_handleImageLoad=()=>{this._updateParallax()};_offset(t){const e=t.getBoundingClientRect(),i=document.documentElement;return{top:e.top+window.scrollY-i.clientTop,left:e.left+window.scrollX-i.clientLeft}}_updateParallax(){const t=this.el.getBoundingClientRect().height>0?this.el.parentElement.offsetHeight:500,i=this._img.offsetHeight-t,s=this._offset(this.el).top+t,n=this._offset(this.el).top,o=e.getDocumentScrollTop(),a=window.innerHeight,l=i*((o+a-n)/(t+a));this._enabled?s>o&&n=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=`${this.options.offset}px`,this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}_removePinClasses(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}static{J._pushpins=[]}}const tt={throttle:100,scrollOffset:200,activeClass:"active",getActiveElement:t=>'a[href="#'+t+'"]',keepTopElementActive:!1,animationDuration:null};class et extends i{static _elements;static _count;static _increment;static _elementsInView;static _visibleElements;static _ticks;static _keptTopActiveElement=null;tickId;id;constructor(t,e){super(t,e,et),this.el.M_ScrollSpy=this,this.options={...et.defaults,...e},et._elements.push(this),et._count++,et._increment++,this.tickId=-1,this.id=et._increment.toString(),this._setupEventHandlers(),this._handleWindowScroll()}static get defaults(){return tt}static init(t,e={}){return super.init(t,e,et)}static getInstance(t){return t.M_ScrollSpy}destroy(){et._elements.splice(et._elements.indexOf(this),1),et._elementsInView.splice(et._elementsInView.indexOf(this),1),et._visibleElements.splice(et._visibleElements.indexOf(this.el),1),et._count--,this._removeEventHandlers();document.querySelector(this.options.getActiveElement(this.el.id)).classList.remove(this.options.activeClass),this.el.M_ScrollSpy=void 0}_setupEventHandlers(){1===et._count&&(window.addEventListener("scroll",this._handleWindowScroll),window.addEventListener("resize",this._handleThrottledResize),document.body.addEventListener("click",this._handleTriggerClick))}_removeEventHandlers(){0===et._count&&(window.removeEventListener("scroll",this._handleWindowScroll),window.removeEventListener("resize",this._handleThrottledResize),document.body.removeEventListener("click",this._handleTriggerClick))}_handleThrottledResize=()=>e.throttle(this._handleWindowScroll,200).bind(this);_handleTriggerClick=t=>{const e=t.target;for(let i=et._elements.length-1;i>=0;i--){const s=et._elements[i];if(e===document.querySelector('a[href="#'+s.el.id+'"]')){t.preventDefault(),s.el.M_ScrollSpy.options.animationDuration?et._smoothScrollIntoView(s.el,s.el.M_ScrollSpy.options.animationDuration):s.el.scrollIntoView({behavior:"smooth"});break}}};_handleWindowScroll=()=>{et._ticks++;const t=e.getDocumentScrollTop(),i=e.getDocumentScrollLeft(),s=i+window.innerWidth,n=t+window.innerHeight,o=et._findElements(t,s,n,i);for(let t=0;t=0&&i!==et._ticks&&(e._exit(),e.tickId=-1)}if(et._elementsInView=o,et._elements.length){const t=et._elements[0].el.M_ScrollSpy.options;if(t.keepTopElementActive&&0===et._visibleElements.length){this._resetKeptTopActiveElementIfNeeded();const e=et._elements.filter(t=>et._getDistanceToViewport(t.el)<=0).sort((t,e)=>{const i=et._getDistanceToViewport(t.el),s=et._getDistanceToViewport(e.el);return is?1:0}),i=e.length?e[e.length-1]:et._elements[0],s=document.querySelector(t.getActiveElement(i.el.id));s?.classList.add(t.activeClass),et._keptTopActiveElement=s}}};static _offset(t){const e=t.getBoundingClientRect(),i=document.documentElement;return{top:e.top+window.pageYOffset-i.clientTop,left:e.left+window.pageXOffset-i.clientLeft}}static _findElements(t,e,i,s){const n=[];for(let o=0;o0){const t=et._offset(a.el).top,o=et._offset(a.el).left,r=o+a.el.getBoundingClientRect().width,h=t+a.el.getBoundingClientRect().height;!(o>e||ri||h0!==t.getBoundingClientRect().height),et._visibleElements[0]){const t=document.querySelector(this.options.getActiveElement(et._visibleElements[0].id));t?.classList.remove(this.options.activeClass),et._visibleElements[0].M_ScrollSpy&&this.id0!==t.getBoundingClientRect().height),et._visibleElements[0]){const t=document.querySelector(this.options.getActiveElement(et._visibleElements[0].id));if(t?.classList.remove(this.options.activeClass),et._visibleElements=et._visibleElements.filter(t=>t.id!=this.el.id),et._visibleElements[0]){const t=this.options.getActiveElement(et._visibleElements[0].id);document.querySelector(t)?.classList.add(this.options.activeClass),this._resetKeptTopActiveElementIfNeeded()}}}_resetKeptTopActiveElementIfNeeded(){et._keptTopActiveElement&&(et._keptTopActiveElement.classList.remove(this.options.activeClass),et._keptTopActiveElement=null)}static _getDistanceToViewport(t){return t.getBoundingClientRect().top}static _smoothScrollIntoView(t,e=300){const i=t.getBoundingClientRect().top+(window.scrollY||window.pageYOffset),s=window.scrollY||window.pageYOffset,n=i-s,o=performance.now();requestAnimationFrame(function t(a){const l=a-o,r=Math.min(l/e,1),h=s+n*r;r<1?(window.scrollTo(0,h),requestAnimationFrame(t)):window.scrollTo(0,i)})}static{et._elements=[],et._elementsInView=[],et._visibleElements=[],et._count=0,et._increment=0,et._ticks=0}}class it{static#z(t){const e=t.getBoundingClientRect(),i=document.documentElement;return{top:e.top+window.pageYOffset-i.clientTop,left:e.left+window.pageXOffset-i.clientLeft}}static renderWaveEffect(t,e=null,i=null){const s=null===e;let n,o;const a=l=>{o||(o=l);const r=l-o;if(r<500){const o=r/500*(2-r/500),l=s?"circle at 50% 50%":`circle at ${e.x}px ${e.y}px`,h=`rgba(${i?.r||0}, ${i?.g||0}, ${i?.b||0}, ${.3*(1-o)})`,d=90*o+"%";t.style.backgroundImage="radial-gradient("+l+", "+h+" "+d+", transparent "+d+")",n=window.requestAnimationFrame(a)}else t.style.backgroundImage="none",window.cancelAnimationFrame(n)};n=window.requestAnimationFrame(a)}static Init(){"undefined"!=typeof document&&document?.addEventListener("DOMContentLoaded",()=>{document.body.addEventListener("click",t=>{const e=t.target,i=e.closest(".waves-effect");if(i&&i.contains(e)){const e=i.classList.contains("waves-circle"),s=t.pageX-this.#z(i).left,n=t.pageY-this.#z(i).top;let o=null;i.classList.contains("waves-light")&&(o={r:255,g:255,b:255}),this.renderWaveEffect(i,e?null:{x:s,y:n},o)}})})}}return"undefined"!=typeof document&&(document.addEventListener("keydown",e.docHandleKeydown,!0),document.addEventListener("keyup",e.docHandleKeyup,!0),document.addEventListener("focus",e.docHandleFocus,!0),document.addEventListener("blur",e.docHandleBlur,!0)),D.Init(),g.Init(),it.Init(),X.Init(),d.Init(),t.AutoInit=function(t=document.body,e){const i={Autocomplete:t.querySelectorAll(".autocomplete:not(.no-autoinit)"),Cards:t.querySelectorAll(".cards:not(.no-autoinit)"),Carousel:t.querySelectorAll(".carousel:not(.no-autoinit)"),Chips:t.querySelectorAll(".chips:not(.no-autoinit)"),Collapsible:t.querySelectorAll(".collapsible:not(.no-autoinit)"),Datepicker:t.querySelectorAll(".datepicker:not(.no-autoinit)"),Dropdown:t.querySelectorAll(".dropdown-trigger:not(.no-autoinit)"),Materialbox:t.querySelectorAll(".materialboxed:not(.no-autoinit)"),Modal:t.querySelectorAll(".modal:not(.no-autoinit)"),Parallax:t.querySelectorAll(".parallax:not(.no-autoinit)"),Pushpin:t.querySelectorAll(".pushpin:not(.no-autoinit)"),ScrollSpy:t.querySelectorAll(".scrollspy:not(.no-autoinit)"),FormSelect:t.querySelectorAll("select:not(.no-autoinit)"),Sidenav:t.querySelectorAll(".sidenav:not(.no-autoinit)"),Tabs:t.querySelectorAll(".tabs:not(.no-autoinit)"),TapTarget:t.querySelectorAll(".tap-target:not(.no-autoinit)"),Timepicker:t.querySelectorAll(".timepicker:not(.no-autoinit)"),Tooltip:t.querySelectorAll(".tooltipped:not(.no-autoinit)"),FloatingActionButton:t.querySelectorAll(".fixed-action-btn:not(.no-autoinit)")};a.init(i.Autocomplete,e?.Autocomplete??{}),d.init(i.Cards,e?.Cards??{}),p.init(i.Carousel,e?.Carousel??{}),g.init(i.Chips,e?.Chips??{}),y.init(i.Collapsible,e?.Collapsible??{}),T.init(i.Datepicker,e?.Datepicker??{}),n.init(i.Dropdown,e?.Dropdown??{}),S.init(i.Materialbox,e?.Materialbox??{}),A.init(i.Modal,e?.Modal??{}),G.init(i.Parallax,e?.Parallax??{}),J.init(i.Pushpin,e?.Pushpin??{}),et.init(i.ScrollSpy,e?.ScrollSpy??{}),E.init(i.FormSelect,e?.FormSelect??{}),O.init(i.Sidenav,e?.Sidenav??{}),W.init(i.Tabs,e?.Tabs??{}),Y.init(i.TapTarget,e?.TapTarget??{}),q.init(i.Timepicker,e?.Timepicker??{}),N.init(i.Tooltip,e?.Tooltip??{}),r.init(i.FloatingActionButton,e?.FloatingActionButton??{})},t.Autocomplete=a,t.Cards=d,t.Carousel=p,t.CharacterCounter=U,t.Chips=g,t.Collapsible=y,t.Datepicker=T,t.Dropdown=n,t.FloatingActionButton=r,t.FormSelect=E,t.Forms=D,t.Materialbox=S,t.Modal=A,t.Parallax=G,t.Pushpin=J,t.Range=X,t.ScrollSpy=et,t.Sidenav=O,t.Slider=H,t.Tabs=W,t.TapTarget=Y,t.Timepicker=q,t.Toast=V,t.Tooltip=N,t.Waves=it,t.version="2.3.3",t}({}); diff --git a/dist/js/materialize.mjs b/dist/js/materialize.mjs deleted file mode 100644 index 626a45c4f8..0000000000 --- a/dist/js/materialize.mjs +++ /dev/null @@ -1,8376 +0,0 @@ -/*! -* Materialize v2.3.3 (https://materializeweb.com) -* Copyright 2014-2026 Materialize -* MIT License (https://raw.githubusercontent.com/materializecss/materialize/master/LICENSE) -*/ -/** - * Class with utilitary functions for global usage. - */ -class Utils { - /** Specifies wether tab is pressed or not. */ - static tabPressed = false; - /** Specifies wether there is a key pressed. */ - static keyDown = false; - /** - * Key maps. - */ - static keys = { - TAB: ['Tab'], - ENTER: ['Enter'], - ESC: ['Escape', 'Esc'], - BACKSPACE: ['Backspace'], - ARROW_UP: ['ArrowUp', 'Up'], - ARROW_DOWN: ['ArrowDown', 'Down'], - ARROW_LEFT: ['ArrowLeft', 'Left'], - ARROW_RIGHT: ['ArrowRight', 'Right'], - DELETE: ['Delete', 'Del'] - }; - /** - * Detects when a key is pressed. - * @param e Event instance. - */ - static docHandleKeydown(e) { - Utils.keyDown = true; - if ([...Utils.keys.TAB, ...Utils.keys.ARROW_DOWN, ...Utils.keys.ARROW_UP].includes(e.key)) { - Utils.tabPressed = true; - } - } - /** - * Detects when a key is released. - * @param e Event instance. - */ - static docHandleKeyup(e) { - Utils.keyDown = false; - if ([...Utils.keys.TAB, ...Utils.keys.ARROW_DOWN, ...Utils.keys.ARROW_UP].includes(e.key)) { - Utils.tabPressed = false; - } - } - /** - * Detects when document is focused. - * @param e Event instance. - */ - /* eslint-disabled as of required event type condition check */ - /* eslint-disable-next-line */ - static docHandleFocus(e) { - if (Utils.keyDown) { - document.body.classList.add('keyboard-focused'); - } - } - /** - * Detects when document is not focused. - * @param e Event instance. - */ - /* eslint-disabled as of required event type condition check */ - /* eslint-disable-next-line */ - static docHandleBlur(e) { - document.body.classList.remove('keyboard-focused'); - } - /** - * Generates a unique string identifier. - */ - static guid() { - const s4 = () => { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - }; - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); - } - /** - * Checks for exceeded edges - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkWithinContainer(container, bounding, offset) { - const edges = { - top: false, - right: false, - bottom: false, - left: false - }; - const containerRect = container.getBoundingClientRect(); - // If body element is smaller than viewport, use viewport height instead. - const containerBottom = container === document.body - ? Math.max(containerRect.bottom, window.innerHeight) - : containerRect.bottom; - const scrollLeft = container.scrollLeft; - const scrollTop = container.scrollTop; - const scrolledX = bounding.left - scrollLeft; - const scrolledY = bounding.top - scrollTop; - // Check for container and viewport for each edge - if (scrolledX < containerRect.left + offset || scrolledX < offset) { - edges.left = true; - } - if (scrolledX + bounding.width > containerRect.right - offset || - scrolledX + bounding.width > window.innerWidth - offset) { - edges.right = true; - } - if (scrolledY < containerRect.top + offset || scrolledY < offset) { - edges.top = true; - } - if (scrolledY + bounding.height > containerBottom - offset || - scrolledY + bounding.height > window.innerHeight - offset) { - edges.bottom = true; - } - return edges; - } - /** - * Checks if element can be aligned in multiple directions. - * @param el Element to be inspected. - * @param container Container element. - * @param bounding Bounding rect. - * @param offset Element offset. - */ - static checkPossibleAlignments(el, container, bounding, offset) { - const canAlign = { - top: true, - right: true, - bottom: true, - left: true, - spaceOnTop: null, - spaceOnRight: null, - spaceOnBottom: null, - spaceOnLeft: null - }; - const containerAllowsOverflow = getComputedStyle(container).overflow === 'visible'; - const containerRect = container.getBoundingClientRect(); - const containerHeight = Math.min(containerRect.height, window.innerHeight); - const containerWidth = Math.min(containerRect.width, window.innerWidth); - const elOffsetRect = el.getBoundingClientRect(); - const scrollLeft = container.scrollLeft; - const scrollTop = container.scrollTop; - const scrolledX = bounding.left - scrollLeft; - const scrolledYTopEdge = bounding.top - scrollTop; - const scrolledYBottomEdge = bounding.top + elOffsetRect.height - scrollTop; - // Check for container and viewport for left - canAlign.spaceOnRight = !containerAllowsOverflow - ? containerWidth - (scrolledX + bounding.width) - : window.innerWidth - (elOffsetRect.left + bounding.width); - if (canAlign.spaceOnRight < 0) { - canAlign.left = false; - } - // Check for container and viewport for Right - canAlign.spaceOnLeft = !containerAllowsOverflow - ? scrolledX - bounding.width + elOffsetRect.width - : elOffsetRect.right - bounding.width; - if (canAlign.spaceOnLeft < 0) { - canAlign.right = false; - } - // Check for container and viewport for Top - canAlign.spaceOnBottom = !containerAllowsOverflow - ? containerHeight - (scrolledYTopEdge + bounding.height + offset) - : window.innerHeight - (elOffsetRect.top + bounding.height + offset); - if (canAlign.spaceOnBottom < 0) { - canAlign.top = false; - } - // Check for container and viewport for Bottom - canAlign.spaceOnTop = !containerAllowsOverflow - ? scrolledYBottomEdge - (bounding.height - offset) - : elOffsetRect.bottom - (bounding.height + offset); - if (canAlign.spaceOnTop < 0) { - canAlign.bottom = false; - } - return canAlign; - } - /** - * Retrieves target element id from trigger. - * @param trigger Trigger element. - */ - static getIdFromTrigger(trigger) { - let id = trigger.dataset.target; - if (!id) { - id = trigger.getAttribute('href'); - return id ? id.slice(1) : ''; - } - return id; - } - /** - * Retrieves document scroll postion from top. - */ - static getDocumentScrollTop() { - return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; - } - /** - * Retrieves document scroll postion from left. - */ - static getDocumentScrollLeft() { - return window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft || 0; - } - /** - * Fires the given function after a certain ammount of time. - * @param func Function to be fired. - * @param wait Wait time. - * @param options Additional options. - */ - static throttle(func, wait, options = {}) { - let context, args, result, timeout = null, previous = 0; - const later = () => { - previous = options.leading === false ? 0 : new Date().getTime(); - timeout = null; - result = func.apply(context, args); - context = args = null; - }; - return (...args) => { - const now = new Date().getTime(); - if (!previous && options.leading === false) - previous = now; - const remaining = wait - (now - previous); - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(this, args); - } - else if (!timeout && options.trailing !== false) { - timeout = setTimeout(later, remaining); - } - return result; - }; - } - /** - * Renders confirm/close buttons with callback function - */ - static createConfirmationContainer(container, confirmText, cancelText, onConfirm, onCancel) { - const confirmationButtonsContainer = document.createElement('div'); - confirmationButtonsContainer.classList.add('confirmation-btns'); - container.append(confirmationButtonsContainer); - this.createButton(confirmationButtonsContainer, cancelText, ['btn-cancel'], true, onCancel); - this.createButton(confirmationButtonsContainer, confirmText, ['btn-confirm'], true, onConfirm); - } - /** - * Renders a button with optional callback function - */ - static createButton(container, text, className = [], visibility = true, callback = null) { - className = className.concat(['btn', 'waves-effect', 'text']); - const button = document.createElement('button'); - button.className = className.join(' '); - button.style.visibility = visibility ? 'visible' : 'hidden'; - button.type = 'button'; - button.tabIndex = !!visibility ? 0 : -1; - button.innerText = text; - button.addEventListener('click', callback); - button.addEventListener('keypress', (e) => { - if (Utils.keys.ENTER.includes(e.key)) - callback(e); - }); - container.append(button); - } - static _setAbsolutePosition(origin, container, position, margin, transitionMovement, align = 'center') { - const originHeight = origin.offsetHeight, originWidth = origin.offsetWidth, containerHeight = container.offsetHeight, containerWidth = container.offsetWidth; - let xMovement = 0, yMovement = 0, targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(), targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (position === 'top') { - targetTop += -containerHeight - margin; - if (align === 'center') { - targetLeft += originWidth / 2 - containerWidth / 2; // This is center align - } - yMovement = -transitionMovement; - } - else if (position === 'right') { - targetTop += originHeight / 2 - containerHeight / 2; - targetLeft = originWidth + margin; - xMovement = transitionMovement; - } - else if (position === 'left') { - targetTop += originHeight / 2 - containerHeight / 2; - targetLeft = -containerWidth - margin; - xMovement = -transitionMovement; - } - else { - targetTop += originHeight + margin; - if (align === 'center') { - targetLeft += originWidth / 2 - containerWidth / 2; // This is center align - } - yMovement = transitionMovement; - } - if (align === 'right') { - targetLeft += originWidth - containerWidth - margin; - } - const newCoordinates = Utils._repositionWithinScreen(targetLeft, targetTop, containerWidth, containerHeight, margin, transitionMovement, align); - container.style.top = newCoordinates.y + 'px'; - container.style.left = newCoordinates.x + 'px'; - return { x: xMovement, y: yMovement }; - } - static _repositionWithinScreen(x, y, width, height, margin, transitionMovement, align) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - const bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - let offset; - if (align === 'left' || align == 'center') { - offset = margin + transitionMovement; - } - else if (align === 'right') { - offset = margin - transitionMovement; - } - const edges = Utils.checkWithinContainer(document.body, bounding, offset); - if (edges.left) { - newX = offset; - } - else if (edges.right) { - newX -= newX + width - window.innerWidth; - } - if (edges.top) { - newY = offset; - } - else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; - } -} - -/** - * Base class implementation for Materialize components. - */ -class Component { - /** - * The DOM element the plugin was initialized with. - */ - el; - /** - * The options the instance was initialized with. - */ - options; - /** - * Constructs component instance and set everything up. - */ - constructor(el, options, classDef) { - // Display error if el is not a valid HTML Element - if (!(el instanceof HTMLElement)) { - console.error(Error(el + ' is not an HTML Element')); - } - // If exists, destroy and reinitialize in child - const ins = classDef.getInstance(el); - if (!!ins) { - ins.destroy(); - } - this.el = el; - } - /** - * Initializes component instances. - * @param els HTML elements. - * @param options Component options. - * @param classDef Class definition. - */ - static init(els, options, classDef) { - let instances = null; - if (els instanceof Element) { - instances = new classDef(els, options); - } - else if (!!els && els.length) { - instances = []; - for (let i = 0; i < els.length; i++) { - instances.push(new classDef(els[i], options)); - } - } - return instances; - } - /** - * @returns default options for component instance. - */ - static get defaults() { - return {}; - } - /** - * Retrieves component instance for the given element. - * @param el Associated HTML Element. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - static getInstance(el) { - throw new Error('This method must be implemented.'); - } - /** - * Destroy plugin instance and teardown. - */ - destroy() { - throw new Error('This method must be implemented.'); - } -} - -const _defaults$o = { - alignment: 'left', - autoFocus: true, - constrainWidth: true, - container: null, - coverTrigger: true, - closeOnClick: true, - hover: false, - inDuration: 150, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - onItemClick: null -}; -class Dropdown extends Component { - static _dropdowns = []; - /** ID of the dropdown element. */ - id; - /** The DOM element of the dropdown. */ - dropdownEl; - /** If the dropdown is open. */ - isOpen; - /** If the dropdown content is scrollable. */ - isScrollable; - isTouchMoving; - /** The index of the item focused. */ - focusedIndex; - filterQuery; - filterTimeout; - constructor(el, options) { - super(el, options, Dropdown); - this.el['M_Dropdown'] = this; - Dropdown._dropdowns.push(this); - this.id = Utils.getIdFromTrigger(el); - this.dropdownEl = document.getElementById(this.id); - this.options = { - ...Dropdown.defaults, - ...options - }; - this.isOpen = false; - this.isScrollable = false; - this.isTouchMoving = false; - this.focusedIndex = -1; - this.filterQuery = []; - this.el.ariaExpanded = 'false'; - // Move dropdown-content after dropdown-trigger - this._moveDropdownToElement(); - this._makeDropdownFocusable(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$o; - } - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Dropdown); - } - static getInstance(el) { - return el['M_Dropdown']; - } - destroy() { - this._resetDropdownStyles(); - this._removeEventHandlers(); - Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); - this.el['M_Dropdown'] = undefined; - } - _setupEventHandlers() { - // Trigger keydown handler - this.el.addEventListener('keydown', this._handleTriggerKeydown); - // Item click handler - this.dropdownEl?.addEventListener('click', this._handleDropdownClick); - // Hover event handlers - if (this.options.hover) { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave); - // Click event handlers - } - else { - this.el.addEventListener('click', this._handleClick); - } - } - _removeEventHandlers() { - this.el.removeEventListener('keydown', this._handleTriggerKeydown); - this.dropdownEl.removeEventListener('click', this._handleDropdownClick); - if (this.options.hover) { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave); - } - else { - this.el.removeEventListener('click', this._handleClick); - } - } - _setupTemporaryEventHandlers() { - document.body.addEventListener('click', this._handleDocumentClick); - document.body.addEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); - window.addEventListener('resize', this._handleWindowResize); - } - _removeTemporaryEventHandlers() { - document.body.removeEventListener('click', this._handleDocumentClick); - document.body.removeEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); - window.removeEventListener('resize', this._handleWindowResize); - } - _handleClick = (e) => { - e.preventDefault(); - //this._moveDropdown((e.target).closest('li')); - if (this.isOpen) { - this.close(); - } - else { - this.open(); - } - }; - _handleMouseEnter = () => { - //this._moveDropdown((e.target).closest('li')); - this.open(); - }; - _handleMouseLeave = (e) => { - const toEl = e.relatedTarget; - const leaveToDropdownContent = !!toEl.closest('.dropdown-content'); - let leaveToActiveDropdownTrigger = false; - const closestTrigger = toEl.closest('.dropdown-trigger'); - if (closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen) { - leaveToActiveDropdownTrigger = true; - } - // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content - if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { - this.close(); - } - }; - _handleDocumentClick = (e) => { - const target = e.target; - if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) { - // isTouchMoving to check if scrolling on mobile. - this.close(); - } - else if (!target.closest('.dropdown-content')) { - // Do this one frame later so that if the element clicked also triggers _handleClick - // For example, if a label for a select was clicked, that we don't close/open the dropdown - setTimeout(() => { - if (this.isOpen) { - this.close(); - } - }, 0); - } - this.isTouchMoving = false; - }; - _handleTriggerKeydown = (e) => { - // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown - const arrowDownOrEnter = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key); - if (arrowDownOrEnter && !this.isOpen) { - e.preventDefault(); - this.open(); - } - }; - _handleDocumentTouchmove = (e) => { - const target = e.target; - if (target.closest('.dropdown-content')) { - this.isTouchMoving = true; - } - }; - _handleDropdownClick = (e) => { - // onItemClick callback - if (typeof this.options.onItemClick === 'function') { - const itemEl = e.target.closest('li'); - this.options.onItemClick.call(this, itemEl); - } - }; - _handleDropdownKeydown = (e) => { - const arrowUpOrDown = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key); - if (Utils.keys.TAB.includes(e.key)) { - e.preventDefault(); - this.close(); - } - // Navigate down dropdown list - else if (arrowUpOrDown && this.isOpen) { - e.preventDefault(); - const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1; - let newFocusedIndex = this.focusedIndex; - let hasFoundNewIndex = false; - do { - newFocusedIndex = newFocusedIndex + direction; - if (!!this.dropdownEl.children[newFocusedIndex] && - this.dropdownEl.children[newFocusedIndex].tabIndex !== -1) { - hasFoundNewIndex = true; - break; - } - } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); - if (hasFoundNewIndex) { - // Remove active class from old element - if (this.focusedIndex >= 0) - this.dropdownEl.children[this.focusedIndex].classList.remove('active'); - this.focusedIndex = newFocusedIndex; - this._focusFocusedItem(); - } - } - // ENTER selects choice on focused item - else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) { - // Search for
and ` + - ''); - } - renderRow(days, isRTL, isRowSelected) { - return ('' + - (isRTL ? days.reverse() : days).join('') + - ''); - } - renderTable(opts, data, randId) { - return ('
' + - this.renderHead(opts) + - this.renderBody(data) + - '
'); - } - renderHead(opts) { - const arr = []; - let i; - for (i = 0; i < 7; i++) { - arr.push(`${this.renderDayName(opts, i, true)}`); - } - return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; - } - renderBody(rows) { - return '' + rows.join('') + ''; - } - renderTitle(instance, c, year, month, refYear, randId) { - const opts = this.options, isMinYear = year === opts.minYear, isMaxYear = year === opts.maxYear; - let i, j, arr = [], html = '
', prev = true, next = true; - for (i = 0; i < 12; i++) { - arr.push(''); - } - const monthHtml = ''; - if (Array.isArray(opts.yearRange)) { - i = opts.yearRange[0]; - j = opts.yearRange[1] + 1; - } - else { - i = year - opts.yearRange; - j = 1 + year + opts.yearRange; - } - for (arr = []; i < j && i <= opts.maxYear; i++) { - if (i >= opts.minYear) { - arr.push(``); - } - } - if (opts.yearRangeReverse) - arr.reverse(); - const yearHtml = ``; - const leftArrow = ''; - html += ``; - html += '
'; - if (opts.showMonthAfterYear) { - html += yearHtml + monthHtml; - } - else { - html += monthHtml + yearHtml; - } - html += '
'; - if (isMinYear && (month === 0 || opts.minMonth >= month)) { - prev = false; - } - if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { - next = false; - } - const rightArrow = ''; - html += ``; - return (html += '
'); - } - // refresh HTML - draw() { - const opts = this.options, minYear = opts.minYear, maxYear = opts.maxYear, minMonth = opts.minMonth, maxMonth = opts.maxMonth; - let html = ''; - if (this._y <= minYear) { - this._y = minYear; - if (!isNaN(minMonth) && this._m < minMonth) { - this._m = minMonth; - } - } - if (this._y >= maxYear) { - this._y = maxYear; - if (!isNaN(maxMonth) && this._m > maxMonth) { - this._m = maxMonth; - } - } - const randId = 'datepicker-title-' + - Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .substr(0, 2); - for (let c = 0; c < 1; c++) { - if (!this.options.isDateRange) { - this._renderDateDisplay(this.date); - } - else { - this._renderDateDisplay(this.date, this.endDate); - } - html += - this.renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId); - } - this.destroySelects(); - this.calendarEl.innerHTML = html; - // Init Materialize Select - const yearSelect = this.calendarEl.querySelector('.orig-select-year'); - const monthSelect = this.calendarEl.querySelector('.orig-select-month'); - // @todo fix accessibility @see https://github.com/materializecss/materialize/issues/522 - FormSelect.init(yearSelect, { - classes: 'select-year', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - FormSelect.init(monthSelect, { - classes: 'select-month', - dropdownOptions: { container: document.body, constrainWidth: false } - }); - // Add change handlers for select - yearSelect.addEventListener('change', this._handleYearChange); - monthSelect.addEventListener('change', this._handleMonthChange); - if (typeof this.options.onDraw === 'function') { - this.options.onDraw.call(this); - } - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - this.el.addEventListener('keydown', this._handleInputKeydown); - this.el.addEventListener('change', this._handleInputChange); - this.calendarEl.addEventListener('click', this._handleCalendarClick); - } - _setupVariables() { - const template = document.createElement('template'); - template.innerHTML = Datepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.calendarEl = this.containerEl.querySelector('.datepicker-calendar'); - this.yearTextEl = this.containerEl.querySelector('.year-text'); - this.dateTextEl = this.containerEl.querySelector('.date-text'); - this.footer = this.containerEl.querySelector('.datepicker-footer'); - this.formats = { - d: (date) => { - return date.getDate(); - }, - dd: (date) => { - const d = date.getDate(); - return (d < 10 ? '0' : '') + d; - }, - ddd: (date) => { - return this.options.i18n.weekdaysShort[date.getDay()]; - }, - dddd: (date) => { - return this.options.i18n.weekdays[date.getDay()]; - }, - m: (date) => { - return date.getMonth() + 1; - }, - mm: (date) => { - const m = date.getMonth() + 1; - return (m < 10 ? '0' : '') + m; - }, - mmm: (date) => { - return this.options.i18n.monthsShort[date.getMonth()]; - }, - mmmm: (date) => { - return this.options.i18n.months[date.getMonth()]; - }, - yy: (date) => { - return ('' + date.getFullYear()).slice(2); - }, - yyyy: (date) => { - return date.getFullYear(); - } - }; - } - _pickerSetup() { - if (!this.options.defaultDate) { - this.options.defaultDate = new Date(Date.parse(this.el.value)); - } - const defDate = this.options.defaultDate; - if (Datepicker._isDate(defDate)) { - if (this.options.setDefaultDate) { - this.setDate(defDate, true); - this.setInputValue(this.el, defDate); - } - else { - this.gotoDate(defDate); - } - } - else { - this.gotoDate(new Date()); - } - if (this.options.isDateRange) { - this.multiple = true; - const defEndDate = this.options.defaultEndDate; - if (Datepicker._isDate(defEndDate)) { - if (this.options.setDefaultEndDate) { - this.setDate(defEndDate, true, true); - this.setInputValue(this.endDateEl, defEndDate); - } - } - } - if (this.options.isMultipleSelection) { - this.multiple = true; - this.dates = []; - this.dateEls = []; - this.dateEls.push(this.el); - } - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - this.el.removeEventListener('change', this._handleInputChange); - this.calendarEl.removeEventListener('click', this._handleCalendarClick); - if (this.options.isDateRange) { - this.endDateEl.removeEventListener('click', this._handleInputClick); - this.endDateEl.removeEventListener('keypress', this._handleInputKeydown); - this.endDateEl.removeEventListener('change', this._handleInputChange); - } - } - _handleInputClick = (e) => { - // Prevents default browser datepicker modal rendering - if (e.type == 'date') { - e.preventDefault(); - } - this.setDateFromInput(e.target); - this.draw(); - this.gotoDate(e.target === this.el ? this.date : this.endDate); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.setDateFromInput(e.target); - this.draw(); - if (this.displayPlugin) - this.displayPlugin.show(); - if (this.options.onInputInteraction) - this.options.onInputInteraction.call(this); - } - }; - _handleCalendarClick = (e) => { - const target = e.target; - if (!target.classList.contains('is-disabled')) { - if (target.classList.contains('datepicker-day-button') && - !target.classList.contains('is-empty') && - !target.parentElement.classList.contains('is-disabled')) { - const selectedDate = new Date(e.target.getAttribute('data-year'), e.target.getAttribute('data-month'), e.target.getAttribute('data-day')); - if (!this.multiple || (this.multiple && this.options.isMultipleSelection)) { - this.setDate(selectedDate); - } - if (this.options.isDateRange) { - const confirmAfterSelection = Datepicker._isDate(this.date) && this.options.autoSubmit; - this._handleDateRangeCalendarClick(selectedDate); - if (confirmAfterSelection) { - this._confirm(); - } - } - if (!this.options.isDateRange && this.options.autoSubmit) - this._confirm(); - } - else if (target.closest('.month-prev')) { - this.prevMonth(); - } - else if (target.closest('.month-next')) { - this.nextMonth(); - } - } - }; - _handleDateRangeCalendarClick = (date) => { - if (this.endDate == null || !Datepicker._compareDates(date, this.endDate)) { - if (Datepicker._isDate(this.date) && Datepicker._comparePastDate(date, this.date)) { - return; - } - this.setDate(date, true, Datepicker._isDate(this.date)); - return; - } - this._clearDates(); - this.draw(); - }; - _handleClearClick = () => { - this._clearDates(); - this.setInputValues(); - }; - _clearDates = () => { - this.date = null; - this.endDate = null; - this.draw(); - }; - _handleMonthChange = (e) => { - this.gotoMonth(e.target.value); - }; - _handleYearChange = (e) => { - this.gotoYear(e.target.value); - }; - // change view to a specific month (zero-index, e.g. 0: January) - gotoMonth(month) { - if (!isNaN(month)) { - this.calendars[0].month = parseInt(month, 10); - this.adjustCalendars(); - } - } - // change view to a specific full year (e.g. "2012") - gotoYear(year) { - if (!isNaN(year)) { - this.calendars[0].year = parseInt(year, 10); - this.adjustCalendars(); - } - } - _handleInputChange = (e) => { - let date; - const el = e.target; - // Prevent change event from being fired when triggered by the plugin - if (e['detail']?.firedBy === this) - return; - // Prevent change event from being fired if an end date is set without a start date - if (el == this.endDateEl && !this.date) - return; - if (this.options.parse) { - date = this.options.parse(e.target.value, typeof this.options.format === 'function' - ? this.options.format(new Date(this.el.value)) - : this.options.format); - } - else { - date = new Date(Date.parse(e.target.value)); - } - if (Datepicker._isDate(date)) { - this.setDate(date, false, el == this.endDateEl, true); - if (e.type == 'date') { - this.setDataDate(e, date); - this.setInputValues(); - } - } - }; - renderDayName(opts, day, abbr = false) { - day += opts.firstDay; - while (day >= 7) { - day -= 7; - } - return abbr ? opts.i18n.weekdaysAbbrev[day] : opts.i18n.weekdays[day]; - } - createDateInput() { - const dateInput = this.el.cloneNode(true); - dateInput.addEventListener('click', this._handleInputClick); - dateInput.addEventListener('keypress', this._handleInputKeydown); - dateInput.addEventListener('change', this._handleInputChange); - this.el.parentElement.appendChild(dateInput); - return dateInput; - } - // Set input value to the selected date and close Datepicker - _finishSelection = () => { - this.setInputValues(); - // Commented out because of function deprecations - // this.close(); - }; - _confirm = () => { - this._finishSelection(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onConfirm === 'function') - this.options.onConfirm.call(this); - }; - _cancel = () => { - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - // deprecated - open() { - console.warn('Datepicker.open() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Datepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Datepicker._template = ` -
-
- - -
-
-
- -
-
`; - } -} - -class Forms { - /** - * Checks if the label has validation and apply - * the correct class and styles - * @param textfield - */ - static validateField(textfield) { - if (!textfield) { - console.error('No text field element found'); - return; - } - const hasLength = textfield.getAttribute('data-length') !== null; - const lenAttr = parseInt(textfield.getAttribute('data-length')); - const len = textfield.value.length; - if (len === 0 && - textfield.validity.badInput === false && - !textfield.required && - textfield.classList.contains('validate')) { - textfield.classList.remove('invalid'); - } - else if (textfield.classList.contains('validate')) { - // Check for character counter attributes - if ((textfield.validity.valid && hasLength && len <= lenAttr) || - (textfield.validity.valid && !hasLength)) { - textfield.classList.remove('invalid'); - } - else { - textfield.classList.add('invalid'); - } - } - } - /** - * Resizes the given TextArea after updating the - * value content dynamically. - * @param e EventTarget - */ - static textareaAutoResize(e) { - const textarea = e; - // if (!textarea) { - // console.error('No textarea element found'); - // return; - // } - // Textarea Auto Resize - let hiddenDiv = document.querySelector('.hiddendiv'); - if (!hiddenDiv) { - hiddenDiv = document.createElement('div'); - hiddenDiv.classList.add('hiddendiv', 'common'); - document.body.append(hiddenDiv); - } - const style = getComputedStyle(textarea); - // Set font properties of hiddenDiv - const fontFamily = style.fontFamily; //textarea.css('font-family'); - const fontSize = style.fontSize; //textarea.css('font-size'); - const lineHeight = style.lineHeight; //textarea.css('line-height'); - // Firefox can't handle padding shorthand. - const paddingTop = style.paddingTop; //getComputedStyle(textarea).css('padding-top'); - const paddingRight = style.paddingRight; //textarea.css('padding-right'); - const paddingBottom = style.paddingBottom; //textarea.css('padding-bottom'); - const paddingLeft = style.paddingLeft; //textarea.css('padding-left'); - if (fontSize) - hiddenDiv.style.fontSize = fontSize; //('font-size', fontSize); - if (fontFamily) - hiddenDiv.style.fontFamily = fontFamily; //css('font-family', fontFamily); - if (lineHeight) - hiddenDiv.style.lineHeight = lineHeight; //css('line-height', lineHeight); - if (paddingTop) - hiddenDiv.style.paddingTop = paddingTop; //ss('padding-top', paddingTop); - if (paddingRight) - hiddenDiv.style.paddingRight = paddingRight; //css('padding-right', paddingRight); - if (paddingBottom) - hiddenDiv.style.paddingBottom = paddingBottom; //css('padding-bottom', paddingBottom); - if (paddingLeft) - hiddenDiv.style.paddingLeft = paddingLeft; //css('padding-left', paddingLeft); - // Set original-height, if none - if (!textarea.hasAttribute('original-height')) - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - if (textarea.getAttribute('wrap') === 'off') { - hiddenDiv.style.overflowWrap = 'normal'; // ('overflow-wrap', 'normal') - hiddenDiv.style.whiteSpace = 'pre'; //.css('white-space', 'pre'); - } - hiddenDiv.innerText = textarea.value + '\n'; - hiddenDiv.innerHTML = hiddenDiv.innerHTML.replace(/\n/g, '
'); - // When textarea is hidden, width goes crazy. - // Approximate with half of window size - if (textarea.offsetWidth > 0 && textarea.offsetHeight > 0) { - hiddenDiv.style.width = textarea.getBoundingClientRect().width + 'px'; // ('width', textarea.width() + 'px'); - } - else { - hiddenDiv.style.width = window.innerWidth / 2 + 'px'; //css('width', window.innerWidth / 2 + 'px'); - } - // Resize if the new height is greater than the - // original height of the textarea - const originalHeight = parseInt(textarea.getAttribute('original-height')); - const prevLength = parseInt(textarea.getAttribute('previous-length')); - if (isNaN(originalHeight)) - return; - if (originalHeight <= hiddenDiv.clientHeight) { - textarea.style.height = hiddenDiv.clientHeight + 'px'; //css('height', hiddenDiv.innerHeight() + 'px'); - } - else if (textarea.value.length < prevLength) { - // In case the new height is less than original height, it - // means the textarea has less text than before - // So we set the height to the original one - textarea.style.height = originalHeight + 'px'; - } - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - } - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.addEventListener('change', (e) => { - const target = e.target; - if (target instanceof HTMLInputElement) { - if (target.value.length !== 0 || target.getAttribute('placeholder') !== null) { - for (const child of target.parentNode.children) { - if (child.tagName == 'label') { - child.classList.add('active'); - } - } - } - Forms.validateField(target); - } - }); - document.addEventListener('keyup', (e) => { - const target = e.target; - // Radio and Checkbox focus class - if (target instanceof HTMLInputElement && ['radio', 'checkbox'].includes(target.type)) { - // TAB, check if tabbing to radio or checkbox. - if (Utils.keys.TAB.includes(e.key)) { - target.classList.add('tabbed'); - target.addEventListener('blur', () => target.classList.remove('tabbed'), { - once: true - }); - } - } - }); - document - .querySelectorAll('.materialize-textarea') - .forEach((textArea) => { - Forms.InitTextarea(textArea); - }); - // File Input Path - document - .querySelectorAll('.file-field input[type="file"]') - .forEach((fileInput) => { - Forms.InitFileInputPath(fileInput); - }); - }); - } - static InitTextarea(textarea) { - // Save Data in Element - textarea.setAttribute('original-height', textarea.getBoundingClientRect().height.toString()); - textarea.setAttribute('previous-length', (textarea.value || '').length.toString()); - Forms.textareaAutoResize(textarea); - textarea.addEventListener('keyup', (e) => Forms.textareaAutoResize(e.target)); - textarea.addEventListener('keydown', (e) => Forms.textareaAutoResize(e.target)); - } - static InitFileInputPath(fileInput) { - fileInput.addEventListener('change', () => { - const fileField = fileInput.closest('.file-field'); - const pathInput = fileField.querySelector('input.file-path'); - const files = fileInput.files; - const filenames = []; - for (let i = 0; i < files.length; i++) { - filenames.push(files[i].name); - } - pathInput.value = filenames.join(', '); - pathInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - }); - } -} - -const _defaults$d = { - inDuration: 275, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null -}; -class Materialbox extends Component { - /** If the materialbox overlay is showing. */ - overlayActive; - /** If the materialbox is no longer being animated. */ - doneAnimating; - /** Caption, if specified. */ - caption; - /** Original width of image. */ - originalWidth; - /** Original height of image. */ - originalHeight; - originInlineStyles; - placeholder; - _changedAncestorList; - newHeight; - newWidth; - windowWidth; - windowHeight; - attrWidth; - attrHeight; - _overlay; - _photoCaption; - constructor(el, options) { - super(el, options, Materialbox); - this.el['M_Materialbox'] = this; - this.options = { - ...Materialbox.defaults, - ...options - }; - this.overlayActive = false; - this.doneAnimating = true; - this.placeholder = document.createElement('div'); - this.placeholder.classList.add('material-placeholder'); - this.originalWidth = 0; - this.originalHeight = 0; - this.originInlineStyles = this.el.getAttribute('style'); - this.caption = this.el.getAttribute('data-caption') || ''; - this.el.tabIndex = 0; - // Wrap - this.el.before(this.placeholder); - this.placeholder.append(this.el); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$d; - } - /** - * Initializes instances of MaterialBox. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Materialbox); - } - static getInstance(el) { - return el['M_Materialbox']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_Materialbox'] = undefined; - // Unwrap image - //this.placeholder.after(this.el).remove(); - this.placeholder.remove(); - this.el.removeAttribute('style'); - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleMaterialboxClick); - this.el.addEventListener('keypress', this._handleMaterialboxKeypress); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleMaterialboxClick); - this.el.removeEventListener('keypress', this._handleMaterialboxKeypress); - } - _handleMaterialboxClick = () => { - this._handleMaterialboxToggle(); - }; - _handleMaterialboxKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleMaterialboxToggle(); - } - }; - _handleMaterialboxToggle = () => { - // If already modal, return to original - if (this.doneAnimating === false || (this.overlayActive && this.doneAnimating)) - this.close(); - else - this.open(); - }; - _handleWindowScroll = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowResize = () => { - if (this.overlayActive) - this.close(); - }; - _handleWindowEscape = (e) => { - if (Utils.keys.ESC.includes(e.key) && this.doneAnimating && this.overlayActive) - this.close(); - }; - _makeAncestorsOverflowVisible() { - this._changedAncestorList = []; - let ancestor = this.placeholder.parentNode; - while (ancestor !== null && ancestor !== document) { - const curr = ancestor; - if (curr.style.overflow !== 'visible') { - curr.style.overflow = 'visible'; - this._changedAncestorList.push(curr); - } - ancestor = ancestor.parentNode; - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateVars() { - this.windowWidth = window.innerWidth; - this.windowHeight = window.innerHeight; - this.caption = this.el.getAttribute('data-caption') || ''; - } - // Image - _animateImageIn() { - this.el.style.maxHeight = this.newHeight.toString() + 'px'; - this.el.style.maxWidth = this.newWidth.toString() + 'px'; - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.height = this.originalHeight + 'px'; - this.el.style.width = this.originalWidth + 'px'; - setTimeout(() => { - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.newHeight + 'px'; - this.el.style.width = this.newWidth + 'px'; - this.el.style.left = - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2 + - 'px'; - this.el.style.top = - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2 + - 'px'; - }, 1); - setTimeout(() => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - /* - anim({ - targets: this.el, // image - height: [this.originalHeight, this.newHeight], - width: [this.originalWidth, this.newWidth], - left: - Utils.getDocumentScrollLeft() + - this.windowWidth / 2 - - this._offset(this.placeholder).left - - this.newWidth / 2, - top: - Utils.getDocumentScrollTop() + - this.windowHeight / 2 - - this._offset(this.placeholder).top - - this.newHeight / 2, - - duration: this.options.inDuration, - easing: 'easeOutQuad', - complete: () => { - this.doneAnimating = true; - if (typeof this.options.onOpenEnd === 'function') this.options.onOpenEnd.call(this, this.el); - } - }); - */ - } - _animateImageOut() { - const duration = this.options.outDuration; - // easeOutQuad - this.el.style.transition = `height ${duration}ms ease, - width ${duration}ms ease, - left ${duration}ms ease, - top ${duration}ms ease - `; - // to - this.el.style.height = this.originalWidth + 'px'; - this.el.style.width = this.originalWidth + 'px'; - this.el.style.left = '0'; - this.el.style.top = '0'; - setTimeout(() => { - this.placeholder.style.height = ''; - this.placeholder.style.width = ''; - this.placeholder.style.position = ''; - this.placeholder.style.top = ''; - this.placeholder.style.left = ''; - // Revert to width or height attribute - if (this.attrWidth) - this.el.setAttribute('width', this.attrWidth.toString()); - if (this.attrHeight) - this.el.setAttribute('height', this.attrHeight.toString()); - this.el.removeAttribute('style'); - if (this.originInlineStyles) - this.el.setAttribute('style', this.originInlineStyles); - // Remove class - this.el.classList.remove('active'); - this.doneAnimating = true; - // Remove overflow overrides on ancestors - this._changedAncestorList.forEach((anchestor) => (anchestor.style.overflow = '')); - // onCloseEnd callback - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - // Caption - _addCaption() { - this._photoCaption = document.createElement('div'); - this._photoCaption.classList.add('materialbox-caption'); - this._photoCaption.innerText = this.caption; - document.body.append(this._photoCaption); - this._photoCaption.style.display = 'inline'; - // Animate - this._photoCaption.style.transition = 'none'; - this._photoCaption.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '1'; - }, 1); - } - _removeCaption() { - const duration = this.options.outDuration; - this._photoCaption.style.transition = `opacity ${duration}ms ease`; - this._photoCaption.style.opacity = '0'; - setTimeout(() => { - this._photoCaption.remove(); - }, duration); - } - // Overlay - _addOverlay() { - this._overlay = document.createElement('div'); - this._overlay.id = 'materialbox-overlay'; - this._overlay.addEventListener('click', () => { - if (this.doneAnimating) - this.close(); - }, { once: true }); - // Put before in origin image to preserve z-index layering. - this.el.before(this._overlay); - // Set dimensions if needed - const overlayOffset = this._overlay.getBoundingClientRect(); - this._overlay.style.width = this.windowWidth + 'px'; - this._overlay.style.height = this.windowHeight + 'px'; - this._overlay.style.left = -1 * overlayOffset.left + 'px'; - this._overlay.style.top = -1 * overlayOffset.top + 'px'; - // Animate - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = '0'; - const duration = this.options.inDuration; - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '1'; - }, 1); - } - _removeOverlay() { - const duration = this.options.outDuration; - this._overlay.style.transition = `opacity ${duration}ms ease`; - this._overlay.style.opacity = '0'; - setTimeout(() => { - this.overlayActive = false; - this._overlay.remove(); - }, duration); - } - /** - * Open materialbox. - */ - open = () => { - this._updateVars(); - this.originalWidth = this.el.getBoundingClientRect().width; - this.originalHeight = this.el.getBoundingClientRect().height; - // Set states - this.doneAnimating = false; - this.el.classList.add('active'); - this.overlayActive = true; - // onOpenStart callback - if (typeof this.options.onOpenStart === 'function') - this.options.onOpenStart.call(this, this.el); - // Set positioning for placeholder - this.placeholder.style.width = this.placeholder.getBoundingClientRect().width + 'px'; - this.placeholder.style.height = this.placeholder.getBoundingClientRect().height + 'px'; - this.placeholder.style.position = 'relative'; - this.placeholder.style.top = '0'; - this.placeholder.style.left = '0'; - this._makeAncestorsOverflowVisible(); - // Set css on origin - this.el.style.position = 'absolute'; - this.el.style.zIndex = '1000'; - this.el.style.willChange = 'left, top, width, height'; - // Change from width or height attribute to css - this.attrWidth = this.el.getAttribute('width'); - this.attrHeight = this.el.getAttribute('height'); - if (this.attrWidth) { - this.el.style.width = this.attrWidth + 'px'; - this.el.removeAttribute('width'); - } - if (this.attrHeight) { - this.el.style.width = this.attrHeight + 'px'; - this.el.removeAttribute('height'); - } - this._addOverlay(); - // Add and animate caption if it exists - if (this.caption !== '') - this._addCaption(); - // Resize Image - const widthPercent = this.originalWidth / this.windowWidth; - const heightPercent = this.originalHeight / this.windowHeight; - this.newWidth = 0; - this.newHeight = 0; - if (widthPercent > heightPercent) { - // Width first - const ratio = this.originalHeight / this.originalWidth; - this.newWidth = this.windowWidth * 0.9; - this.newHeight = this.windowWidth * 0.9 * ratio; - } - else { - // Height first - const ratio = this.originalWidth / this.originalHeight; - this.newWidth = this.windowHeight * 0.9 * ratio; - this.newHeight = this.windowHeight * 0.9; - } - this._animateImageIn(); - // Handle Exit triggers - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleWindowResize); - window.addEventListener('keyup', this._handleWindowEscape); - }; - /** - * Close materialbox. - */ - close = () => { - this._updateVars(); - this.doneAnimating = false; - // onCloseStart callback - if (typeof this.options.onCloseStart === 'function') - this.options.onCloseStart.call(this, this.el); - //anim.remove(this.el); - //anim.remove(this._overlay); - //if (this.caption !== '') anim.remove(this._photoCaption); - // disable exit handlers - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleWindowResize); - window.removeEventListener('keyup', this._handleWindowEscape); - this._removeOverlay(); - this._animateImageOut(); - if (this.caption !== '') - this._removeCaption(); - }; -} - -const _defaults$c = { - opacity: 0.5, - inDuration: 250, - outDuration: 250, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true, - dismissible: true, - startingTop: '4%', - endingTop: '10%' -}; -class Modal extends Component { - constructor(el, options) { - super(el, options, Modal); - this.el['M_Modal'] = this; - this.options = { - ...Modal.defaults, - ...options - }; - this.el.tabIndex = 0; - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$c; - } - static init(els, options = {}) { - return super.init(els, options, Modal); - } - static getInstance(el) { - return el['M_Modal']; - } - destroy() { } - _setupEventHandlers() { } - _removeEventHandlers() { } - _handleTriggerClick() { } - _handleOverlayClick() { } - _handleModalCloseClick() { } - _handleKeydown() { } - _handleFocus() { } - open() { - return this; - } - close() { - return this; - } - // Experimental! - static #createHtml(config) { - return ` - ${config.header ? '' : ''} - - ${config.header ? '' : ''} - `; - } - static #createHtmlElement(config) { - const dialog = document.createElement('dialog'); - dialog.id = config.id; - return dialog; - } - static create(config) { - return this.#createHtmlElement(config); - } - static { } -} - -const _defaults$b = { - edge: 'left', - draggable: true, - dragTargetWidth: '10px', - inDuration: 250, - outDuration: 200, - onOpenStart: null, - onOpenEnd: null, - onCloseStart: null, - onCloseEnd: null, - preventScrolling: true -}; -class Sidenav extends Component { - id; - /** Describes open/close state of Sidenav. */ - isOpen; - /** Describes if sidenav is fixed. */ - isFixed; - /** Describes if Sidenav is being dragged. */ - isDragged; - lastWindowWidth; - lastWindowHeight; - static _sidenavs; - _overlay; - dragTarget; - _startingXpos; - _xPos; - _time; - _width; - _initialScrollTop; - _verticallyScrolling; - deltaX; - velocityX; - percentOpen; - constructor(el, options) { - super(el, options, Sidenav); - this.el['M_Sidenav'] = this; - this.options = { - ...Sidenav.defaults, - ...options - }; - this.id = this.el.id; - this.isOpen = false; - this.isFixed = this.el.classList.contains('sidenav-fixed'); - this.isDragged = false; - // Window size variables for window resize checks - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - this._createOverlay(); - this._createDragTarget(); - this._setupEventHandlers(); - this._setupClasses(); - this._setupFixed(); - Sidenav._sidenavs.push(this); - } - static get defaults() { - return _defaults$b; - } - /** - * Initializes instances of Sidenav. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Sidenav); - } - static getInstance(el) { - return el['M_Sidenav']; - } - destroy() { - this._removeEventHandlers(); - this._enableBodyScrolling(); - this._overlay.parentNode.removeChild(this._overlay); - this.dragTarget.parentNode.removeChild(this.dragTarget); - this.el['M_Sidenav'] = undefined; - this.el.style.transform = ''; - const index = Sidenav._sidenavs.indexOf(this); - if (index >= 0) { - Sidenav._sidenavs.splice(index, 1); - } - } - _createOverlay() { - this._overlay = document.createElement('div'); - this._overlay.classList.add('sidenav-overlay'); - this._overlay.addEventListener('click', this.close); - document.body.appendChild(this._overlay); - } - _setupEventHandlers() { - if (Sidenav._sidenavs.length === 0) { - document.body.addEventListener('click', this._handleTriggerClick); - } - const passiveIfSupported = null; - this.dragTarget.addEventListener('touchmove', this._handleDragTargetDrag, passiveIfSupported); - this.dragTarget.addEventListener('touchend', this._handleDragTargetRelease); - this._overlay.addEventListener('touchmove', this._handleCloseDrag, passiveIfSupported); - this._overlay.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('touchmove', this._handleCloseDrag); // , passiveIfSupported); - this.el.addEventListener('touchend', this._handleCloseRelease); - this.el.addEventListener('click', this._handleCloseTriggerClick); - // Add resize for side nav fixed - if (this.isFixed) { - window.addEventListener('resize', this._handleWindowResize); - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - _removeEventHandlers() { - if (Sidenav._sidenavs.length === 1) { - document.body.removeEventListener('click', this._handleTriggerClick); - } - this.dragTarget.removeEventListener('touchmove', this._handleDragTargetDrag); - this.dragTarget.removeEventListener('touchend', this._handleDragTargetRelease); - this._overlay.removeEventListener('touchmove', this._handleCloseDrag); - this._overlay.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('touchmove', this._handleCloseDrag); - this.el.removeEventListener('touchend', this._handleCloseRelease); - this.el.removeEventListener('click', this._handleCloseTriggerClick); - // Remove resize for side nav fixed - if (this.isFixed) { - window.removeEventListener('resize', this._handleWindowResize); - } - } - _handleTriggerClick(e) { - const trigger = e.target.closest('.sidenav-trigger'); - if (e.target && trigger) { - const sidenavId = Utils.getIdFromTrigger(trigger); - const sidenavInstance = document.getElementById(sidenavId)['M_Sidenav']; - if (sidenavInstance) { - sidenavInstance.open(); - } - e.preventDefault(); - } - } - // Set variables needed at the beginning of drag and stop any current transition. - _startDrag(e) { - const clientX = e.targetTouches[0].clientX; - this.isDragged = true; - this._startingXpos = clientX; - this._xPos = this._startingXpos; - this._time = Date.now(); - this._width = this.el.getBoundingClientRect().width; - this._overlay.style.display = 'block'; - this._initialScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this._verticallyScrolling = false; - } - //Set variables needed at each drag move update tick - _dragMoveUpdate(e) { - const clientX = e.targetTouches[0].clientX; - const currentScrollTop = this.isOpen ? this.el.scrollTop : Utils.getDocumentScrollTop(); - this.deltaX = Math.abs(this._xPos - clientX); - this._xPos = clientX; - this.velocityX = this.deltaX / (Date.now() - this._time); - this._time = Date.now(); - if (this._initialScrollTop !== currentScrollTop) { - this._verticallyScrolling = true; - } - } - _handleDragTargetDrag = (e) => { - // Check if draggable - if (!this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - // Don't allow totalDeltaX to exceed Sidenav width or be dragged in the opposite direction - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge === dragDirection) { - totalDeltaX = 0; - } - /** - * transformX is the drag displacement - * transformPrefix is the initial transform placement - * Invert values if Sidenav is right edge - */ - let transformX = totalDeltaX; - let transformPrefix = 'translateX(-100%)'; - if (this.options.edge === 'right') { - transformPrefix = 'translateX(100%)'; - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `${transformPrefix} translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _handleDragTargetRelease = () => { - if (this.isDragged) { - if (this.percentOpen > 0.2) { - this.open(); - } - else { - this._animateOut(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - _handleCloseDrag = (e) => { - // Check if open and draggable - if (!this.isOpen || !this._isDraggable()) - return; - let totalDeltaX = this._calculateDelta(e); - // dragDirection is the attempted user drag direction - const dragDirection = totalDeltaX > 0 ? 'right' : 'left'; - totalDeltaX = Math.min(this._width, Math.abs(totalDeltaX)); - if (this.options.edge !== dragDirection) { - totalDeltaX = 0; - } - let transformX = -totalDeltaX; - if (this.options.edge === 'right') { - transformX = -transformX; - } - // Calculate open/close percentage of sidenav, with open = 1 and close = 0 - this.percentOpen = Math.min(1, 1 - totalDeltaX / this._width); - // Set transform and opacity styles - this.el.style.transform = `translateX(${transformX}px)`; - this._overlay.style.opacity = this.percentOpen.toString(); - }; - _calculateDelta = (e) => { - // If not being dragged, set initial drag start variables - if (!this.isDragged) { - this._startDrag(e); - } - // Run touchmove updates - this._dragMoveUpdate(e); - // Calculate raw deltaX - return this._xPos - this._startingXpos; - }; - _handleCloseRelease = () => { - if (this.isOpen && this.isDragged) { - if (this.percentOpen > 0.8) { - this._animateIn(); - } - else { - this.close(); - } - this.isDragged = false; - this._verticallyScrolling = false; - } - }; - // Handles closing of Sidenav when element with class .sidenav-close - _handleCloseTriggerClick = (e) => { - const closeTrigger = e.target.closest('.sidenav-close'); - if (closeTrigger && !this._isCurrentlyFixed()) { - this.close(); - } - }; - _handleWindowResize = () => { - // Only handle horizontal resizes - if (this.lastWindowWidth !== window.innerWidth) { - if (window.innerWidth > 992) { - this.open(); - } - else { - this.close(); - } - } - this.lastWindowWidth = window.innerWidth; - this.lastWindowHeight = window.innerHeight; - }; - _setupClasses() { - if (this.options.edge === 'right') { - this.el.classList.add('right-aligned'); - this.dragTarget.classList.add('right-aligned'); - } - } - _removeClasses() { - this.el.classList.remove('right-aligned'); - this.dragTarget.classList.remove('right-aligned'); - } - _setupFixed() { - if (this._isCurrentlyFixed()) - this.open(); - } - _isDraggable() { - return this.options.draggable && !this._isCurrentlyFixed() && !this._verticallyScrolling; - } - _isCurrentlyFixed() { - return this.isFixed && window.innerWidth > 992; - } - _createDragTarget() { - const dragTarget = document.createElement('div'); - dragTarget.classList.add('drag-target'); - dragTarget.style.width = this.options.dragTargetWidth; - document.body.appendChild(dragTarget); - this.dragTarget = dragTarget; - } - _preventBodyScrolling() { - document.body.style.overflow = 'hidden'; - } - _enableBodyScrolling() { - document.body.style.overflow = ''; - } - /** - * Opens Sidenav. - */ - open = () => { - if (this.isOpen === true) - return; - this.isOpen = true; - // Run onOpenStart callback - if (typeof this.options.onOpenStart === 'function') { - this.options.onOpenStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - // Show if fixed - this.el.style.transform = 'translateX(0)'; - this._enableBodyScrolling(); - this._overlay.style.display = 'none'; - } - // Handle non-fixed Sidenav - else { - if (this.options.preventScrolling) - this._preventBodyScrolling(); - if (!this.isDragged || this.percentOpen != 1) - this._animateIn(); - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - /** - * Closes Sidenav. - */ - close = () => { - if (this.isOpen === false) - return; - this.isOpen = false; - // Run onCloseStart callback - if (typeof this.options.onCloseStart === 'function') { - this.options.onCloseStart.call(this, this.el); - } - // Handle fixed Sidenav - if (this._isCurrentlyFixed()) { - const transformX = this.options.edge === 'left' ? '-105%' : '105%'; - this.el.style.transform = `translateX(${transformX})`; - } - // Handle non-fixed Sidenav - else { - this._enableBodyScrolling(); - if (!this.isDragged || this.percentOpen != 0) { - this._animateOut(); - } - else { - this._overlay.style.display = 'none'; - } - /* Set aria-hidden state */ - this._setAriaHidden(); - this._setTabIndex(); - } - }; - _animateIn() { - this._animateSidenavIn(); - this._animateOverlayIn(); - } - _animateOut() { - this._animateSidenavOut(); - this._animateOverlayOut(); - } - _animateSidenavIn() { - let slideOutPercent = this.options.edge === 'left' ? -1 : 1; - if (this.isDragged) { - slideOutPercent = - this.options.edge === 'left' - ? slideOutPercent + this.percentOpen - : slideOutPercent - this.percentOpen; - } - const duration = this.options.inDuration; - // from - this.el.style.transition = 'none'; - this.el.style.transform = 'translateX(' + slideOutPercent * 100 + '%)'; - setTimeout(() => { - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(0)'; - }, 1); - setTimeout(() => { - if (typeof this.options.onOpenEnd === 'function') - this.options.onOpenEnd.call(this, this.el); - }, duration); - } - _animateSidenavOut() { - const endPercent = this.options.edge === 'left' ? -1 : 1; - // let slideOutPercent = 0; - // if (this.isDragged) { - // // @todo unused variable - // slideOutPercent = - // this.options.edge === 'left' - // ? endPercent + this.percentOpen - // : endPercent - this.percentOpen; - // } - const duration = this.options.outDuration; - this.el.style.transition = `transform ${duration}ms ease`; // easeOutQuad - // to - this.el.style.transform = 'translateX(' + endPercent * 100 + '%)'; - setTimeout(() => { - if (typeof this.options.onCloseEnd === 'function') - this.options.onCloseEnd.call(this, this.el); - }, duration); - } - _animateOverlayIn() { - let start = 0; - if (this.isDragged) - start = this.percentOpen; - else - this._overlay.style.display = 'block'; - // Animation - const duration = this.options.inDuration; - // from - this._overlay.style.transition = 'none'; - this._overlay.style.opacity = start.toString(); - // easeOutQuad - setTimeout(() => { - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '1'; - }, 1); - } - _animateOverlayOut() { - const duration = this.options.outDuration; - // easeOutQuad - this._overlay.style.transition = `opacity ${duration}ms ease`; - // to - this._overlay.style.opacity = '0'; - setTimeout(() => { - this._overlay.style.display = 'none'; - }, duration); - } - _setAriaHidden = () => { - this.el.ariaHidden = this.isOpen ? 'false' : 'true'; - const navWrapper = document.querySelector('.nav-wrapper ul'); - if (navWrapper) - navWrapper.ariaHidden = this.isOpen.toString(); - }; - _setTabIndex = () => { - const navLinks = document.querySelectorAll('.nav-wrapper ul li a'); - const sideNavLinks = document.querySelectorAll('.sidenav li a'); - if (navLinks) - navLinks.forEach((navLink) => { - navLink.tabIndex = this.isOpen ? -1 : 0; - }); - if (sideNavLinks) - sideNavLinks.forEach((sideNavLink) => { - sideNavLink.tabIndex = this.isOpen ? 0 : -1; - }); - }; - static { - Sidenav._sidenavs = []; - } -} - -const _defaults$a = { - indicators: true, - height: 400, - duration: 500, - interval: 6000, - pauseOnFocus: true, - pauseOnHover: true, - indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA) -}; -class Slider extends Component { - /** Index of current slide. */ - activeIndex; - interval; - eventPause; - _slider; - _slides; - _activeSlide; - _indicators; - _hovered; - _focused; - _focusCurrent; - _sliderId; - constructor(el, options) { - super(el, options, Slider); - this.el['M_Slider'] = this; - this.options = { - ...Slider.defaults, - ...options - }; - // init props - this.interval = null; - this.eventPause = false; - this._hovered = false; - this._focused = false; - this._focusCurrent = false; - // setup - this._slider = this.el.querySelector('.slides'); - this._slides = Array.from(this._slider.querySelectorAll('li')); - this.activeIndex = this._slides.findIndex((li) => li.classList.contains('active')); - if (this.activeIndex !== -1) { - this._activeSlide = this._slides[this.activeIndex]; - } - this._setSliderHeight(); - // Sets element id if it does not have one - if (this._slider.hasAttribute('id')) - this._sliderId = this._slider.getAttribute('id'); - else { - this._sliderId = 'slider-' + Utils.guid(); - this._slider.setAttribute('id', this._sliderId); - } - const placeholderBase64 = 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; - // Set initial positions of captions - this._slides.forEach((slide) => { - // Caption - //const caption = slide.querySelector('.caption'); - //if (caption) this._animateCaptionIn(caption, 0); - // Set Images as Background Images - const img = slide.querySelector('img'); - if (img) { - if (img.src !== placeholderBase64) { - img.style.backgroundImage = 'url(' + img.src + ')'; - img.src = placeholderBase64; - } - } - // Sets slide as focusable by code - if (!slide.hasAttribute('tabindex')) - slide.setAttribute('tabindex', '-1'); - // Removes initial visibility from "inactive" slides - slide.style.visibility = 'hidden'; - }); - this._setupIndicators(); - // Show active slide - if (this._activeSlide) { - this._activeSlide.style.display = 'block'; - this._activeSlide.style.visibility = 'visible'; - } - else { - this.activeIndex = 0; - this._slides[0].classList.add('active'); - this._slides[0].style.visibility = 'visible'; - this._activeSlide = this._slides[0]; - this._animateSlide(this._slides[0], true); - // Update indicators - if (this.options.indicators) { - this._indicators[this.activeIndex].children[0].classList.add('active'); - } - } - this._setupEventHandlers(); - // auto scroll - this.start(); - } - static get defaults() { - return _defaults$a; - } - /** - * Initializes instances of Slider. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Slider); - } - static getInstance(el) { - return el['M_Slider']; - } - destroy() { - this.pause(); - this._removeIndicators(); - this._removeEventHandlers(); - this.el['M_Slider'] = undefined; - } - _setupEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.addEventListener('focusin', this._handleAutoPauseFocus); - this.el.addEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.addEventListener('mouseenter', this._handleAutoPauseHover); - this.el.addEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.addEventListener('click', this._handleIndicatorClick); - }); - } - } - _removeEventHandlers() { - if (this.options.pauseOnFocus) { - this.el.removeEventListener('focusin', this._handleAutoPauseFocus); - this.el.removeEventListener('focusout', this._handleAutoStartFocus); - } - if (this.options.pauseOnHover) { - this.el.removeEventListener('mouseenter', this._handleAutoPauseHover); - this.el.removeEventListener('mouseleave', this._handleAutoStartHover); - } - if (this.options.indicators) { - this._indicators.forEach((el) => { - el.removeEventListener('click', this._handleIndicatorClick); - }); - } - } - _handleIndicatorClick = (e) => { - const el = e.target.parentElement; - const currIndex = [...el.parentNode.children].indexOf(el); - this._focusCurrent = true; - this.set(currIndex); - }; - _handleAutoPauseHover = () => { - this._hovered = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoPauseFocus = () => { - this._focused = true; - if (this.interval != null) { - this._pause(true); - } - }; - _handleAutoStartHover = () => { - this._hovered = false; - if (!(this.options.pauseOnFocus && this._focused) && this.eventPause) { - this.start(); - } - }; - _handleAutoStartFocus = () => { - this._focused = false; - if (!(this.options.pauseOnHover && this._hovered) && this.eventPause) { - this.start(); - } - }; - _handleInterval = () => { - const activeElem = this._slider.querySelector('.active'); - let newActiveIndex = [...activeElem.parentNode.children].indexOf(activeElem); - if (this._slides.length === newActiveIndex + 1) - newActiveIndex = 0; // loop to start - else - newActiveIndex += 1; - this.set(newActiveIndex); - }; - _animateSlide(slide, isDirectionIn) { - let dx = 0, dy = 0; - // from - slide.style.opacity = isDirectionIn ? '0' : '1'; - setTimeout(() => { - slide.style.transition = `opacity ${this.options.duration}ms ease`; - // to - slide.style.opacity = isDirectionIn ? '1' : '0'; - }, 1); - // Caption - const caption = slide.querySelector('.caption'); - if (!caption) - return; - if (caption.classList.contains('center-align')) - dy = -100; - else if (caption.classList.contains('right-align')) - dx = 100; - else if (caption.classList.contains('left-align')) - dx = -100; - // from - caption.style.opacity = isDirectionIn ? '0' : '1'; - caption.style.transform = isDirectionIn ? `translate(${dx}px, ${dy}px)` : `translate(0, 0)`; - setTimeout(() => { - caption.style.transition = `opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`; - // to - caption.style.opacity = isDirectionIn ? '1' : '0'; - caption.style.transform = isDirectionIn ? `translate(0, 0)` : `translate(${dx}px, ${dy}px)`; - }, this.options.duration); // delay - } - _setSliderHeight() { - // If fullscreen, do nothing - if (!this.el.classList.contains('fullscreen')) { - if (this.options.indicators) { - // Add height if indicators are present - this.el.style.height = this.options.height + 40 + 'px'; //.css('height', this.options.height + 40 + 'px'); - } - else { - this.el.style.height = this.options.height + 'px'; - } - this._slider.style.height = this.options.height + 'px'; - } - } - _setupIndicators() { - if (this.options.indicators) { - const ul = document.createElement('ul'); - ul.classList.add('indicators'); - const arrLi = []; - this._slides.forEach((el, i) => { - const label = this.options.indicatorLabelFunc - ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) - : `${i + 1}`; - const li = document.createElement('li'); - li.classList.add('indicator-item'); - li.innerHTML = ``; - arrLi.push(li); - ul.append(li); - }); - this.el.append(ul); - this._indicators = arrLi; - } - } - _removeIndicators() { - this.el.querySelector('ul.indicators').remove(); //find('ul.indicators').remove(); - } - set(index) { - // Wrap around indices. - if (index >= this._slides.length) - index = 0; - else if (index < 0) - index = this._slides.length - 1; - // Only do if index changes - if (this.activeIndex === index) - return; - this._activeSlide = this._slides[this.activeIndex]; - const _caption = this._activeSlide.querySelector('.caption'); - this._activeSlide.classList.remove('active'); - // Enables every slide - this._slides.forEach((slide) => (slide.style.visibility = 'visible')); - //--- Hide active Slide + Caption - this._activeSlide.style.opacity = '0'; - setTimeout(() => { - this._slides.forEach((slide) => { - if (slide.classList.contains('active')) - return; - slide.style.opacity = '0'; - slide.style.transform = 'translate(0, 0)'; - // Disables invisible slides (for assistive technologies) - slide.style.visibility = 'hidden'; - }); - }, this.options.duration); - // Hide active Caption - //this._animateCaptionIn(_caption, this.options.duration); - _caption.style.opacity = '0'; - // Update indicators - if (this.options.indicators) { - const activeIndicator = this._indicators[this.activeIndex].children[0]; - const nextIndicator = this._indicators[index].children[0]; - activeIndicator.classList.remove('active'); - nextIndicator.classList.add('active'); - if (typeof this.options.indicatorLabelFunc === 'function') { - activeIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, this.activeIndex, false); - nextIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, index, true); - } - } - //--- Show new Slide + Caption - this._animateSlide(this._slides[index], true); - this._slides[index].classList.add('active'); - this.activeIndex = index; - // Reset interval, if allowed. This check prevents autostart - // when slider is paused, since it can be changed though indicators. - if (this.interval != null) { - this.start(); - } - } - _pause(fromEvent) { - clearInterval(this.interval); - this.eventPause = fromEvent; - this.interval = null; - } - /** - * Pause slider autoslide. - */ - pause = () => { - this._pause(false); - }; - /** - * Start slider autoslide. - */ - start = () => { - clearInterval(this.interval); - this.interval = setInterval(this._handleInterval, this.options.duration + this.options.interval); - this.eventPause = false; - }; - /** - * Move to next slider. - */ - next = () => { - let newIndex = this.activeIndex + 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; - /** - * Move to prev slider. - */ - prev = () => { - let newIndex = this.activeIndex - 1; - // Wrap around indices. - if (newIndex >= this._slides.length) - newIndex = 0; - else if (newIndex < 0) - newIndex = this._slides.length - 1; - this.set(newIndex); - }; -} - -const _defaults$9 = { - duration: 300, - onShow: null, - swipeable: false, - responsiveThreshold: Infinity // breakpoint for swipeable -}; -class Tabs extends Component { - _tabLinks; - _index; - _indicator; - _tabWidth; - _tabsWidth; - _tabsCarousel; - _activeTabLink; - _content; - constructor(el, options) { - super(el, options, Tabs); - this.el['M_Tabs'] = this; - this.options = { - ...Tabs.defaults, - ...options - }; - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - this._index = 0; - this._setupActiveTabLink(); - if (this.options.swipeable) { - this._setupSwipeableTabs(); - } - else { - this._setupNormalTabs(); - } - // Setup tabs indicator after content to ensure accurate widths - this._setTabsAndTabWidth(); - this._createIndicator(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$9; - } - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tabs); - } - static getInstance(el) { - return el['M_Tabs']; - } - destroy() { - this._removeEventHandlers(); - this._indicator.parentNode.removeChild(this._indicator); - if (this.options.swipeable) { - this._teardownSwipeableTabs(); - } - else { - this._teardownNormalTabs(); - } - this.el['M_Tabs'] = undefined; - } - /** - * The index of tab that is currently shown. - */ - get index() { - return this._index; - } - _setupEventHandlers() { - window.addEventListener('resize', this._handleWindowResize); - this.el.addEventListener('click', this._handleTabClick); - // map vertical scrolling to horizontal scrolling because no scrollbar is shown on desktop - this.el.addEventListener('wheel', (event) => { - if (event.deltaY !== 0) { - event.preventDefault(); - this.el.scrollLeft += event.deltaY * 1; - } - }); - } - _removeEventHandlers() { - window.removeEventListener('resize', this._handleWindowResize); - this.el.removeEventListener('click', this._handleTabClick); - } - _handleWindowResize = () => { - this._setTabsAndTabWidth(); - if (this._tabWidth !== 0 && this._tabsWidth !== 0) { - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - }; - _handleTabClick = (e) => { - let tabLink = e.target; - if (!tabLink) - return; - let tab = tabLink.parentElement; - while (tab && !tab.classList.contains('tab')) { - tabLink = tabLink.parentElement; - tab = tab.parentElement; - } - // Handle click on tab link only - if (!tabLink || !tab.classList.contains('tab')) - return; - // is disabled? - if (tab.classList.contains('disabled')) { - e.preventDefault(); - return; - } - // Act as regular link if target attribute is specified. - if (tabLink.hasAttribute('target')) - return; - // Make the old tab inactive. - this._activeTabLink.classList.remove('active'); - const _oldContent = this._content; - // Update the variables with the new link and content - this._activeTabLink = tabLink; - if (tabLink.hash) - this._content = document.querySelector(tabLink.hash); - this._tabLinks = this.el.querySelectorAll('li.tab > a'); - // Make the tab active - this._activeTabLink.classList.add('active'); - const prevIndex = this._index; - this._index = Math.max(Array.from(this._tabLinks).indexOf(tabLink), 0); - // Swap content - if (this.options.swipeable) { - if (this._tabsCarousel) { - this._tabsCarousel.set(this._index, () => { - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - }); - } - } - else { - if (this._content) { - this._content.style.display = 'block'; - this._content.classList.add('active'); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - if (_oldContent && _oldContent !== this._content) { - _oldContent.style.display = 'none'; - _oldContent.classList.remove('active'); - } - } - } - // Update widths after content is swapped (scrollbar bugfix) - this._setTabsAndTabWidth(); - this._animateIndicator(prevIndex); - e.preventDefault(); - }; - _createIndicator() { - const indicator = document.createElement('li'); - indicator.classList.add('indicator'); - this.el.appendChild(indicator); - this._indicator = indicator; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - _setupActiveTabLink() { - // If the location.hash matches one of the links, use that as the active tab. - this._activeTabLink = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === location.hash); - // If no match is found, use the first link or any with class 'active' as the initial active tab. - if (!this._activeTabLink) { - let activeTabLink = this.el.querySelector('li.tab a.active'); - if (!activeTabLink) { - activeTabLink = this.el.querySelector('li.tab a'); - } - this._activeTabLink = activeTabLink; - } - Array.from(this._tabLinks).forEach((a) => a.classList.remove('active')); - this._activeTabLink.classList.add('active'); - this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); - if (this._activeTabLink && this._activeTabLink.hash) { - this._content = document.querySelector(this._activeTabLink.hash); - if (this._content) - this._content.classList.add('active'); - } - } - _setupSwipeableTabs() { - // Change swipeable according to responsive threshold - if (window.innerWidth > this.options.responsiveThreshold) - this.options.swipeable = false; - const tabsContent = []; - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - currContent.classList.add('carousel-item'); - tabsContent.push(currContent); - } - }); - // Create Carousel-Wrapper around Tab-Contents - const tabsWrapper = document.createElement('div'); - tabsWrapper.classList.add('tabs-content', 'carousel', 'carousel-slider'); - // Wrap around - tabsContent[0].parentElement.insertBefore(tabsWrapper, tabsContent[0]); - tabsContent.forEach((tabContent) => { - tabsWrapper.appendChild(tabContent); - tabContent.style.display = ''; - }); - // Keep active tab index to set initial carousel slide - const tab = this._activeTabLink.parentElement; - const activeTabIndex = Array.from(tab.parentNode.children).indexOf(tab); - this._tabsCarousel = Carousel.init(tabsWrapper, { - fullWidth: true, - noWrap: true, - onCycleTo: (item) => { - const prevIndex = this._index; - this._index = Array.from(item.parentNode.children).indexOf(item); - this._activeTabLink.classList.remove('active'); - this._activeTabLink = Array.from(this._tabLinks)[this._index]; - this._activeTabLink.classList.add('active'); - this._animateIndicator(prevIndex); - if (typeof this.options.onShow === 'function') - this.options.onShow.call(this, this._content); - } - }); - // Set initial carousel slide to active tab - this._tabsCarousel.set(activeTabIndex); - } - _teardownSwipeableTabs() { - const tabsWrapper = this._tabsCarousel.el; - this._tabsCarousel.destroy(); - // Unwrap - tabsWrapper.append(tabsWrapper.parentElement); - tabsWrapper.remove(); - } - _setupNormalTabs() { - // Hide Tabs Content - Array.from(this._tabLinks).forEach((a) => { - if (a === this._activeTabLink) - return; - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = 'none'; - } - }); - } - _teardownNormalTabs() { - // show Tabs Content - this._tabLinks.forEach((a) => { - if (a.hash) { - const currContent = document.querySelector(a.hash); - if (currContent) - currContent.style.display = ''; - } - }); - } - _setTabsAndTabWidth() { - this._tabsWidth = this.el.getBoundingClientRect().width; - this._tabWidth = Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length; - } - _calcRightPos(el) { - return Math.ceil(this._tabsWidth - el.offsetLeft - el.getBoundingClientRect().width); - } - _calcLeftPos(el) { - return Math.floor(el.offsetLeft); - } - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ - updateTabIndicator() { - this._setTabsAndTabWidth(); - this._animateIndicator(this._index); - } - _animateIndicator(prevIndex) { - let leftDelay = 0, rightDelay = 0; - const isMovingLeftOrStaying = this._index - prevIndex >= 0; - if (isMovingLeftOrStaying) - leftDelay = 90; - else - rightDelay = 90; - // in v1: easeOutQuad - this._indicator.style.transition = ` - left ${this.options.duration}ms ease-out ${leftDelay}ms, - right ${this.options.duration}ms ease-out ${rightDelay}ms`; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; - } - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ - select(tabId) { - const tab = Array.from(this._tabLinks).find((a) => a.getAttribute('href') === '#' + tabId); - if (tab) - tab.click(); - } -} - -const _defaults$8 = { - dialRadius: 135, - outerRadius: 105, - innerRadius: 70, - tickRadius: 20, - duration: 350, - container: null, - defaultTime: 'now', // default time, 'now' or '13:14' e.g. - fromNow: 0, // Millisecond offset from the defaultTime - showClearBtn: false, - autoSubmit: true, - // internationalization - i18n: { - cancel: 'Cancel', - clear: 'Clear', - done: 'Ok' - }, - twelveHour: true, // change to 12 hour AM/PM clock from 24 hour - vibrate: true, // vibrate the device when dragging clock hand - // Callbacks - onSelect: null, - onInputInteraction: null, - onDone: null, - onCancel: null, - displayPlugin: null, - displayPluginOptions: null -}; -class Timepicker extends Component { - id; - containerEl; - plate; - digitalClock; - inputHours; - inputMinutes; - x0; - y0; - moved; - dx; - dy; - /** - * Current view on the timepicker. - * @default 'hours' - */ - currentView; - hand; - minutesView; - hours; - minutes; - /** The selected time. */ - time; - /** - * If the time is AM or PM on twelve-hour clock. - * @default 'PM' - */ - amOrPm; - static _template; - /** Vibrate device when dragging clock hand. */ - vibrate; - _canvas; - hoursView; - spanAmPm; - footer; - _amBtn; - _pmBtn; - bg; - bearing; - g; - toggleViewTimer; - vibrateTimer; - displayPlugin; - constructor(el, options) { - super(el, options, Timepicker); - this.el['M_Timepicker'] = this; - this.options = { - ...Timepicker.defaults, - ...options - }; - this.id = Utils.guid(); - this._insertHTMLIntoDOM(); - this._setupVariables(); - this._setupEventHandlers(); - this._clockSetup(); - if (this.options.displayPlugin) - this._setupDisplayPlugin(); - this._pickerSetup(); - } - static get defaults() { - return _defaults$8; - } - /** - * Initializes instances of Timepicker. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Timepicker); - } - static _addLeadingZero(num) { - return (num < 10 ? '0' : '') + num; - } - static _createSVGEl(name) { - const svgNS = 'http://www.w3.org/2000/svg'; - return document.createElementNS(svgNS, name); - } - static _Pos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return { - x: e.targetTouches[0].clientX, - y: e.targetTouches[0].clientY - }; - } - // mouse event - return { x: e.clientX, y: e.clientY }; - } - static getInstance(el) { - return el['M_Timepicker']; - } - destroy() { - this._removeEventHandlers(); - this.containerEl.remove(); - this.el['M_Timepicker'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('click', this._handleInputClick); - // @todo allow input field to fill values from input field when container/modal opens - this.el.addEventListener('keydown', this._handleInputKeydown); - this.plate.addEventListener('mousedown', this._handleClockClickStart); - this.plate.addEventListener('touchstart', this._handleClockClickStart); - this.digitalClock.addEventListener('keyup', this._inputFromTextField); - this.inputHours.addEventListener('focus', () => this.showView('hours')); - this.inputHours.addEventListener('focusout', () => this.formatHours()); - this.inputMinutes.addEventListener('focus', () => this.showView('minutes')); - this.inputMinutes.addEventListener('focusout', () => this.formatMinutes()); - } - _removeEventHandlers() { - this.el.removeEventListener('click', this._handleInputClick); - this.el.removeEventListener('keydown', this._handleInputKeydown); - } - _handleInputClick = () => { - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - }; - _handleInputKeydown = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this.inputHours.focus(); - if (typeof this.options.onInputInteraction === 'function') - this.options.onInputInteraction.call(this); - if (this.displayPlugin) - this.displayPlugin.show(); - } - }; - _handleTimeInputEnterKey = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - e.preventDefault(); - this._inputFromTextField(); - } - }; - _handleClockClickStart = (e) => { - e.preventDefault(); - const clockPlateBR = this.plate.getBoundingClientRect(); - const offset = { x: clockPlateBR.left, y: clockPlateBR.top }; - this.x0 = offset.x + this.options.dialRadius; - this.y0 = offset.y + this.options.dialRadius; - this.moved = false; - const clickPos = Timepicker._Pos(e); - this.dx = clickPos.x - this.x0; - this.dy = clickPos.y - this.y0; - // Set clock hands - this.setHand(this.dx, this.dy, false); - // Mousemove on document - document.addEventListener('mousemove', this._handleDocumentClickMove); - document.addEventListener('touchmove', this._handleDocumentClickMove); - // Mouseup on document - document.addEventListener('mouseup', this._handleDocumentClickEnd); - document.addEventListener('touchend', this._handleDocumentClickEnd); - }; - _handleDocumentClickMove = (e) => { - e.preventDefault(); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - this.moved = true; - this.setHand(x, y, false); - }; - _handleDocumentClickEnd = (e) => { - e.preventDefault(); - document.removeEventListener('mouseup', this._handleDocumentClickEnd); - document.removeEventListener('touchend', this._handleDocumentClickEnd); - const clickPos = Timepicker._Pos(e); - const x = clickPos.x - this.x0; - const y = clickPos.y - this.y0; - if (this.moved && x === this.dx && y === this.dy) { - this.setHand(x, y); - } - if (this.currentView === 'hours') { - this.inputMinutes.focus(); - this.showView('minutes', this.options.duration / 2); - } - else { - // this.minutesView.classList.add('timepicker-dial-out'); - setTimeout(() => { - if (this.options.autoSubmit) - this.done(); - }, this.options.duration / 2); - } - if (typeof this.options.onSelect === 'function') { - this.options.onSelect.call(this, this.hours, this.minutes); - } - // Unbind mousemove event - document.removeEventListener('mousemove', this._handleDocumentClickMove); - document.removeEventListener('touchmove', this._handleDocumentClickMove); - }; - _insertHTMLIntoDOM() { - const template = document.createElement('template'); - template.innerHTML = Timepicker._template.trim(); - this.containerEl = template.content.firstChild; - this.containerEl.id = 'container-' + this.id; - // Append popover to input by default - const optEl = this.options.container; - const containerEl = optEl instanceof HTMLElement ? optEl : document.querySelector(optEl); - if (this.options.container && !!containerEl) { - containerEl.append(this.containerEl); - } - else { - this.el.parentElement.appendChild(this.containerEl); - } - } - _setupVariables() { - this.currentView = 'hours'; - this.vibrate = navigator.vibrate - ? 'vibrate' - : navigator['webkitVibrate'] - ? 'webkitVibrate' - : null; - this._canvas = this.containerEl.querySelector('.timepicker-canvas'); - this.plate = this.containerEl.querySelector('.timepicker-plate'); - this.digitalClock = this.containerEl.querySelector('.timepicker-display-column'); - this.hoursView = this.containerEl.querySelector('.timepicker-hours'); - this.minutesView = this.containerEl.querySelector('.timepicker-minutes'); - this.inputHours = this.containerEl.querySelector('.timepicker-input-hours'); - this.inputMinutes = this.containerEl.querySelector('.timepicker-input-minutes'); - this.spanAmPm = this.containerEl.querySelector('.timepicker-span-am-pm'); - this.footer = this.containerEl.querySelector('.timepicker-footer'); - this.amOrPm = 'PM'; - } - /*private _createButton(text: string, visibility: string): HTMLButtonElement { - const button = document.createElement('button'); - button.classList.add('btn', 'waves-effect', 'text'); - button.style.visibility = visibility; - button.type = 'button'; - button.tabIndex = -1; - button.innerText = text; - return button; - }*/ - _pickerSetup() { - // clearButton.classList.add('timepicker-clear'); - // clearButton.addEventListener('click', this.clear); - // this.footer.appendChild(clearButton); - if (this.options.showClearBtn) { - Utils.createButton(this.footer, this.options.i18n.clear, ['timepicker-clear'], true, this.clear); - } - if (!this.options.autoSubmit) { - /*const confirmationBtnsContainer = document.createElement('div'); - confirmationBtnsContainer.classList.add('confirmation-btns'); - this.footer.append(confirmationBtnsContainer); - - const cancelButton = this._createButton(this.options.i18n.cancel, ''); - cancelButton.classList.add('timepicker-close'); - cancelButton.addEventListener('click', this.close); - confirmationBtnsContainer.appendChild(cancelButton); - - const doneButton = this._createButton(this.options.i18n.done, ''); - doneButton.classList.add('timepicker-close'); - //doneButton.addEventListener('click', this._finishSelection); - confirmationBtnsContainer.appendChild(doneButton);*/ - Utils.createConfirmationContainer(this.footer, this.options.i18n.done, this.options.i18n.cancel, this.confirm, this.cancel); - } - this._updateTimeFromInput(); - this.showView('hours'); - } - _setupDisplayPlugin() { - if (this.options.displayPlugin === 'docked') - this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); - if (this.options.displayPlugin === 'modal') { - this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { - ...this.options.displayPluginOptions, - ...{ classList: ['timepicker-modal'] } - }); - this.footer.remove(); - this.footer = this.displayPlugin.footer; - } - } - _clockSetup() { - if (this.options.twelveHour) { - // AM Button - this._amBtn = document.createElement('div'); - this._amBtn.classList.add('am-btn', 'btn'); - this._amBtn.innerText = 'AM'; - this._amBtn.tabIndex = 0; - this._amBtn.addEventListener('click', this._handleAmPmClick); - this._amBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._amBtn); - // PM Button - this._pmBtn = document.createElement('div'); - this._pmBtn.classList.add('pm-btn', 'btn'); - this._pmBtn.innerText = 'PM'; - this._pmBtn.tabIndex = 0; - this._pmBtn.addEventListener('click', this._handleAmPmClick); - this._pmBtn.addEventListener('keypress', this._handleAmPmKeypress); - this.spanAmPm.appendChild(this._pmBtn); - } - this._buildHoursView(); - this._buildMinutesView(); - this._buildSVGClock(); - } - _buildSVGClock() { - // Draw clock hands and others - const dialRadius = this.options.dialRadius; - const tickRadius = this.options.tickRadius; - const diameter = dialRadius * 2; - const svg = Timepicker._createSVGEl('svg'); - svg.setAttribute('class', 'timepicker-svg'); - svg.setAttribute('width', diameter.toString()); - svg.setAttribute('height', diameter.toString()); - const g = Timepicker._createSVGEl('g'); - g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')'); - const bearing = Timepicker._createSVGEl('circle'); - bearing.setAttribute('class', 'timepicker-canvas-bearing'); - bearing.setAttribute('cx', '0'); - bearing.setAttribute('cy', '0'); - bearing.setAttribute('r', '4'); - const hand = Timepicker._createSVGEl('line'); - hand.setAttribute('x1', '0'); - hand.setAttribute('y1', '0'); - const bg = Timepicker._createSVGEl('circle'); - bg.setAttribute('class', 'timepicker-canvas-bg'); - bg.setAttribute('r', tickRadius.toString()); - g.appendChild(hand); - g.appendChild(bg); - g.appendChild(bearing); - svg.appendChild(g); - this._canvas.appendChild(svg); - this.hand = hand; - this.bg = bg; - this.bearing = bearing; - this.g = g; - } - _buildHoursView() { - // const $tick = document.createElement('div'); - // $tick.classList.add('timepicker-tick'); - // Hours view - if (this.options.twelveHour) { - for (let i = 1; i < 13; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const radius = this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - else { - for (let i = 0; i < 24; i += 1) { - // const tick = $tick.cloneNode(true); - const radian = (i / 6) * Math.PI; - const inner = i > 0 && i < 13; - const radius = inner ? this.options.innerRadius : this.options.outerRadius; - this._buildHoursTick(i, radian, radius); - } - } - } - _buildHoursTick(i, radian, radius) { - const tick = document.createElement('div'); - tick.classList.add('timepicker-tick'); - tick.style.left = - this.options.dialRadius + Math.sin(radian) * radius - this.options.tickRadius + 'px'; - tick.style.top = - this.options.dialRadius - Math.cos(radian) * radius - this.options.tickRadius + 'px'; - tick.innerHTML = i === 0 ? '00' : i.toString(); - this.hoursView.appendChild(tick); - } - _buildMinutesView() { - const _tick = document.createElement('div'); - _tick.classList.add('timepicker-tick'); - // Minutes view - for (let i = 0; i < 60; i += 5) { - const tick = _tick.cloneNode(true); - const radian = (i / 30) * Math.PI; - tick.style.left = - this.options.dialRadius + - Math.sin(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.style.top = - this.options.dialRadius - - Math.cos(radian) * this.options.outerRadius - - this.options.tickRadius + - 'px'; - tick.innerHTML = Timepicker._addLeadingZero(i); - this.minutesView.appendChild(tick); - } - } - _handleAmPmClick = (e) => { - this._handleAmPmInteraction(e.target); - }; - _handleAmPmKeypress = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleAmPmInteraction(e.target); - } - }; - _handleAmPmInteraction = (e) => { - this.amOrPm = e.classList.contains('am-btn') ? 'AM' : 'PM'; - this._updateAmPmView(); - }; - _updateAmPmView() { - if (this.options.twelveHour) { - if (this.amOrPm === 'PM') { - this._amBtn.classList.remove('filled'); - this._pmBtn.classList.add('filled'); - } - else if (this.amOrPm === 'AM') { - this._amBtn.classList.add('filled'); - this._pmBtn.classList.remove('filled'); - } - } - } - _updateTimeFromInput() { - // Get the time - let value = ((this.el.value || this.options.defaultTime || '') + '').split(':'); - if (this.options.twelveHour && !(typeof value[1] === 'undefined')) { - if (value[1].toUpperCase().indexOf('AM') > 0) { - this.amOrPm = 'AM'; - } - else { - this.amOrPm = 'PM'; - } - value[1] = value[1].replace('AM', '').replace('PM', ''); - } - if (value[0] === 'now') { - const now = new Date(+new Date() + this.options.fromNow); - value = [now.getHours().toString(), now.getMinutes().toString()]; - if (this.options.twelveHour) { - this.amOrPm = parseInt(value[0]) >= 12 && parseInt(value[0]) < 24 ? 'PM' : 'AM'; - } - } - this.hours = +value[0] || 0; - this.minutes = +value[1] || 0; - this.inputHours.value = Timepicker._addLeadingZero(this.hours); - this.inputMinutes.value = Timepicker._addLeadingZero(this.minutes); - this._updateAmPmView(); - } - /** - * Show hours or minutes view on timepicker. - * @param view The name of the view you want to switch to, 'hours' or 'minutes'. - * @param delay - */ - showView = (view, delay = null) => { - if (view === 'minutes' && getComputedStyle(this.hoursView).visibility === 'visible') ; - const isHours = view === 'hours', nextView = isHours ? this.hoursView : this.minutesView, hideView = isHours ? this.minutesView : this.hoursView; - this.currentView = view; - /*if (isHours) { - this.inputHours.classList.add('text-primary'); - this.inputMinutes.classList.remove('text-primary'); - } else { - this.inputHours.classList.remove('text-primary'); - this.inputMinutes.classList.add('text-primary'); - }*/ - // Transition view - hideView.classList.add('timepicker-dial-out'); - nextView.style.visibility = 'visible'; - nextView.classList.remove('timepicker-dial-out'); - // Reset clock hand - this.resetClock(delay); - // After transitions ended - clearTimeout(this.toggleViewTimer); - this.toggleViewTimer = setTimeout(() => { - hideView.style.visibility = 'hidden'; - }, this.options.duration); - }; - resetClock(delay) { - const view = this.currentView, value = this[view], isHours = view === 'hours', unit = Math.PI / (isHours ? 6 : 30), radian = value * unit, radius = isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius, x = Math.sin(radian) * radius, y = -Math.cos(radian) * radius; - if (delay) { - this._canvas?.classList.add('timepicker-canvas-out'); - setTimeout(() => { - this._canvas?.classList.remove('timepicker-canvas-out'); - this.setHand(x, y); - }, delay); - } - else { - this.setHand(x, y); - } - } - _inputFromTextField = () => { - const isHours = this.currentView === 'hours'; - if (isHours && this.inputHours.value !== '') { - const value = parseInt(this.inputHours.value); - if (value > 0 && value < (this.options.twelveHour ? 13 : 24)) { - this.hours = value; - } - else { - this.setHoursDefault(); - } - this.drawClockFromTimeInput(this.hours, isHours); - } - else if (!isHours && this.inputMinutes.value !== '') { - const value = parseInt(this.inputMinutes.value); - if (value >= 0 && value < 60) { - this.minutes = value; - } - else { - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = this.minutes.toString(); - } - this.drawClockFromTimeInput(this.minutes, isHours); - } - }; - drawClockFromTimeInput(value, isHours) { - const unit = Math.PI / (isHours ? 6 : 30); - const radian = value * unit; - let radius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - else { - radius = - isHours && value > 0 && value < 13 ? this.options.innerRadius : this.options.outerRadius; - } - this.setClockAttributes(radian, radius); - } - setHand(x, y, roundBy5 = false) { - const isHours = this.currentView === 'hours', unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), inner = isHours && z < (this.options.outerRadius + this.options.innerRadius) / 2; - let radian = Math.atan2(x, -y), radius = inner ? this.options.innerRadius : this.options.outerRadius; - if (this.options.twelveHour) { - radius = this.options.outerRadius; - } - // Radian should in range [0, 2PI] - if (radian < 0) { - radian = Math.PI * 2 + radian; - } - // Get the round value - let value = Math.round(radian / unit); - // Get the round radian - radian = value * unit; - // Correct the hours or minutes - if (this.options.twelveHour) { - if (isHours) { - if (value === 0) - value = 12; - } - else { - if (roundBy5) - value *= 5; - if (value === 60) - value = 0; - } - } - else { - if (isHours) { - if (value === 12) { - value = 0; - } - value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12; - } - else { - if (roundBy5) { - value *= 5; - } - if (value === 60) { - value = 0; - } - } - } - // Once hours or minutes changed, vibrate the device - if (this[this.currentView] !== value) { - if (this.vibrate && this.options.vibrate) { - // Do not vibrate too frequently - if (!this.vibrateTimer) { - navigator[this.vibrate](10); - this.vibrateTimer = setTimeout(() => { - this.vibrateTimer = null; - }, 100); - } - } - } - this[this.currentView] = value; - if (isHours) { - this.inputHours.value = Timepicker._addLeadingZero(value); - } - else { - this.inputMinutes.value = Timepicker._addLeadingZero(value); - } - // Set clock hand and others' position - this.setClockAttributes(radian, radius); - } - setClockAttributes(radian, radius) { - const cx1 = Math.sin(radian) * (radius - this.options.tickRadius), cy1 = -Math.cos(radian) * (radius - this.options.tickRadius), cx2 = Math.sin(radian) * radius, cy2 = -Math.cos(radian) * radius; - this.hand.setAttribute('x2', cx1.toString()); - this.hand.setAttribute('y2', cy1.toString()); - this.bg.setAttribute('cx', cx2.toString()); - this.bg.setAttribute('cy', cy2.toString()); - } - formatHours() { - if (this.inputHours.value == '') - this.setHoursDefault(); - this.inputHours.value = Timepicker._addLeadingZero(Number(this.inputHours.value)); - } - formatMinutes() { - if (this.inputMinutes.value == '') - this.minutes = new Date().getMinutes(); - this.inputMinutes.value = Timepicker._addLeadingZero(Number(this.inputMinutes.value)); - } - setHoursDefault() { - this.hours = new Date().getHours(); - this.inputHours.value = (this.hours % (this.options.twelveHour ? 12 : 24)).toString(); - } - done = (clearValue = null) => { - // Set input value - const last = this.el.value; - let value = clearValue - ? '' - : Timepicker._addLeadingZero(this.hours) + ':' + Timepicker._addLeadingZero(this.minutes); - this.time = value; - if (!clearValue && this.options.twelveHour) { - value = `${value} ${this.amOrPm}`; - } - this.el.value = value; - // Trigger change event - if (value !== last) { - this.el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true, composed: true })); - } - }; - confirm = () => { - this.done(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onDone === 'function') - this.options.onDone.call(this); - }; - cancel = () => { - // not logical clearing the input field on cancel, since the end user might want to make use of the previously submitted value - // this.clear(); - if (this.displayPlugin) - this.displayPlugin.hide(); - if (typeof this.options.onCancel === 'function') - this.options.onCancel.call(this); - }; - clear = () => { - // @todo should clear timepicker hour/minute input elems and reset analog clock, currently clears input el - this.done(true); - }; - // deprecated - open() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - close() { - console.warn('Timepicker.close() is deprecated. Remove this method and wrap in modal yourself.'); - return this; - } - static { - Timepicker._template = `
-
-
-
-
- -
-
- : -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
`; - } -} - -const _defaults$7 = { - text: '', - displayLength: 4000, - inDuration: 300, - outDuration: 375, - classes: '', - completeCallback: null, - activationPercent: 0.8 -}; -class Toast { - /** The toast element. */ - el; - /** - * The remaining amount of time in ms that the toast - * will stay before dismissal. - */ - timeRemaining; - /** - * Describes the current pan state of the Toast. - */ - panning; - options; - message; - counterInterval; - wasSwiped; - startingXPos; - xPos; - time; - deltaX; - velocityX; - static _toasts; - static _container; - static _draggedToast; - constructor(options) { - this.options = { - ...Toast.defaults, - ...options - }; - this.message = this.options.text; - this.panning = false; - this.timeRemaining = this.options.displayLength; - if (Toast._toasts.length === 0) { - Toast._createContainer(); - } - // Create new toast - Toast._toasts.push(this); - const toastElement = this._createToast(); - toastElement['M_Toast'] = this; - this.el = toastElement; - this._animateIn(); - this._setTimer(); - } - static get defaults() { - return _defaults$7; - } - static getInstance(el) { - return el['M_Toast']; - } - static _createContainer() { - const container = document.createElement('div'); - container.setAttribute('id', 'toast-container'); - // Add event handler - container.addEventListener('touchstart', Toast._onDragStart); - container.addEventListener('touchmove', Toast._onDragMove); - container.addEventListener('touchend', Toast._onDragEnd); - container.addEventListener('mousedown', Toast._onDragStart); - document.addEventListener('mousemove', Toast._onDragMove); - document.addEventListener('mouseup', Toast._onDragEnd); - document.body.appendChild(container); - Toast._container = container; - } - static _removeContainer() { - document.removeEventListener('mousemove', Toast._onDragMove); - document.removeEventListener('mouseup', Toast._onDragEnd); - if (Toast._container) { - Toast._container.remove(); - Toast._container = null; - } - } - static _onDragStart(e) { - if (e.target && e.target.closest('.toast')) { - const toastElem = e.target.closest('.toast'); - const toast = toastElem['M_Toast']; - toast.panning = true; - Toast._draggedToast = toast; - toast.el.classList.add('panning'); - toast.el.style.transition = ''; - toast.startingXPos = Toast._xPos(e); - toast.time = Date.now(); - toast.xPos = Toast._xPos(e); - } - } - static _onDragMove(e) { - if (!!Toast._draggedToast) { - e.preventDefault(); - const toast = Toast._draggedToast; - toast.deltaX = Math.abs(toast.xPos - Toast._xPos(e)); - toast.xPos = Toast._xPos(e); - toast.velocityX = toast.deltaX / (Date.now() - toast.time); - toast.time = Date.now(); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - toast.el.style.transform = `translateX(${totalDeltaX}px)`; - toast.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString(); - } - } - static _onDragEnd() { - if (!!Toast._draggedToast) { - const toast = Toast._draggedToast; - toast.panning = false; - toast.el.classList.remove('panning'); - const totalDeltaX = toast.xPos - toast.startingXPos; - const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; - const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || toast.velocityX > 1; - // Remove toast - if (shouldBeDismissed) { - toast.wasSwiped = true; - toast.dismiss(); - // Animate toast back to original position - } - else { - toast.el.style.transition = 'transform .2s, opacity .2s'; - toast.el.style.transform = ''; - toast.el.style.opacity = ''; - } - Toast._draggedToast = null; - } - } - static _xPos(e) { - if (e.type.startsWith('touch') && e.targetTouches.length >= 1) { - return e.targetTouches[0].clientX; - } - // mouse event - return e.clientX; - } - /** - * dismiss all toasts. - */ - static dismissAll() { - for (const toastIndex in Toast._toasts) { - Toast._toasts[toastIndex].dismiss(); - } - } - _createToast() { - let toast = this.options.toastId - ? document.getElementById(this.options.toastId) - : document.createElement('div'); - if (toast instanceof HTMLTemplateElement) { - const node = toast.content.cloneNode(true); - toast = node.firstElementChild; - } - toast.classList.add('toast'); - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - toast.setAttribute('aria-atomic', 'true'); - // Add custom classes onto toast - if (this.options.classes.length > 0) { - toast.classList.add(...this.options.classes.split(' ')); - } - if (this.message) - toast.innerText = this.message; - if (Toast._container) { - Toast._container.appendChild(toast); - } - return toast; - } - _animateIn() { - // Animate toast in - this.el.style.display = ''; - this.el.style.opacity = '0'; - // easeOutCubic - this.el.style.transition = ` - top ${this.options.inDuration}ms ease, - opacity ${this.options.inDuration}ms ease - `; - setTimeout(() => { - this.el.style.top = '0'; - this.el.style.opacity = '1'; - }, 1); - } - /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. - */ - _setTimer() { - if (this.timeRemaining !== Infinity) { - this.counterInterval = setInterval(() => { - // If toast is not being dragged, decrease its time remaining - if (!this.panning) { - this.timeRemaining -= 20; - } - // Animate toast out - if (this.timeRemaining <= 0) { - this.dismiss(); - } - }, 20); - } - } - /** - * Dismiss toast with animation. - */ - dismiss() { - clearInterval(this.counterInterval); - const activationDistance = this.el.offsetWidth * this.options.activationPercent; - if (this.wasSwiped) { - this.el.style.transition = 'transform .05s, opacity .05s'; - this.el.style.transform = `translateX(${activationDistance}px)`; - this.el.style.opacity = '0'; - } - // easeOutExpo - this.el.style.transition = ` - margin ${this.options.outDuration}ms ease, - opacity ${this.options.outDuration}ms ease`; - setTimeout(() => { - this.el.style.opacity = '0'; - this.el.style.marginTop = '-40px'; - }, 1); - setTimeout(() => { - // Call the optional callback - if (typeof this.options.completeCallback === 'function') { - this.options.completeCallback(); - } - // Remove toast from DOM - if (this.el.id != this.options.toastId) { - this.el.remove(); - Toast._toasts.splice(Toast._toasts.indexOf(this), 1); - if (Toast._toasts.length === 0) { - Toast._removeContainer(); - } - } - }, this.options.outDuration); - } - static { - Toast._toasts = []; - Toast._container = null; - Toast._draggedToast = null; - } -} - -const _defaults$6 = { - exitDelay: 200, - enterDelay: 0, - text: '', - margin: 5, - inDuration: 250, - outDuration: 200, - position: 'bottom', - transitionMovement: 10, - opacity: 1 -}; -class Tooltip extends Component { - /** - * If tooltip is open. - */ - isOpen; - /** - * If tooltip is hovered. - */ - isHovered; - /** - * If tooltip is focused. - */ - isFocused; - tooltipEl; - _exitDelayTimeout; - _enterDelayTimeout; - xMovement; - yMovement; - constructor(el, options) { - super(el, options, Tooltip); - this.el['M_Tooltip'] = this; - this.options = { - ...Tooltip.defaults, - ...this._getAttributeOptions(), - ...options - }; - this.isOpen = false; - this.isHovered = false; - this.isFocused = false; - this._appendTooltipEl(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$6; - } - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Tooltip); - } - static getInstance(el) { - return el['M_Tooltip']; - } - destroy() { - this.tooltipEl.remove(); - this._removeEventHandlers(); - this.el['M_Tooltip'] = undefined; - } - _appendTooltipEl() { - this.tooltipEl = document.createElement('div'); - this.tooltipEl.classList.add('material-tooltip'); - const tooltipContentEl = this.options.tooltipId - ? document.getElementById(this.options.tooltipId) - : document.createElement('div'); - this.tooltipEl.append(tooltipContentEl); - tooltipContentEl.style.display = ''; - tooltipContentEl.classList.add('tooltip-content'); - this._setTooltipContent(tooltipContentEl); - this.tooltipEl.appendChild(tooltipContentEl); - document.body.appendChild(this.tooltipEl); - } - _setTooltipContent(tooltipContentEl) { - if (this.options.tooltipId) - return; - tooltipContentEl.innerText = this.options.text; - } - _updateTooltipContent() { - this._setTooltipContent(this.tooltipEl.querySelector('.tooltip-content')); - } - _setupEventHandlers() { - this.el.addEventListener('mouseenter', this._handleMouseEnter); - this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.el.addEventListener('focus', this._handleFocus, true); - this.el.addEventListener('blur', this._handleBlur, true); - } - _removeEventHandlers() { - this.el.removeEventListener('mouseenter', this._handleMouseEnter); - this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.el.removeEventListener('focus', this._handleFocus, true); - this.el.removeEventListener('blur', this._handleBlur, true); - } - /** - * Show tooltip. - */ - open = (isManual) => { - if (this.isOpen) - return; - isManual = isManual === undefined ? true : undefined; // Default value true - this.isOpen = true; - // Update tooltip content with HTML attribute options - this.options = { ...this.options, ...this._getAttributeOptions() }; - this._updateTooltipContent(); - this._setEnterDelayTimeout(isManual); - }; - /** - * Hide tooltip. - */ - close = () => { - if (!this.isOpen) - return; - this.isHovered = false; - this.isFocused = false; - this.isOpen = false; - this._setExitDelayTimeout(); - }; - _setExitDelayTimeout() { - clearTimeout(this._exitDelayTimeout); - this._exitDelayTimeout = setTimeout(() => { - if (this.isHovered || this.isFocused) - return; - this._animateOut(); - }, this.options.exitDelay); - } - _setEnterDelayTimeout(isManual) { - clearTimeout(this._enterDelayTimeout); - this._enterDelayTimeout = setTimeout(() => { - if (!this.isHovered && !this.isFocused && !isManual) - return; - this._animateIn(); - }, this.options.enterDelay); - } - _positionTooltip() { - const tooltip = this.tooltipEl; - const origin = this.el, originHeight = origin.offsetHeight, originWidth = origin.offsetWidth, tooltipHeight = tooltip.offsetHeight, tooltipWidth = tooltip.offsetWidth, margin = this.options.margin; - this.xMovement = 0; - this.yMovement = 0; - let targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(); - let targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (this.options.position === 'top') { - targetTop += -tooltipHeight - margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = -this.options.transitionMovement; - } - else if (this.options.position === 'right') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += originWidth + margin; - this.xMovement = this.options.transitionMovement; - } - else if (this.options.position === 'left') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += -tooltipWidth - margin; - this.xMovement = -this.options.transitionMovement; - } - else { - targetTop += originHeight + margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = this.options.transitionMovement; - } - const newCoordinates = this._repositionWithinScreen(targetLeft, targetTop, tooltipWidth, tooltipHeight); - tooltip.style.top = newCoordinates.y + 'px'; - tooltip.style.left = newCoordinates.x + 'px'; - } - _repositionWithinScreen(x, y, width, height) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - const bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - const offset = this.options.margin + this.options.transitionMovement; - const edges = Utils.checkWithinContainer(document.body, bounding, offset); - if (edges.left) { - newX = offset; - } - else if (edges.right) { - newX -= newX + width - window.innerWidth; - } - if (edges.top) { - newY = offset; - } - else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; - } - _animateIn() { - this._positionTooltip(); - this.tooltipEl.style.visibility = 'visible'; - const duration = this.options.inDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(${this.xMovement}px) translateY(${this.yMovement}px)`; - this.tooltipEl.style.opacity = (this.options.opacity || 1).toString(); - }, 1); - } - _animateOut() { - const duration = this.options.outDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(0px) translateY(0px)`; - this.tooltipEl.style.opacity = '0'; - }, 1); - /* - anim.remove(this.tooltipEl); - anim({ - targets: this.tooltipEl, - opacity: 0, - translateX: 0, - translateY: 0, - duration: this.options.outDuration, - easing: 'easeOutCubic' - }); - */ - } - _handleMouseEnter = () => { - this.isHovered = true; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.open(false); - }; - _handleMouseLeave = () => { - this.isHovered = false; - this.isFocused = false; // Allows close of tooltip when opened by focus. - this.close(); - }; - _handleFocus = () => { - if (Utils.tabPressed) { - this.isFocused = true; - this.open(false); - } - }; - _handleBlur = () => { - this.isFocused = false; - this.close(); - }; - _getAttributeOptions() { - const attributeOptions = {}; - const tooltipTextOption = this.el.getAttribute('data-tooltip'); - const tooltipId = this.el.getAttribute('data-tooltip-id'); - const positionOption = this.el.getAttribute('data-position'); - if (tooltipTextOption) { - attributeOptions.text = tooltipTextOption; - } - if (positionOption) { - attributeOptions.position = positionOption; - } - if (tooltipId) { - attributeOptions.tooltipId = tooltipId; - } - return attributeOptions; - } -} - -const _defaults$5 = {}; -// TODO: !!!!! -class Range extends Component { - _mousedown; - value; - thumb; - constructor(el, options) { - super(el, options, Range); - this.el['M_Range'] = this; - this.options = { - ...Range.defaults, - ...options - }; - this._mousedown = false; - this._setupThumb(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$5; - } - /** - * Initializes instances of Range. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Range); - } - static getInstance(el) { - return el['M_Range']; - } - destroy() { - this._removeEventHandlers(); - this._removeThumb(); - this.el['M_Range'] = undefined; - } - _setupEventHandlers() { - this.el.addEventListener('change', this._handleRangeChange); - this.el.addEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.addEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.addEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.addEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.addEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.addEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.addEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _removeEventHandlers() { - this.el.removeEventListener('change', this._handleRangeChange); - this.el.removeEventListener('mousedown', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('touchstart', this._handleRangeMousedownTouchstart); - this.el.removeEventListener('input', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mousemove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('touchmove', this._handleRangeInputMousemoveTouchmove); - this.el.removeEventListener('mouseup', this._handleRangeMouseupTouchend); - this.el.removeEventListener('touchend', this._handleRangeMouseupTouchend); - this.el.removeEventListener('blur', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('mouseout', this._handleRangeBlurMouseoutTouchleave); - this.el.removeEventListener('touchleave', this._handleRangeBlurMouseoutTouchleave); - } - _handleRangeChange = () => { - this.value.innerHTML = this.el.value; - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - }; - _handleRangeMousedownTouchstart = (e) => { - // Set indicator value - this.value.innerHTML = this.el.value; - this._mousedown = true; - this.el.classList.add('active'); - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - if (e.type !== 'input') { - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - } - }; - _handleRangeInputMousemoveTouchmove = () => { - if (this._mousedown) { - if (!this.thumb.classList.contains('active')) { - this._showRangeBubble(); - } - const offsetLeft = this._calcRangeOffset(); - this.thumb.classList.add('active'); - this.thumb.style.left = offsetLeft + 'px'; - this.value.innerHTML = this.el.value; - } - }; - _handleRangeMouseupTouchend = () => { - this._mousedown = false; - this.el.classList.remove('active'); - }; - _handleRangeBlurMouseoutTouchleave = () => { - if (!this._mousedown) { - const paddingLeft = parseInt(getComputedStyle(this.el).paddingLeft); - const marginLeftText = 7 + paddingLeft + 'px'; - if (this.thumb.classList.contains('active')) { - const duration = 100; - // from - this.thumb.style.transition = 'none'; - setTimeout(() => { - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '0'; - this.thumb.style.width = '0'; - this.thumb.style.top = '0'; - this.thumb.style.marginLeft = marginLeftText; - }, 1); - } - this.thumb.classList.remove('active'); - } - }; - _setupThumb() { - this.thumb = document.createElement('span'); - this.value = document.createElement('span'); - this.thumb.classList.add('thumb'); - this.value.classList.add('value'); - this.thumb.append(this.value); - this.el.after(this.thumb); - } - _removeThumb() { - this.thumb.remove(); - } - _showRangeBubble() { - const paddingLeft = parseInt(getComputedStyle(this.thumb.parentElement).paddingLeft); - const marginLeftText = -7 + paddingLeft + 'px'; // TODO: fix magic number? - const duration = 300; - // easeOutQuint - this.thumb.style.transition = ` - height ${duration}ms ease, - width ${duration}ms ease, - top ${duration}ms ease, - margin ${duration}ms ease - `; - // to - this.thumb.style.height = '30px'; - this.thumb.style.width = '30px'; - this.thumb.style.top = '-30px'; - this.thumb.style.marginLeft = marginLeftText; - } - _calcRangeOffset() { - const width = this.el.getBoundingClientRect().width - 15; - const max = parseFloat(this.el.getAttribute('max')) || 100; // Range default max - const min = parseFloat(this.el.getAttribute('min')) || 0; // Range default min - const percent = (parseFloat(this.el.value) - min) / (max - min); - return percent * width; - } - /** - * Initializes every range input in the current document. - */ - static Init() { - if (typeof document !== 'undefined') - Range.init(document?.querySelectorAll('input[type=range]'), {}); - } -} - -const _defaults$4 = { - onOpen: null, - onClose: null -}; -class TapTarget extends Component { - /** - * If the tap target is open. - */ - isOpen; - static _taptargets; - wrapper; - // private _origin: HTMLElement; - originEl; - waveEl; - contentEl; - constructor(el, options) { - super(el, options, TapTarget); - this.el['M_TapTarget'] = this; - this.options = { - ...TapTarget.defaults, - ...options - }; - this.isOpen = false; - // setup - this.originEl = document.querySelector(`#${el.dataset.target}`); - this.originEl.tabIndex = 0; - this._setup(); - this._calculatePositioning(); - this._setupEventHandlers(); - TapTarget._taptargets.push(this); - } - static get defaults() { - return _defaults$4; - } - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, TapTarget); - } - static getInstance(el) { - return el['M_TapTarget']; - } - destroy() { - this._removeEventHandlers(); - this.el['M_TapTarget'] = undefined; - const index = TapTarget._taptargets.indexOf(this); - if (index >= 0) { - TapTarget._taptargets.splice(index, 1); - } - } - _setupEventHandlers() { - this.originEl.addEventListener('click', this._handleTargetToggle); - this.originEl.addEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.addEventListener('click', this._handleOriginClick); - // Resize - window.addEventListener('resize', this._handleThrottledResize); - } - _removeEventHandlers() { - this.originEl.removeEventListener('click', this._handleTargetToggle); - this.originEl.removeEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.removeEventListener('click', this._handleOriginClick); - window.removeEventListener('resize', this._handleThrottledResize); - } - _handleThrottledResize = () => Utils.throttle(this._handleResize, 200).bind(this); - _handleKeyboardInteraction = (e) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleTargetToggle(); - } - }; - _handleTargetToggle = () => { - if (!this.isOpen) - this.open(); - else - this.close(); - }; - /*_handleOriginClick = () => { - this.close(); - }*/ - _handleResize = () => { - this._calculatePositioning(); - }; - _handleDocumentClick = (e) => { - if (e.target.closest(`#${this.el.dataset.target}`) !== this.originEl && - !e.target.closest('.tap-target-wrapper')) { - this.close(); - // e.preventDefault(); - // e.stopPropagation(); - } - }; - _setup() { - // Creating tap target - this.wrapper = this.el.parentElement; - this.waveEl = this.wrapper.querySelector('.tap-target-wave'); - this.el.parentElement.ariaExpanded = 'false'; - this.originEl.style.zIndex = '1002'; - // this.originEl = this.wrapper.querySelector('.tap-target-origin'); - this.contentEl = this.el.querySelector('.tap-target-content'); - // Creating wrapper - if (!this.wrapper.classList.contains('.tap-target-wrapper')) { - this.wrapper = document.createElement('div'); - this.wrapper.classList.add('tap-target-wrapper'); - this.el.before(this.wrapper); - this.wrapper.append(this.el); - } - // Creating content - if (!this.contentEl) { - this.contentEl = document.createElement('div'); - this.contentEl.classList.add('tap-target-content'); - this.el.append(this.contentEl); - } - // Creating foreground wave - if (!this.waveEl) { - this.waveEl = document.createElement('div'); - this.waveEl.classList.add('tap-target-wave'); - // Creating origin - /*if (!this.originEl) { - this.originEl = this._origin.cloneNode(true); // .clone(true, true); - this.originEl.classList.add('tap-target-origin'); - this.originEl.removeAttribute('id'); - this.originEl.removeAttribute('style'); - this.waveEl.append(this.originEl); - }*/ - this.wrapper.append(this.waveEl); - } - } - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - _calculatePositioning() { - // Element or parent is fixed position? - let isFixed = getComputedStyle(this.originEl).position === 'fixed'; - if (!isFixed) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let currentElem = this.originEl; - const parents = []; - while ((currentElem = currentElem.parentNode) && currentElem !== document) - parents.push(currentElem); - for (let i = 0; i < parents.length; i++) { - isFixed = getComputedStyle(parents[i]).position === 'fixed'; - if (isFixed) - break; - } - } - // Calculating origin - const originWidth = this.originEl.offsetWidth; - const originHeight = this.originEl.offsetHeight; - const originTop = isFixed - ? this._offset(this.originEl).top - Utils.getDocumentScrollTop() - : this._offset(this.originEl).top; - const originLeft = isFixed - ? this._offset(this.originEl).left - Utils.getDocumentScrollLeft() - : this._offset(this.originEl).left; - // Calculating screen - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - const scrollBarWidth = windowWidth - document.documentElement.clientWidth; - const centerX = windowWidth / 2; - const centerY = windowHeight / 2; - const isLeft = originLeft <= centerX; - const isRight = originLeft > centerX; - const isTop = originTop <= centerY; - const isBottom = originTop > centerY; - const isCenterX = originLeft >= windowWidth * 0.25 && originLeft <= windowWidth * 0.75; - // Calculating tap target - const tapTargetWidth = this.el.offsetWidth; - const tapTargetHeight = this.el.offsetHeight; - const tapTargetTop = originTop + originHeight / 2 - tapTargetHeight / 2; - const tapTargetLeft = originLeft + originWidth / 2 - tapTargetWidth / 2; - const tapTargetPosition = isFixed ? 'fixed' : 'absolute'; - // Calculating content - const tapTargetTextWidth = isCenterX ? tapTargetWidth : tapTargetWidth / 2 + originWidth; - const tapTargetTextHeight = tapTargetHeight / 2; - const tapTargetTextTop = isTop ? tapTargetHeight / 2 : 0; - const tapTargetTextBottom = 0; - const tapTargetTextLeft = isLeft && !isCenterX ? tapTargetWidth / 2 - originWidth : 0; - const tapTargetTextRight = 0; - const tapTargetTextPadding = originWidth; - const tapTargetTextAlign = isBottom ? 'bottom' : 'top'; - // Calculating wave - const tapTargetWaveWidth = originWidth > originHeight ? originWidth * 2 : originWidth * 2; - const tapTargetWaveHeight = tapTargetWaveWidth; - const tapTargetWaveTop = tapTargetHeight / 2 - tapTargetWaveHeight / 2; - const tapTargetWaveLeft = tapTargetWidth / 2 - tapTargetWaveWidth / 2; - // Setting tap target - this.wrapper.style.top = isTop ? tapTargetTop + 'px' : ''; - this.wrapper.style.right = isRight - ? windowWidth - tapTargetLeft - tapTargetWidth - scrollBarWidth + 'px' - : ''; - this.wrapper.style.bottom = isBottom - ? windowHeight - tapTargetTop - tapTargetHeight + 'px' - : ''; - this.wrapper.style.left = isLeft ? tapTargetLeft + 'px' : ''; - this.wrapper.style.position = tapTargetPosition; - // Setting content - this.contentEl.style.width = tapTargetTextWidth + 'px'; - this.contentEl.style.height = tapTargetTextHeight + 'px'; - this.contentEl.style.top = tapTargetTextTop + 'px'; - this.contentEl.style.right = tapTargetTextRight + 'px'; - this.contentEl.style.bottom = tapTargetTextBottom + 'px'; - this.contentEl.style.left = tapTargetTextLeft + 'px'; - this.contentEl.style.padding = tapTargetTextPadding + 'px'; - this.contentEl.style.verticalAlign = tapTargetTextAlign; - // Setting wave - this.waveEl.style.top = tapTargetWaveTop + 'px'; - this.waveEl.style.left = tapTargetWaveLeft + 'px'; - this.waveEl.style.width = tapTargetWaveWidth + 'px'; - this.waveEl.style.height = tapTargetWaveHeight + 'px'; - } - /** - * Open Tap Target. - */ - open = () => { - if (this.isOpen) - return; - // onOpen callback - if (typeof this.options.onOpen === 'function') { - this.options.onOpen.call(this, this.originEl); - } - this.isOpen = true; - this.wrapper.classList.add('open'); - this.wrapper.ariaExpanded = 'true'; - document.body.addEventListener('click', this._handleDocumentClick, true); - document.body.addEventListener('keypress', this._handleDocumentClick, true); - document.body.addEventListener('touchend', this._handleDocumentClick); - }; - /** - * Close Tap Target. - */ - close = () => { - if (!this.isOpen) - return; - // onClose callback - if (typeof this.options.onClose === 'function') { - this.options.onClose.call(this, this.originEl); - } - this.isOpen = false; - this.wrapper.classList.remove('open'); - this.wrapper.ariaExpanded = 'false'; - document.body.removeEventListener('click', this._handleDocumentClick, true); - document.body.removeEventListener('keypress', this._handleDocumentClick, true); - document.body.removeEventListener('touchend', this._handleDocumentClick); - }; - static { - TapTarget._taptargets = []; - } -} - -const _defaults$3 = Object.freeze({}); -class CharacterCounter extends Component { - /** Stores the reference to the counter HTML element. */ - counterEl; - /** Specifies whether the input is valid or not. */ - isInvalid; - /** Specifies whether the input text has valid length or not. */ - isValidLength; - constructor(el, options) { - super(el, {}, CharacterCounter); - this.el['M_CharacterCounter'] = this; - this.options = { - ...CharacterCounter.defaults, - ...options - }; - this.isInvalid = false; - this.isValidLength = false; - this._setupCounter(); - this._setupEventHandlers(); - } - static get defaults() { - return _defaults$3; - } - /** - * Initializes instances of CharacterCounter. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, CharacterCounter); - } - static getInstance(el) { - return el['M_CharacterCounter']; - } - destroy() { - this._removeEventHandlers(); - this.el['CharacterCounter'] = undefined; - this._removeCounter(); - } - _setupEventHandlers() { - this.el.addEventListener('focus', this.updateCounter, true); - this.el.addEventListener('input', this.updateCounter, true); - } - _removeEventHandlers() { - this.el.removeEventListener('focus', this.updateCounter, true); - this.el.removeEventListener('input', this.updateCounter, true); - } - _setupCounter() { - this.counterEl = document.createElement('span'); - this.counterEl.classList.add('character-counter'); - this.counterEl.style.float = 'right'; - this.counterEl.style.fontSize = '12px'; - this.counterEl.style.height = '1'; - this.el.parentElement.appendChild(this.counterEl); - } - _removeCounter() { - this.counterEl.remove(); - } - updateCounter = () => { - const maxLength = parseInt(this.el.getAttribute('maxlength')), actualLength = this.el.value.length; - this.isValidLength = actualLength <= maxLength; - let counterString = actualLength.toString(); - if (maxLength) { - counterString += '/' + maxLength; - this._validateInput(); - } - this.counterEl.innerHTML = counterString; - }; - _validateInput() { - if (this.isValidLength && this.isInvalid) { - this.isInvalid = false; - this.el.classList.remove('invalid'); - } - else if (!this.isValidLength && !this.isInvalid) { - this.isInvalid = true; - this.el.classList.remove('valid'); - this.el.classList.add('invalid'); - } - } -} - -const _defaults$2 = { - responsiveThreshold: 0 // breakpoint for swipeable -}; -class Parallax extends Component { - _enabled; - _img; - static _parallaxes = []; - static _handleScrollThrottled; - static _handleWindowResizeThrottled; - constructor(el, options) { - super(el, options, Parallax); - this.el['M_Parallax'] = this; - this.options = { - ...Parallax.defaults, - ...options - }; - this._enabled = window.innerWidth > this.options.responsiveThreshold; - this._img = this.el.querySelector('img'); - this._updateParallax(); - this._setupEventHandlers(); - this._setupStyles(); - Parallax._parallaxes.push(this); - } - static get defaults() { - return _defaults$2; - } - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Parallax); - } - static getInstance(el) { - return el['M_Parallax']; - } - destroy() { - Parallax._parallaxes.splice(Parallax._parallaxes.indexOf(this), 1); - this._img.style.transform = ''; - this._removeEventHandlers(); - this.el['M_Parallax'] = undefined; - } - static _handleScroll() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._updateParallax.call(parallaxInstance); - } - } - static _handleWindowResize() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._enabled = window.innerWidth > parallaxInstance.options.responsiveThreshold; - } - } - _setupEventHandlers() { - this._img.addEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - if (!Parallax._handleScrollThrottled) { - Parallax._handleScrollThrottled = Utils.throttle(Parallax._handleScroll, 5); - } - if (!Parallax._handleWindowResizeThrottled) { - Parallax._handleWindowResizeThrottled = Utils.throttle(Parallax._handleWindowResize, 5); - } - window.addEventListener('scroll', Parallax._handleScrollThrottled); - window.addEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _removeEventHandlers() { - this._img.removeEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - window.removeEventListener('scroll', Parallax._handleScrollThrottled); - window.removeEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - _setupStyles() { - this._img.style.opacity = '1'; - } - _handleImageLoad = () => { - this._updateParallax(); - }; - _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - _updateParallax() { - const containerHeight = this.el.getBoundingClientRect().height > 0 ? this.el.parentElement.offsetHeight : 500; - const imgHeight = this._img.offsetHeight; - const parallaxDist = imgHeight - containerHeight; - const bottom = this._offset(this.el).top + containerHeight; - const top = this._offset(this.el).top; - const scrollTop = Utils.getDocumentScrollTop(); - const windowHeight = window.innerHeight; - const windowBottom = scrollTop + windowHeight; - const percentScrolled = (windowBottom - top) / (containerHeight + windowHeight); - const parallax = parallaxDist * percentScrolled; - if (!this._enabled) { - this._img.style.transform = ''; - } - else if (bottom > scrollTop && top < scrollTop + windowHeight) { - this._img.style.transform = `translate3D(-50%, ${parallax}px, 0)`; - } - } -} - -const _defaults$1 = { - top: 0, - bottom: Infinity, - offset: 0, - onPositionChange: null -}; -class Pushpin extends Component { - static _pushpins; - originalOffset; - constructor(el, options) { - super(el, options, Pushpin); - this.el['M_Pushpin'] = this; - this.options = { - ...Pushpin.defaults, - ...options - }; - this.originalOffset = this.el.offsetTop; - Pushpin._pushpins.push(this); - this._setupEventHandlers(); - this._updatePosition(); - } - static get defaults() { - return _defaults$1; - } - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, Pushpin); - } - static getInstance(el) { - return el['M_Pushpin']; - } - destroy() { - this.el.style.top = null; - this._removePinClasses(); - // Remove pushpin Inst - const index = Pushpin._pushpins.indexOf(this); - Pushpin._pushpins.splice(index, 1); - if (Pushpin._pushpins.length === 0) { - this._removeEventHandlers(); - } - this.el['M_Pushpin'] = undefined; - } - static _updateElements() { - for (const elIndex in Pushpin._pushpins) { - const pInstance = Pushpin._pushpins[elIndex]; - pInstance._updatePosition(); - } - } - _setupEventHandlers() { - document.addEventListener('scroll', Pushpin._updateElements); - } - _removeEventHandlers() { - document.removeEventListener('scroll', Pushpin._updateElements); - } - _updatePosition() { - const scrolled = Utils.getDocumentScrollTop() + this.options.offset; - if (this.options.top <= scrolled && - this.options.bottom >= scrolled && - !this.el.classList.contains('pinned')) { - this._removePinClasses(); - this.el.style.top = `${this.options.offset}px`; - this.el.classList.add('pinned'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pinned'); - } - } - // Add pin-top (when scrolled position is above top) - if (scrolled < this.options.top && !this.el.classList.contains('pin-top')) { - this._removePinClasses(); - this.el.style.top = '0'; - this.el.classList.add('pin-top'); - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-top'); - } - } - // Add pin-bottom (when scrolled position is below bottom) - if (scrolled > this.options.bottom && !this.el.classList.contains('pin-bottom')) { - this._removePinClasses(); - this.el.classList.add('pin-bottom'); - this.el.style.top = `${this.options.bottom - this.originalOffset}px`; - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-bottom'); - } - } - } - _removePinClasses() { - // IE 11 bug (can't remove multiple classes in one line) - this.el.classList.remove('pin-top'); - this.el.classList.remove('pinned'); - this.el.classList.remove('pin-bottom'); - } - static { - Pushpin._pushpins = []; - } -} - -const _defaults = { - throttle: 100, - scrollOffset: 200, // offset - 200 allows elements near bottom of page to scroll - activeClass: 'active', - getActiveElement: (id) => { - return 'a[href="#' + id + '"]'; - }, - keepTopElementActive: false, - animationDuration: null -}; -class ScrollSpy extends Component { - static _elements; - static _count; - static _increment; - static _elementsInView; - static _visibleElements; - static _ticks; - static _keptTopActiveElement = null; - tickId; - id; - constructor(el, options) { - super(el, options, ScrollSpy); - this.el['M_ScrollSpy'] = this; - this.options = { - ...ScrollSpy.defaults, - ...options - }; - ScrollSpy._elements.push(this); - ScrollSpy._count++; - ScrollSpy._increment++; - this.tickId = -1; - this.id = ScrollSpy._increment.toString(); - this._setupEventHandlers(); - this._handleWindowScroll(); - } - static get defaults() { - return _defaults; - } - /** - * Initializes instances of ScrollSpy. - * @param els HTML elements. - * @param options Component options. - */ - static init(els, options = {}) { - return super.init(els, options, ScrollSpy); - } - static getInstance(el) { - return el['M_ScrollSpy']; - } - destroy() { - ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1); - ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1); - ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.el), 1); - ScrollSpy._count--; - this._removeEventHandlers(); - const actElem = document.querySelector(this.options.getActiveElement(this.el.id)); - actElem.classList.remove(this.options.activeClass); - this.el['M_ScrollSpy'] = undefined; - } - _setupEventHandlers() { - if (ScrollSpy._count === 1) { - window.addEventListener('scroll', this._handleWindowScroll); - window.addEventListener('resize', this._handleThrottledResize); - document.body.addEventListener('click', this._handleTriggerClick); - } - } - _removeEventHandlers() { - if (ScrollSpy._count === 0) { - window.removeEventListener('scroll', this._handleWindowScroll); - window.removeEventListener('resize', this._handleThrottledResize); - document.body.removeEventListener('click', this._handleTriggerClick); - } - } - _handleThrottledResize = () => Utils.throttle(this._handleWindowScroll, 200).bind(this); - _handleTriggerClick = (e) => { - const trigger = e.target; - for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) { - const scrollspy = ScrollSpy._elements[i]; - const x = document.querySelector('a[href="#' + scrollspy.el.id + '"]'); - if (trigger === x) { - e.preventDefault(); - if (scrollspy.el['M_ScrollSpy'].options.animationDuration) { - ScrollSpy._smoothScrollIntoView(scrollspy.el, scrollspy.el['M_ScrollSpy'].options.animationDuration); - } - else { - scrollspy.el.scrollIntoView({ behavior: 'smooth' }); - } - break; - } - } - }; - _handleWindowScroll = () => { - // unique tick id - ScrollSpy._ticks++; - // viewport rectangle - const top = Utils.getDocumentScrollTop(), left = Utils.getDocumentScrollLeft(), right = left + window.innerWidth, bottom = top + window.innerHeight; - // determine which elements are in view - const intersections = ScrollSpy._findElements(top, right, bottom, left); - for (let i = 0; i < intersections.length; i++) { - const scrollspy = intersections[i]; - const lastTick = scrollspy.tickId; - if (lastTick < 0) { - // entered into view - scrollspy._enter(); - } - // update tick id - scrollspy.tickId = ScrollSpy._ticks; - } - for (let i = 0; i < ScrollSpy._elementsInView.length; i++) { - const scrollspy = ScrollSpy._elementsInView[i]; - const lastTick = scrollspy.tickId; - if (lastTick >= 0 && lastTick !== ScrollSpy._ticks) { - // exited from view - scrollspy._exit(); - scrollspy.tickId = -1; - } - } - // remember elements in view for next tick - ScrollSpy._elementsInView = intersections; - if (ScrollSpy._elements.length) { - const options = ScrollSpy._elements[0].el['M_ScrollSpy'].options; - if (options.keepTopElementActive && ScrollSpy._visibleElements.length === 0) { - this._resetKeptTopActiveElementIfNeeded(); - const topElements = ScrollSpy._elements - .filter((value) => ScrollSpy._getDistanceToViewport(value.el) <= 0) - .sort((a, b) => { - const distanceA = ScrollSpy._getDistanceToViewport(a.el); - const distanceB = ScrollSpy._getDistanceToViewport(b.el); - if (distanceA < distanceB) - return -1; - if (distanceA > distanceB) - return 1; - return 0; - }); - const nearestTopElement = topElements.length - ? topElements[topElements.length - 1] - : ScrollSpy._elements[0]; - const actElem = document.querySelector(options.getActiveElement(nearestTopElement.el.id)); - actElem?.classList.add(options.activeClass); - ScrollSpy._keptTopActiveElement = actElem; - } - } - }; - static _offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - static _findElements(top, right, bottom, left) { - const hits = []; - for (let i = 0; i < ScrollSpy._elements.length; i++) { - const scrollspy = ScrollSpy._elements[i]; - const currTop = top + scrollspy.options.scrollOffset || 200; - if (scrollspy.el.getBoundingClientRect().height > 0) { - const elTop = ScrollSpy._offset(scrollspy.el).top, elLeft = ScrollSpy._offset(scrollspy.el).left, elRight = elLeft + scrollspy.el.getBoundingClientRect().width, elBottom = elTop + scrollspy.el.getBoundingClientRect().height; - const isIntersect = !(elLeft > right || - elRight < left || - elTop > bottom || - elBottom < currTop); - if (isIntersect) { - hits.push(scrollspy); - } - } - } - return hits; - } - _enter() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - if (ScrollSpy._visibleElements[0]['M_ScrollSpy'] && - this.id < ScrollSpy._visibleElements[0]['M_ScrollSpy'].id) { - ScrollSpy._visibleElements.unshift(this.el); - } - else { - ScrollSpy._visibleElements.push(this.el); - } - } - else { - ScrollSpy._visibleElements.push(this.el); - } - this._resetKeptTopActiveElementIfNeeded(); - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - } - _exit() { - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((value) => value.getBoundingClientRect().height !== 0); - if (ScrollSpy._visibleElements[0]) { - const actElem = document.querySelector(this.options.getActiveElement(ScrollSpy._visibleElements[0].id)); - actElem?.classList.remove(this.options.activeClass); - ScrollSpy._visibleElements = ScrollSpy._visibleElements.filter((x) => x.id != this.el.id); - if (ScrollSpy._visibleElements[0]) { - // Check if empty - const selector = this.options.getActiveElement(ScrollSpy._visibleElements[0].id); - document.querySelector(selector)?.classList.add(this.options.activeClass); - this._resetKeptTopActiveElementIfNeeded(); - } - } - } - _resetKeptTopActiveElementIfNeeded() { - if (ScrollSpy._keptTopActiveElement) { - ScrollSpy._keptTopActiveElement.classList.remove(this.options.activeClass); - ScrollSpy._keptTopActiveElement = null; - } - } - static _getDistanceToViewport(element) { - const rect = element.getBoundingClientRect(); - const distance = rect.top; - return distance; - } - static _smoothScrollIntoView(element, duration = 300) { - const targetPosition = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset); - const startPosition = window.scrollY || window.pageYOffset; - const distance = targetPosition - startPosition; - const startTime = performance.now(); - function scrollStep(currentTime) { - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - const scrollY = startPosition + distance * progress; - if (progress < 1) { - window.scrollTo(0, scrollY); - requestAnimationFrame(scrollStep); - } - else { - window.scrollTo(0, targetPosition); - } - } - requestAnimationFrame(scrollStep); - } - static { - ScrollSpy._elements = []; - ScrollSpy._elementsInView = []; - ScrollSpy._visibleElements = []; // Array. - ScrollSpy._count = 0; - ScrollSpy._increment = 0; - ScrollSpy._ticks = 0; - } -} - -// type RGBColor = { -// r: number; -// g: number; -// b: number; -// }; - -// type Position = { -// x: number; -// y: number; -// }; - -class Waves { - /** - * - * @param {HTMLElement} el - * @returns - */ - static #offset(el) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - - // https://phoenix-dx.com/css-techniques-for-material-ripple-effect/ - - /** - * - * @param {HTMLElement} targetElement - * @param position - * @param color - */ - static renderWaveEffect(targetElement, position = null, color = null) { - const isCentered = position === null; - const duration = 500; - let animationFrame, animationStart; - - const animationStep = (timestamp) => { - if (!animationStart) animationStart = timestamp; - const frame = timestamp - animationStart; - if (frame < duration) { - const easing = (frame / duration) * (2 - frame / duration); - const circle = isCentered - ? 'circle at 50% 50%' - : `circle at ${position.x}px ${position.y}px`; - const waveColor = `rgba(${color?.r || 0}, ${color?.g || 0}, ${color?.b || 0}, ${0.3 * (1 - easing)})`; - const stop = 90 * easing + '%'; - targetElement.style.backgroundImage = - 'radial-gradient(' + - circle + - ', ' + - waveColor + - ' ' + - stop + - ', transparent ' + - stop + - ')'; - animationFrame = window.requestAnimationFrame(animationStep); - } else { - targetElement.style.backgroundImage = 'none'; - window.cancelAnimationFrame(animationFrame); - } - }; - - animationFrame = window.requestAnimationFrame(animationStep); - } - - static Init() { - if (typeof document !== 'undefined') - document?.addEventListener('DOMContentLoaded', () => { - document.body.addEventListener('click', (e) => { - const trigger = e.target; - const el = trigger.closest('.waves-effect'); - if (el && el.contains(trigger)) { - const isCircular = el.classList.contains('waves-circle'); - const x = e.pageX - this.#offset(el).left; - const y = e.pageY - this.#offset(el).top; - let color = null; - if (el.classList.contains('waves-light')) color = { r: 255, g: 255, b: 255 }; - this.renderWaveEffect(el, isCircular ? null : { x, y }, color); - } - }); - }); - } -} - -/* eslint-disable @typescript-eslint/no-unused-vars */ -const version = '2.3.3'; -/** - * Automatically initialize components. - * @param context Root element to initialize. Defaults to `document.body`. - * @param options Options for each component. - */ -function AutoInit(context = document.body, options) { - const registry = { - Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'), - Cards: context.querySelectorAll('.cards:not(.no-autoinit)'), - Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'), - Chips: context.querySelectorAll('.chips:not(.no-autoinit)'), - Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'), - Datepicker: context.querySelectorAll('.datepicker:not(.no-autoinit)'), - Dropdown: context.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'), - Materialbox: context.querySelectorAll('.materialboxed:not(.no-autoinit)'), - Modal: context.querySelectorAll('.modal:not(.no-autoinit)'), - Parallax: context.querySelectorAll('.parallax:not(.no-autoinit)'), - Pushpin: context.querySelectorAll('.pushpin:not(.no-autoinit)'), - ScrollSpy: context.querySelectorAll('.scrollspy:not(.no-autoinit)'), - FormSelect: context.querySelectorAll('select:not(.no-autoinit)'), - Sidenav: context.querySelectorAll('.sidenav:not(.no-autoinit)'), - Tabs: context.querySelectorAll('.tabs:not(.no-autoinit)'), - TapTarget: context.querySelectorAll('.tap-target:not(.no-autoinit)'), - Timepicker: context.querySelectorAll('.timepicker:not(.no-autoinit)'), - Tooltip: context.querySelectorAll('.tooltipped:not(.no-autoinit)'), - FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)') - }; - Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {}); - Cards.init(registry.Cards, options?.Cards ?? {}); - Carousel.init(registry.Carousel, options?.Carousel ?? {}); - Chips.init(registry.Chips, options?.Chips ?? {}); - Collapsible.init(registry.Collapsible, options?.Collapsible ?? {}); - Datepicker.init(registry.Datepicker, options?.Datepicker ?? {}); - Dropdown.init(registry.Dropdown, options?.Dropdown ?? {}); - Materialbox.init(registry.Materialbox, options?.Materialbox ?? {}); - Modal.init(registry.Modal, options?.Modal ?? {}); - Parallax.init(registry.Parallax, options?.Parallax ?? {}); - Pushpin.init(registry.Pushpin, options?.Pushpin ?? {}); - ScrollSpy.init(registry.ScrollSpy, options?.ScrollSpy ?? {}); - FormSelect.init(registry.FormSelect, options?.FormSelect ?? {}); - Sidenav.init(registry.Sidenav, options?.Sidenav ?? {}); - Tabs.init(registry.Tabs, options?.Tabs ?? {}); - TapTarget.init(registry.TapTarget, options?.TapTarget ?? {}); - Timepicker.init(registry.Timepicker, options?.Timepicker ?? {}); - Tooltip.init(registry.Tooltip, options?.Tooltip ?? {}); - FloatingActionButton.init(registry.FloatingActionButton, options?.FloatingActionButton ?? {}); -} -// Init -if (typeof document !== 'undefined') { - document.addEventListener('keydown', Utils.docHandleKeydown, true); - document.addEventListener('keyup', Utils.docHandleKeyup, true); - document.addEventListener('focus', Utils.docHandleFocus, true); - document.addEventListener('blur', Utils.docHandleBlur, true); -} -Forms.Init(); -Chips.Init(); -Waves.Init(); -Range.Init(); -Cards.Init(); - -export { AutoInit, Autocomplete, Cards, Carousel, CharacterCounter, Chips, Collapsible, Datepicker, Dropdown, FloatingActionButton, FormSelect, Forms, Materialbox, Modal, Parallax, Pushpin, Range, ScrollSpy, Sidenav, Slider, Tabs, TapTarget, Timepicker, Toast, Tooltip, Waves, version }; diff --git a/docs/FOR_MAINTAINERS.md b/docs/FOR_MAINTAINERS.md index 93c300cfbe..21a9de504b 100644 --- a/docs/FOR_MAINTAINERS.md +++ b/docs/FOR_MAINTAINERS.md @@ -1,51 +1,54 @@ # For Maintainers + This is the documentation that will be used by the maintainers. If you have ideas on how to do things better, please let us know by creating an issue or PR 😉. - [Labels](#labels) - [Releasing a new version](#releasing-a-new-version) ## Labels + We have several labels that can be used to organize issues and PRs on this repo and also to make the changelog generation easier. All the labels and their descriptions can be [read here](https://github.com/materializecss/materialize/issues/labels). Any other PRs that hasn't labeled with one of these will not be shown on the changelog. Please take a look at the table below | Label | Section | -|---------------|--------------------------| +| ------------- | ------------------------ | | enhancement | Implemented enhancements | | bug | Fixed bugs | | documentation | Documentation changes | | meta | Meta changes | ## Releasing a new version + To fully release a new version, you need to have access to the @materializecss organization on the npmjs. Then, please follow the steps below: - In your local copy of Materialize, go into the dev Branch with `git checkout v2-dev` and pull the newest version - with `git pull origin v2-dev` to have the newest version from the server. Run Tests and check if everything works. + with `git pull origin v2-dev` to have the newest version from the server. Run Tests and check if everything works. - Run `npm version ` - Where ` = patch, minor, major`. - [details](https://docs.npmjs.com/updating-your-published-package-version-number) + Where ` = patch, minor, major`. + [details](https://docs.npmjs.com/updating-your-published-package-version-number) - Verify that the version is correctly replaced and files were generated in: - * package.json - * src/index.ts - * dist/js - * dist/css + - package.json + - src/index.ts + - dist/js + - dist/css -- Push to server. `git push --tags`. +- Push to server. `git push --tags`. - Create a Pull Request from dev into main -- Merge the Pull Request if all checks pass and there were no concerns. (Set the draft release to public.) +- Merge the Pull Request if all checks pass and there were no concerns. - Create a new release on GitHub [New Release](https://github.com/materializecss/materialize/releases/new) - * Select the tag from the pushed commit - * Click "generate Release notes" - * Upload the zip-files from the dist-folder. + - Select the tag from the pushed commit + - Click "generate Release notes" + - Upload the zip-files from the dist-folder. - Checkout and pull main branch locally - Publish the release on npmjs - * If you never logged in on npm, please do `npm login` first and enter your credentials - * Check with `npm publish --dry-run` if all files are included correctly (dist/js/*.js + sass files) - * Then run `npm publish` and follow the instructions there + - If you never logged in on npm, please do `npm login` first and enter your credentials + - Check with `npm publish --dry-run` if all files are included correctly (dist/js/\*.js + sass files) + - Then run `npm publish` and follow the instructions there -- Generate new docs \ No newline at end of file +- Generate new docs diff --git a/eslint.config.mjs b/eslint.config.mjs index edaaec625d..3fe345d845 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,4 @@ import typescriptEslint from 'typescript-eslint'; -import storybook from 'eslint-plugin-storybook'; export default [ { files: ['src/**/*.{ts,js,mjs}'] }, @@ -8,7 +7,6 @@ export default [ ignores: ['dist', 'tsconfig.json', 'node_modules'] }, ...typescriptEslint.configs.recommended, - ...storybook.configs['flat/recommended'], { rules: { 'prefer-const': 'error', diff --git a/examples/compile.sh b/examples/compile.sh new file mode 100755 index 0000000000..2558f91c05 --- /dev/null +++ b/examples/compile.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +pwd +cd examples +mkdir html + +node ./src/blog.js > ./html/blog.html +node ./src/landingpage.js > ./html/landingpage.html +node ./src/portfolio.js > ./html/portfolio.html +node ./src/test-forms.js > ./html/test-forms.html diff --git a/examples/counter-app.iso.js b/examples/counter-app.iso.js new file mode 100644 index 0000000000..e35646bc9b --- /dev/null +++ b/examples/counter-app.iso.js @@ -0,0 +1,64 @@ +import { Number, Text } from '../src/atomic/atomic.mjs'; +import { Card } from '../src/card/card.mjs'; +import { Button } from '../src/button/button.mjs'; + +function createCounterApp() { + return new Card({ + children: [ + new Text('My Counter').setTagName('div'), + new Number(9), + new Button('+'), + new Button('-') + ] + }).addClassname('p-3'); +} + +//====== Client Side Logic + +function count(input) { + const value = input.value + input.increment; + return { value }; +} + +function hydrate(domElement, initState = { value: 12 }) { + const state = initState; + + const btns = domElement.querySelectorAll('.btn'); // input + const numberEl = domElement.querySelector('.mw-number'); // output + + numberEl.innerHTML = state.value; + + btns[0].addEventListener('click', () => { + state.value = count({ value: state.value, increment: 1 }).value; + numberEl.innerHTML = state.value; + }); + btns[1].addEventListener('click', () => { + state.value = count({ value: state.value, increment: -1 }).value; + numberEl.innerHTML = state.value; + }); +} + +class CounterApp extends HTMLElement { + constructor() { + super(); + this.count = 0; + } + + connectedCallback() { + // Da das Template via DSD geladen wurde, existiert shadowRoot bereits! + if (this.shadowRoot) { + this.btn = this.shadowRoot.querySelector('#btn'); + this.output = this.shadowRoot.querySelector('#output'); + + // Event-Listener hinzufügen + this.btn.addEventListener('click', () => this.increment()); + } + } + + increment() { + this.count++; + this.output.textContent = this.count; + } +} + +export { createCounterApp, hydrate, CounterApp }; diff --git a/examples/src/blog.js b/examples/src/blog.js new file mode 100644 index 0000000000..cabaa593b8 --- /dev/null +++ b/examples/src/blog.js @@ -0,0 +1,101 @@ +import { Text } from '../../src/atomic/atomic.mjs'; +import { Component } from '../../src/atomic/component.mjs'; +import { Page, Container } from '../../src/atomic/page.mjs'; +import { Breadcrumb } from '../../src/breadcrumb/breadcrumb.mjs'; +import { Button } from '../../src/button/button.mjs'; +import { AssistChip } from '../../src/chip/chip.mjs'; + +// Article +class BlogArticle extends Component { + constructor(options) { + super(options); + this.setTagName('article'); + this.addClassname('card').addClassname('p-5'); + } +} +const myArticle = new BlogArticle(` +

01/01/2026 posted by Daniel

+

Crafting Sleek, Responsive Interfaces with MaterializeWeb

+

In the fast-evolving landscape of web development, building clean, responsive, and intuitive user interfaces remains a top priority. While major CSS frameworks like Bootstrap have long dominated the scene, MaterializeWeb—the community-driven evolution of MaterializeCSS—offers a refreshing design philosophy anchored directly in Google’s Material Design principles.

+

What is MaterializeWeb?

+

MaterializeWeb is an open-source, responsive front-end framework designed to streamline frontend development. Inspired by physical tactile materials like paper and ink, Materialize translates real-world physics into digital interfaces. It packages complex design concepts into ready-to-use HTML, CSS, and JavaScript components, allowing developers to create consistent, polished web applications with minimal overhead.

+

Key Features and Benefits

+
    +
  • Material Design Aesthetic: Out of the box, MaterializeWeb provides realistic shadow depths, smooth motion transitions, and responsive ripple effects that make UI elements feel interactive and tactile.
  • +
  • Community-Driven Continuity: Originally created as MaterializeCSS, the framework was actively adapted and maintained as MaterializeWeb by an open-source developer community, ensuring modern web standard compliance, security updates, and active bug fixes.
  • +
  • Flexible Grid & Utilities: Featuring a robust CSS grid system, flexbox support, and utility classes for typography, color palettes, and spacing, setting up complex responsive layouts takes minutes rather than hours.
  • +
  • Rich Component Suite: Developers get access to pre-styled components, including floating action buttons (FABs), navigation bars, dynamic modals, toast notifications, cards, and interactive carousels.
  • +
+

Why Choose It for Your Next Project?

+

If you want your web applications to mirror the sleek, unified visual identity of modern mobile apps without spending weeks writing custom styles, MaterializeWeb is an outstanding choice. It strikes an ideal balance between aesthetic polish and developer efficiency, making it perfect for rapid prototyping and production-ready sites alike.

+

Give MaterializeWeb a try on your next project to experience how effortless modern visual design can be!

`); + +// Page +const blogPage = new Page({ + title: 'Materialize Web Blog', + description: 'This is a custom blog', + keywords: 'News sites, personal journals, niche resource hubs, and content creators.', + children: new Container({ + children: [ + // TODO: use NavigationBar Component + new Container({ + children: new Text('Materialize Web Blog').setTagName('p').addClassname('py-3') + }).addClassname('wrapper-1'), + + new Container({ + children: [ + new AssistChip({ href: './landingpage.html', name: 'Visit Landingpage' }) + .addClassname('tooltipped') // tooltip + .setAttribute('data-tooltip', "I'm a toolip") + .setAttribute('data-position', 'bottom'), + new AssistChip({ href: './portfolio.html', name: 'Visit Porfolio' }) + ] + }) + .addClassname('g-2') + .addClassname('p-2') + .addClassname('secondary') + .addClassname('row'), + + new Breadcrumb() + .setCrumbs([ + 'Home', + 'Articles', + 'Crafting Sleek, Responsive Interfaces with MaterializeWeb' + ]) + .addClassname('py-5'), + + myArticle, + new Container({ + children: + '2 or 3-column masonry grid showing article thumbnails, titles, and publication dates' + }).addClassname('py-5'), + new Button({ children: 'Call to action' }), + new Text( + `This is a test
Landingpage. ok it can produce nested html too...
+ Or you can go to the Portfolio` + ), + new Container({ + children: 'Sidebar or navigation dropdowns ("Tech," "Lifestyle")' + }).addClassname('py-5') + ] + }) + .setTagName('main') + .addClassname('container') +}); +// CSS +blogPage + .addStyleUrl('/dist/css/materialize.css') + .addStyleUrl('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined') + .addStyle(`article { font-family: Times; line-height: 1.25; } + h1 { font-size: 2.75em; } + h2 { font-size: 2em; } + h3 { font-size: 1.5em; } + p, li { font-size: 1.2em; } + `); +// Client JS +blogPage.addJavascriptUrl('/dist/js/materialize.js'); +blogPage.addJavascript(`M.AutoInit();`); + +// Render +const html = blogPage.toHTML(); +console.log(html); diff --git a/examples/src/landingpage.js b/examples/src/landingpage.js new file mode 100644 index 0000000000..452243223e --- /dev/null +++ b/examples/src/landingpage.js @@ -0,0 +1,49 @@ +import { Text } from '../../src/atomic/atomic.mjs'; +import { Container, Page } from '../../src/atomic/page.mjs'; + +const landingPage = new Page({ + title: 'T-Shirt Landingpage', + description: 'Introducing the new T-Shirt from Materialize', + keywords: 'App launches, SaaS companies, small service businesses, and single-product e-commerce', + children: [ + new Container({ children: new Text('a').setTagName('h1') }), + //new Container(``), + new Container({ children: 'T-Shirt', description: `Nothing to see here` }), + // + new Container(`A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
+ feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
+ You can also checkout the Blog + klöxökkökökxl +
+ + + + + `), + + new Container('Hello, this is my landing page!').addClassname('pt-5') + ] +}); +// CSS +landingPage + .addStyleUrl('/dist/css/materialize.css') + .addStyleUrl('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined'); +// Client JS +landingPage.addJavascriptUrl('/dist/js/materialize.js'); +landingPage.addJavascript(`M.AutoInit();`); + +const html = landingPage.toHTML(); +console.log(html); diff --git a/examples/src/portfolio.js b/examples/src/portfolio.js new file mode 100644 index 0000000000..112ed9cffe --- /dev/null +++ b/examples/src/portfolio.js @@ -0,0 +1,29 @@ +import { Text } from '../../src/atomic/atomic.mjs'; +import { Container, Page } from '../../src/atomic/page.mjs'; +import { Grid } from '../../src/grid/grid.mjs'; + +const portfolioPage = new Page({ + title: 'My Portfolio', + description: 'This is a custom gallery of my items', + keywords: 'Gallery, Products, Stuff', + children: [ + new Container({ children: new Text('My Portfolio').setTagName('h1') }), + new Container( + `Hello, this is my portfolio. You can also checkout the Blog` + ).addClassname('pb-5'), + new Container({ children: 'Sidebar' }), + new Grid({ + children: [ + ``, + ``, + ``, + `` + ] + }) + .setColumns(2) + .addClassname('py-5') + ] +}).addStyleUrl('/dist/css/materialize.css'); + +const html = portfolioPage.toHTML(); +console.log(html); diff --git a/examples/src/test-forms.js b/examples/src/test-forms.js new file mode 100644 index 0000000000..8380901b72 --- /dev/null +++ b/examples/src/test-forms.js @@ -0,0 +1,155 @@ +import { Page } from '../../src/atomic/page.mjs'; +import { Component } from '../../src/atomic/component.mjs'; +import { Grid } from '../../src/grid/grid.mjs'; +import { Button } from '../../src/button/button.mjs'; +import { TextField } from '../../src/textfield/textfield.mjs'; +import { Dialog } from '../../src/dialog/dialog.mjs'; +import { Divider } from '../../src/divider/divider.mjs'; +import { List, ListItem } from '../../src/list/list.mjs'; +import { RadioButton } from '../../src/radiobutton/radiobutton.mjs'; +import { Switch } from '../../src/switch/switch.mjs'; +import { Checkbox } from '../../src/checkbox/checkbox.mjs'; +import { AssistChip } from '../../src/chip/chip.mjs'; +import { Breadcrumb } from '../../src/breadcrumb/breadcrumb.mjs'; +import { MiniCalendar } from '../../src/datepicker/minicalendar.mjs'; + +// Dynamic Content Generation on Serverside + +const formTestPage = new Page({ + title: 'Test: Forms | MaterializeWeb', + children: [ + new Component({ + children: [ + new Component('Grid').addClassname('my-3'), + new Component(`
+
YO
+
`), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Modals').addClassname('my-3'), + // new Dialog({ header: 'Question of the day', content: 'Wazz up?' }), + new Dialog({ + header: 'How old are you?', + content: + new Component('Please enter your age:').addClassname('my-3').toHTML() + + new TextField().setLabel('Age').toHTML(), + footer: new Button('Confirm').addClassname('text').toHTML() + }), + new Button('Show').addClassname('tonal').addClassname('btn-modal-1'), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Lists').addClassname('my-3'), + new List() + .setLines(2) + .addItem( + new ListItem() + .setText('Forks') + .setSupportingText('Some Forks') + .setImage('https://picsum.photos/id/23/200') + ) + .addItem( + new ListItem() + .setText('Books') + .setSupportingText('An open book') + .setImage('https://picsum.photos/id/24/200') + .setTrailingContent( + new Button('more_vert') + .addClassname('text') + .toHTML() + ) + ), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Buttons').addClassname('my-3'), + new Grid({ + children: [ + ...Array.from({ length: 6 }, (_, i) => new Button('Default-' + (i + 1)).toHTML()), + ...Array.from({ length: 6 }, (_, i) => + new Button('Tonal-' + (i + 1)).addClassname('tonal').toHTML() + ), + ...Array.from({ length: 6 }, (_, i) => + new Button('Outl-' + (i + 1)).addClassname('outlined').toHTML() + ), + ...Array.from({ length: 6 }, (_, i) => + new Button('Text-' + (i + 1)).addClassname('text').toHTML() + ) + ] + }) + .setColumns(6) + .addClassname('g-1') + .addClassname('mb-3'), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Chips : Assist (for States)').addClassname('my-3'), + new AssistChip('Ready'), + new AssistChip('Active').setAttribute('style', 'color: orange; outline-color: orange;'), + new AssistChip('Completed').setAttribute('style', 'color: green; outline-color: green;'), + new AssistChip('Failed').setAttribute('style', 'color: red; outline-color: red;'), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Radio Buttons, Switch and Checkboxes').addClassname('my-3'), + new Grid({ + children: [ + new Component({ + children: [ + new RadioButton({ group: 'group1', text: 'Red' }), // todo: checked + new RadioButton({ group: 'group1', text: 'Green' }), + new RadioButton({ group: 'group1', text: 'Blue' }), + new RadioButton({ group: 'group1', text: 'Brown' }) // todo: disabled + ] + }).toHTML(), + new Switch().toHTML(), + new Component({ + children: [ + new Checkbox(), // + new Checkbox('Accept Privacy Agreement'), + new Checkbox({ isChecked: true, text: 'Pre-checked' }), + new Checkbox({ text: 'Indeterminate' }).addClassname('cb-ind'), // has to be set via js + new Checkbox({ text: 'Disabled', isDisabled: true }) + ] + }).toHTML() + ] + }).setColumns(3), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Date- and Timepicker').addClassname('my-3'), + new MiniCalendar(), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Breadcrumbs').addClassname('my-3'), + new Breadcrumb().setCrumbs(['Home', 'Chillaxing', 'Living Room']), + + new Divider().addClassname('my-3'), //---------------------- + new Component('Many inputs').addClassname('my-3'), + new Grid({ + children: Array.from({ length: 33 }, (_, i) => new TextField().setLabel(i + 1).toHTML()) + }) + .setColumns(6) + .addClassname('g-1') + ] + }) + .addClassname('p-2') + .addClassname('container') + .addClassname('my-5') + ] +}) + .addStyleUrl('/dist/css/materialize.css') + .addStyleUrl('/src/breadcrumb/breadcrumb.css') + .addStyleUrl('/src/datepicker/minicalendar.css').addStyle(`:root { + --mw-chip-height: 20px; + --mw-input-height: 42px; + --mw-padding-left: 4px; + --mw-border-radius: 4px; + }`) // client-side JS + .addJavascript(` +document.querySelector('.btn-modal-1').addEventListener('click', () => { + document.querySelector('dialog').showModal(); +}); + +const cb = document.querySelector('.cb-ind input'); +if (cb) cb.indeterminate = true; +`); +//.addStyleUrl('/src/breadcrumb/breadcrumb.css') // client-side custom theming + +const html = formTestPage.toHTML(); +console.log(html); diff --git a/index.html b/index.html index a104bce7ad..ca8e551fac 100644 --- a/index.html +++ b/index.html @@ -17,8 +17,8 @@ href="/dist/css/materialize.min.css" media="screen,projection" /> - - + + `).join('\n')} + + + ${content} + ${this.#scriptUrls.map((url) => ``).join('\n')} + ${this.#scripts.map((s) => ``).join('\n')} + +`; + } +} + +class Container extends Component {} + +export { Page, Container }; diff --git a/src/autocomplete/README.md b/src/autocomplete/README.md new file mode 100644 index 0000000000..c079848511 --- /dev/null +++ b/src/autocomplete/README.md @@ -0,0 +1,3 @@ +# Autocomplete + +This will soon be deprecated. Please use the select at textfields and chip sets for multiselect. diff --git a/src/autocomplete/autocomplete.test.ts b/src/autocomplete/autocomplete.test.ts new file mode 100644 index 0000000000..5594a85239 --- /dev/null +++ b/src/autocomplete/autocomplete.test.ts @@ -0,0 +1,137 @@ +// @vitest-environment happy-dom +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import { Autocomplete, AutocompleteData } from './autocomplete'; + +// Mock dependencies if required by your setup +vi.mock('../dropdown/dropdown', () => ({ + Dropdown: { + init: vi.fn(() => ({ + open: vi.fn(), + close: vi.fn(), + recalculateDimensions: vi.fn(), + isOpen: false + })) + } +})); + +vi.mock('../../src/utils', () => ({ + Utils: { + guid: () => '12345', + keys: { + ENTER: ['Enter'], + ARROW_UP: ['ArrowUp'], + ARROW_DOWN: ['ArrowDown'] + }, + tabPressed: false + } +})); + +describe('Autocomplete Component', () => { + let inputEl: HTMLInputElement; + let sampleData: AutocompleteData[]; + + beforeEach(() => { + document.body.innerHTML = ` +
+ +
+ `; + inputEl = document.querySelector('#autocomplete-input') as HTMLInputElement; + + sampleData = [ + { id: 1, text: 'Apple', description: 'Fruit' }, + { id: 2, text: 'Banana', description: 'Fruit' }, + { id: 3, text: 'Cherry', description: 'Fruit' } + ]; + }); + + afterEach(() => { + const instance = Autocomplete.getInstance(inputEl); + if (instance) { + instance.destroy(); + } + document.body.innerHTML = ''; + vi.clearAllMocks(); + }); + + it('should initialize correctly and attach instance to element', () => { + const instance = new Autocomplete(inputEl, { data: sampleData }); + + expect(instance).toBeInstanceOf(Autocomplete); + expect(Autocomplete.getInstance(inputEl)).toBe(instance); + expect(inputEl.getAttribute('data-target')).toContain('autocomplete-options-'); + }); + + it('should render menu items when opened', () => { + const instance = new Autocomplete(inputEl, { data: sampleData, minLength: 1 }); + inputEl.value = 'App'; + + instance.open(); + + const listItems = instance.container.querySelectorAll('li'); + expect(listItems.length).toBe(3); + expect(listItems[0].getAttribute('data-id')).toBe('1'); + expect(listItems[0].textContent).toContain('Apple'); + }); + + it('should filter items via default onSearch handler', () => { + const instance = new Autocomplete(inputEl, { data: sampleData }); + inputEl.value = 'Ban'; + // Simulate input change detection triggered by keyup/focus + instance._inputChangeDetection('ban'); + expect(instance.menuItems.length).toBe(1); + expect(instance.menuItems[0].text).toBe('Banana'); + }); + + it('should trigger onAutocomplete callback when an option is selected', () => { + const onAutocompleteMock = vi.fn(); + const instance = new Autocomplete(inputEl, { + data: sampleData, + onAutocomplete: onAutocompleteMock + }); + + instance.selectOption(2); + + expect(onAutocompleteMock).toHaveBeenCalledWith([ + { id: 2, text: 'Banana', description: 'Fruit' } + ]); + expect(inputEl.value).toBe('Banana'); + }); + + it('should handle multi-selection correctly', () => { + const onAutocompleteMock = vi.fn(); + const instance = new Autocomplete(inputEl, { + data: sampleData, + isMultiSelect: true, + onAutocomplete: onAutocompleteMock + }); + + instance.selectOption(1); + instance.selectOption(3); + + expect(instance.selectedValues.length).toBe(2); + expect(instance.selectedValues).toEqual([sampleData[0], sampleData[2]]); + expect(onAutocompleteMock).toHaveBeenLastCalledWith([sampleData[0], sampleData[2]]); + }); + + it('should highlight partial matching text correctly', () => { + const instance = new Autocomplete(inputEl, { data: sampleData }); + inputEl.value = 'App'; + + const item = instance._createDropdownItem(sampleData[0]); + const highlightSpan = item.querySelector('.highlight'); + + expect(highlightSpan).not.toBeNull(); + expect(highlightSpan?.textContent).toBe('App'); + }); + + it('should clean up DOM and event listeners on destroy', () => { + const instance = new Autocomplete(inputEl, { data: sampleData }); + const containerId = instance.container.id; + + instance.destroy(); + + expect(Autocomplete.getInstance(inputEl)).toBeUndefined(); + expect(document.getElementById(containerId)).toBeNull(); + }); +}); diff --git a/components/search/autocomplete.ts b/src/autocomplete/autocomplete.ts similarity index 69% rename from components/search/autocomplete.ts rename to src/autocomplete/autocomplete.ts index 1d68c7d431..1c5a89110a 100644 --- a/components/search/autocomplete.ts +++ b/src/autocomplete/autocomplete.ts @@ -1,8 +1,8 @@ -import { Utils } from '../../src/utils'; +import { Utils } from '../utils'; import { Dropdown, DropdownOptions } from '../dropdown/dropdown'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; -export interface AutocompleteData { +interface AutocompleteData { /** * A primitive value that can be converted to string. * If "text" is not provided, it will also be used as "option text" as well @@ -23,7 +23,7 @@ export interface AutocompleteData { description?: string; } -export interface AutocompleteOptions extends BaseOptions { +interface AutocompleteOptions extends BaseOptions { /** * Data object defining autocomplete options with * optional icon strings. @@ -37,7 +37,7 @@ export interface AutocompleteOptions extends BaseOptions { /** * Callback for when autocompleted. */ - onAutocomplete: (entries: AutocompleteData[]) => void; + onAutocomplete: ((entries: AutocompleteData[]) => void) | null; /** * Minimum number of characters before autocomplete starts. * @default 1 @@ -69,19 +69,18 @@ export interface AutocompleteOptions extends BaseOptions { /** * Predefined selected values */ - selected: number[] | string[]; + selected: (number | string)[]; } const _defaults: AutocompleteOptions = { - data: [], // Autocomplete data set - onAutocomplete: null, // Callback for when autocompleted + data: [], + onAutocomplete: null, dropdownOptions: { - // Default dropdown options autoFocus: false, closeOnClick: false, coverTrigger: false }, - minLength: 1, // Min characters before autocomplete starts + minLength: 1, isMultiSelect: false, onSearch: (text: string, autocomplete: Autocomplete) => { const normSearch = text.toLocaleLowerCase(); @@ -95,10 +94,10 @@ const _defaults: AutocompleteOptions = { }, maxDropDownHeight: '300px', allowUnsafeHTML: false, - selected: [], + selected: [] }; -export class Autocomplete extends Component { +class Autocomplete extends Component { declare el: HTMLInputElement; /** If the autocomplete is open. */ isOpen: boolean; @@ -106,7 +105,7 @@ export class Autocomplete extends Component { count: number; /** Index of the current selected option. */ activeIndex: number; - private oldVal: string; + private oldVal: string | null; private $active: HTMLElement | null; private _mousedown: boolean; container: HTMLElement; @@ -132,8 +131,9 @@ export class Autocomplete extends Component { this.oldVal = ''; this.selectedValues = this.selectedValues || - this.options.selected.map((value: number | string) => { id: value }) || - []; + (this.options.selected + ? this.options.selected.map((value: number | string) => ({ id: value }) as AutocompleteData) + : []); this.menuItems = this.options.data || []; this.data = this.options.data || []; this.$active = null; @@ -183,41 +183,48 @@ export class Autocomplete extends Component { this.el['M_Autocomplete'] = undefined; } - _setupEventHandlers() { + _setupEventHandlers = () => { this.el.addEventListener('blur', this._handleInputBlur); this.el.addEventListener('keyup', this._handleInputKeyup); this.el.addEventListener('focus', this._handleInputFocus); this.el.addEventListener('keydown', this._handleInputKeydown); this.el.addEventListener('click', this._handleInputClick); - this.container.addEventListener('mousedown', this._handleContainerMousedownAndTouchstart); - this.container.addEventListener('mouseup', this._handleContainerMouseupAndTouchend); - if (typeof window.ontouchstart !== 'undefined') { - this.container.addEventListener('touchstart', this._handleContainerMousedownAndTouchstart); - this.container.addEventListener('touchend', this._handleContainerMouseupAndTouchend); + if (this.container) { + this.container.addEventListener('mousedown', this._handleContainerMousedownAndTouchstart); + this.container.addEventListener('mouseup', this._handleContainerMouseupAndTouchend); + if (typeof window !== 'undefined' && 'ontouchstart' in window) { + this.container.addEventListener('touchstart', this._handleContainerMousedownAndTouchstart); + this.container.addEventListener('touchend', this._handleContainerMouseupAndTouchend); + } } - } + }; - _removeEventHandlers() { + _removeEventHandlers = () => { this.el.removeEventListener('blur', this._handleInputBlur); this.el.removeEventListener('keyup', this._handleInputKeyup); this.el.removeEventListener('focus', this._handleInputFocus); this.el.removeEventListener('keydown', this._handleInputKeydown); this.el.removeEventListener('click', this._handleInputClick); - this.container.removeEventListener('mousedown', this._handleContainerMousedownAndTouchstart); - this.container.removeEventListener('mouseup', this._handleContainerMouseupAndTouchend); - - if (typeof window.ontouchstart !== 'undefined') { - this.container.removeEventListener('touchstart', this._handleContainerMousedownAndTouchstart); - this.container.removeEventListener('touchend', this._handleContainerMouseupAndTouchend); + if (this.container) { + this.container.removeEventListener('mousedown', this._handleContainerMousedownAndTouchstart); + this.container.removeEventListener('mouseup', this._handleContainerMouseupAndTouchend); + + if (typeof window !== 'undefined' && 'ontouchstart' in window) { + this.container.removeEventListener( + 'touchstart', + this._handleContainerMousedownAndTouchstart + ); + this.container.removeEventListener('touchend', this._handleContainerMouseupAndTouchend); + } } - } + }; _setupDropdown() { this.container = document.createElement('ul'); this.container.style.maxHeight = this.options.maxDropDownHeight; this.container.id = `autocomplete-options-${Utils.guid()}`; this.container.classList.add('autocomplete-content', 'dropdown-content'); - this.container.ariaExpanded = 'true'; + this.container.setAttribute('aria-expanded', 'true'); this.el.setAttribute('data-target', this.container.id); this.menuItems.forEach((menuItem) => { @@ -225,53 +232,78 @@ export class Autocomplete extends Component { this.container.append(itemElement); }); - // ! Issue in Component Dropdown: _placeDropdown moves dom-position - this.el.parentElement.appendChild(this.container); + if (this.el.parentElement) { + this.el.parentElement.appendChild(this.container); + } // Initialize dropdown const dropdownOptions = { ...Autocomplete.defaults.dropdownOptions, ...this.options.dropdownOptions }; - // @todo shouldn't we conditionally check if dropdownOptions.onItemClick is set in first place? + const userOnItemClick = dropdownOptions.onItemClick; - // Ensuring the select Option call when user passes custom onItemClick function to dropdown - dropdownOptions.onItemClick = (li) => { + dropdownOptions.onItemClick = (li: HTMLElement) => { if (!li) return; const entryID = li.getAttribute('data-id'); - this.selectOption(entryID); - // Handle user declared onItemClick if needed - if (userOnItemClick && typeof userOnItemClick === 'function') + if (entryID !== null) { + this.selectOption(entryID); + } + if (userOnItemClick && typeof userOnItemClick === 'function') { userOnItemClick.call(this.dropdown, this.el); + } }; - this.dropdown = Dropdown.init(this.el, dropdownOptions); - // ! Workaround for Label: move label up again - // TODO: Just use PopperJS in future! - const label = this.el.parentElement.querySelector('label'); - if (label) this.el.after(label); + // Safely resolve Dropdown implementation without using `any` + type DropdownConstructor = { + init: (el: HTMLElement, options?: Partial) => Dropdown; + }; - // Sketchy removal of dropdown click handler - this.el.removeEventListener('click', this.dropdown._handleClick); - if (!this.options.isMultiSelect && !(this.options.selected.length === 0)) { + const DropdownClass = Dropdown?.init + ? Dropdown + : (Dropdown as unknown as { default?: DropdownConstructor })?.default?.init + ? (Dropdown as unknown as { default: DropdownConstructor }).default + : (globalThis as unknown as { Dropdown: DropdownConstructor }).Dropdown || + (window as unknown as { Dropdown: DropdownConstructor }).Dropdown; + + this.dropdown = DropdownClass.init(this.el, dropdownOptions); + + if (this.el.parentElement) { + const label = this.el.parentElement.querySelector('label'); + if (label) this.el.after(label); + } + + if (this.dropdown && this.dropdown._handleClick) { + this.el.removeEventListener('click', this.dropdown._handleClick); + } + + if (!this.options.isMultiSelect && this.options.selected && this.options.selected.length > 0) { const selectedValue = this.menuItems.filter( - (value) => value.id === this.selectedValues[0].id + (value) => value.id === this.selectedValues[0]?.id ); - this.el.value = selectedValue[0].text; + if (selectedValue[0]?.text) { + this.el.value = selectedValue[0].text; + } } - // Set Value if already set in HTML + if (this.el.value) this.selectOption(this.el.value); - // Add StatusInfo + const div = document.createElement('div'); div.classList.add('status-info'); div.setAttribute('style', 'position: absolute;right:0;top:0;'); - this.el.parentElement.appendChild(div); + if (this.el.parentElement) { + this.el.parentElement.appendChild(div); + } this._updateSelectedInfo(); } _removeDropdown() { - this.container.ariaExpanded = 'false'; - this.container.parentNode.removeChild(this.container); + if (this.container) { + this.container.setAttribute('aria-expanded', 'false'); + if (this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + } } _handleInputBlur = () => { @@ -285,15 +317,13 @@ export class Autocomplete extends Component { if (e.type === 'keyup') Autocomplete._keydown = false; this.count = 0; const actualValue = this.el.value.toLocaleLowerCase(); - // Don't capture enter or arrow key usage. if ( Utils.keys.ENTER.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key) || Utils.keys.ARROW_DOWN.includes(e.key) ) return; - // Check if the input isn't empty - // Check if focus triggered by tab + if (this.oldVal !== actualValue && Utils.tabPressed) { this.open(); } @@ -307,13 +337,15 @@ export class Autocomplete extends Component { }; _inputChangeDetection = (value: string) => { - // Value has changed! if (this.oldVal !== value) { this._setStatusLoading(); this.options.onSearch(this.el.value, this); } - // Reset Single-Select when Input cleared - if (!this.options.isMultiSelect && this.el.value.length === 0) { + if ( + !this.options.isMultiSelect && + this.el.value.length === 0 && + this.selectedValues.length > 0 + ) { this.selectedValues = []; this._triggerChanged(); } @@ -322,18 +354,18 @@ export class Autocomplete extends Component { _handleInputKeydown = (e: KeyboardEvent) => { Autocomplete._keydown = true; - // Arrow keys and enter key usage const numItems = this.container.querySelectorAll('li').length; - // select element on Enter + if (Utils.keys.ENTER.includes(e.key) && this.activeIndex >= 0) { const liElement = this.container.querySelectorAll('li')[this.activeIndex]; if (liElement) { - this.selectOption(liElement.getAttribute('data-id')); + const id = liElement.getAttribute('data-id'); + if (id !== null) this.selectOption(id); e.preventDefault(); } return; } - // Capture up and down key + if (Utils.keys.ARROW_UP.includes(e.key) || Utils.keys.ARROW_DOWN.includes(e.key)) { e.preventDefault(); if (Utils.keys.ARROW_UP.includes(e.key) && this.activeIndex > 0) this.activeIndex--; @@ -341,10 +373,9 @@ export class Autocomplete extends Component { this.activeIndex++; this.$active?.classList.remove('active'); if (this.activeIndex >= 0) { - this.$active = this.container.querySelectorAll('li')[this.activeIndex]; + this.$active = this.container.querySelectorAll('li')[this.activeIndex] as HTMLElement; this.$active?.classList.add('active'); - // Focus selected - this.container.children[this.activeIndex].scrollIntoView({ + this.container.children[this.activeIndex]?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' @@ -371,18 +402,19 @@ export class Autocomplete extends Component { } _resetAutocomplete() { - this.container.replaceChildren(); + if (this.container) { + this.container.innerHTML = ''; + } this._resetCurrentElementPosition(); this.oldVal = null; this.isOpen = false; this._mousedown = false; } - _highlightPartialText(input: string, label: string) { - const start = label.toLocaleLowerCase().indexOf('' + input.toLocaleLowerCase() + ''); + _highlightPartialText(input: string, label: string): [string, string, string] { + const start = label.toLocaleLowerCase().indexOf(input.toLocaleLowerCase()); const end = start + input.length - 1; - //custom filters may return results where the string does not match any part - if (start == -1 || end == -1) { + if (start === -1 || end === -1 || input === '') { return [label, '', '']; } return [label.slice(0, start), label.slice(start, end + 1), label.slice(end + 1)]; @@ -390,13 +422,13 @@ export class Autocomplete extends Component { _createDropdownItem(entry: AutocompleteData) { const item = document.createElement('li'); - item.setAttribute('data-id', entry.id); + item.setAttribute('data-id', String(entry.id)); item.setAttribute( 'style', 'display:grid; grid-auto-flow: column; user-select: none; align-items: center;' ); item.tabIndex = 0; - // Checkbox + if (this.options.isMultiSelect) { item.innerHTML = `
@@ -405,7 +437,7 @@ export class Autocomplete extends Component { }>
`; } - // Image + if (entry.image) { const img = document.createElement('img'); img.classList.add('circle'); @@ -413,11 +445,11 @@ export class Autocomplete extends Component { item.appendChild(img); } - // Text const inputText = this.el.value.toLocaleLowerCase(); const parts = this._highlightPartialText(inputText, (entry.text || entry.id).toString()); const div = document.createElement('div'); div.setAttribute('style', 'line-height:1.2;font-weight:500;'); + if (this.options.allowUnsafeHTML) { div.innerHTML = parts[0] + '' + parts[1] + '' + parts[2]; } else { @@ -434,39 +466,39 @@ export class Autocomplete extends Component { const itemText = document.createElement('div'); itemText.classList.add('item-text'); itemText.setAttribute('style', 'padding:5px;overflow:hidden;'); + itemText.appendChild(div); item.appendChild(itemText); - item.querySelector('.item-text').appendChild(div); - // Description - if ( - typeof entry.description === 'string' || - (typeof entry.description === 'number' && !isNaN(entry.description)) - ) { + + if (typeof entry.description === 'string' || typeof entry.description === 'number') { const description = document.createElement('small'); description.setAttribute( 'style', 'line-height:1.3;color:grey;white-space:nowrap;text-overflow:ellipsis;display:block;width:90%;overflow:hidden;' ); - description.innerText = entry.description; - item.querySelector('.item-text').appendChild(description); + description.innerText = String(entry.description); + itemText.appendChild(description); } - // Set Grid + const getGridConfig = () => { if (this.options.isMultiSelect) { - if (entry.image) return '40px min-content auto'; // cb-img-txt - return '40px auto'; // cb-txt + if (entry.image) return '40px min-content auto'; + return '40px auto'; } - if (entry.image) return 'min-content auto'; // img-txt - return 'auto'; // txt + if (entry.image) return 'min-content auto'; + return 'auto'; }; item.style.gridTemplateColumns = getGridConfig(); return item; } _renderDropdown() { - this._resetAutocomplete(); - // Check if Data is empty + if (this.container) { + this.container.innerHTML = ''; + } + this._resetCurrentElementPosition(); + if (this.menuItems.length === 0) { - this.menuItems = this.selectedValues; // Show selected Items + this.menuItems = this.selectedValues; } for (let i = 0; i < this.menuItems.length; i++) { const item = this._createDropdownItem(this.menuItems[i]); @@ -475,17 +507,18 @@ export class Autocomplete extends Component { } _setStatusLoading() { - this.el.parentElement.querySelector( - '.status-info' - ).innerHTML = `
- - - -
`; + const statusInfo = this.el.parentElement?.querySelector('.status-info'); + if (statusInfo) { + statusInfo.innerHTML = `
+ + + +
`; + } } _updateSelectedInfo() { - const statusElement = this.el.parentElement.querySelector('.status-info'); + const statusElement = this.el.parentElement?.querySelector('.status-info'); if (statusElement) { if (this.options.isMultiSelect) statusElement.innerHTML = this.selectedValues.length.toString(); @@ -496,13 +529,12 @@ export class Autocomplete extends Component { _refreshInputText() { if (this.selectedValues.length === 1) { const entry = this.selectedValues[0]; - this.el.value = entry.text || entry.id; // Write Text to Input + this.el.value = entry.text || String(entry.id); } } _triggerChanged() { this.el.dispatchEvent(new Event('change')); - // Trigger Autocomplete Event if (typeof this.options.onAutocomplete === 'function') this.options.onAutocomplete.call(this, this.selectedValues); } @@ -512,24 +544,26 @@ export class Autocomplete extends Component { */ open = () => { const inputText = this.el.value.toLocaleLowerCase(); - this._resetAutocomplete(); if (inputText.length >= this.options.minLength) { this.isOpen = true; this._renderDropdown(); } - // Open dropdown - if (!this.dropdown.isOpen) { + if (this.dropdown && !this.dropdown.isOpen) { setTimeout(() => { this.dropdown.open(); - }, 0); // TODO: why? - } else this.dropdown.recalculateDimensions(); // Recalculate dropdown when its already open + }, 0); + } else if (this.dropdown) { + this.dropdown.recalculateDimensions(); + } }; /** * Hide autocomplete. */ close = () => { - this.dropdown.close(); + if (this.dropdown) { + this.dropdown.close(); + } }; /** @@ -541,9 +575,9 @@ export class Autocomplete extends Component { */ setMenuItems( menuItems: AutocompleteData[], - selected: number[] | string[] = null, + selected: (number | string)[] | null = null, open: boolean = true, - initial: boolean = false, + initial: boolean = false ) { this.menuItems = menuItems; this.options.data = menuItems; @@ -551,9 +585,7 @@ export class Autocomplete extends Component { this.data = menuItems; } if (selected) { - this.selectedValues = this.menuItems.filter( - (item) => !(selected.indexOf(item.id) === -1) - ); + this.selectedValues = this.menuItems.filter((item) => selected.indexOf(item.id) !== -1); } if (this.options.isMultiSelect) { this._renderDropdown(); @@ -584,24 +616,18 @@ export class Autocomplete extends Component { selectOption(id: number | string) { const entry = this.menuItems.find((item) => item.id == id); if (!entry) return; - // Toggle Checkbox - /* const li = this.container.querySelector('li[data-id="' + id + '"]'); - if (!li) return;*/ + if (this.options.isMultiSelect) { - /* const checkbox = li.querySelector('input[type="checkbox"]'); - checkbox.checked = !checkbox.checked;*/ - if ( - !(this.selectedValues.filter((selectedEntry) => selectedEntry.id === entry.id).length >= 1) - ) + if (!this.selectedValues.some((selectedEntry) => selectedEntry.id === entry.id)) { this.selectedValues.push(entry); - else + } else { this.selectedValues = this.selectedValues.filter( (selectedEntry) => selectedEntry.id !== entry.id ); + } this._renderDropdown(); this.el.focus(); } else { - // Single-Select this.selectedValues = [entry]; this._refreshInputText(); this._resetAutocomplete(); @@ -611,10 +637,12 @@ export class Autocomplete extends Component { this._triggerChanged(); } - selectOptions(ids: []) { - const entries = this.menuItems.filter((item) => !(ids.indexOf(item.id) === -1)); - if (!entries) return; + selectOptions(ids: (number | string)[]) { + const entries = this.menuItems.filter((item) => ids.includes(item.id)); + if (!entries.length) return; this.selectedValues = entries; this._renderDropdown(); } } + +export { AutocompleteData, AutocompleteOptions, Autocomplete }; diff --git a/src/badge/README.md b/src/badge/README.md new file mode 100644 index 0000000000..5533e0fd07 --- /dev/null +++ b/src/badge/README.md @@ -0,0 +1,3 @@ +# Badges + +Indicate if there is any new information related to the underlying element. diff --git a/src/badge/badge.css b/src/badge/badge.css new file mode 100644 index 0000000000..d691871a81 --- /dev/null +++ b/src/badge/badge.css @@ -0,0 +1,76 @@ +:root { + --badge-height: 22px; +} + +span.badge { + min-width: 3rem; + padding: 0 6px; + margin-left: 14px; + text-align: center; + font-size: 1rem; + line-height: var(--badge-height); + height: var(--badge-height); + color: var(--md-sys-color-on-surface-variant); + float: right; + box-sizing: border-box; +} + +span.badge.new { + font-weight: 300; + font-size: 0.8rem; + color: var(--md-sys-color-on-primary); + background-color: var(--md-sys-color-primary); + border-radius: 2px; +} + +span.badge.new::after { + content: ' new'; +} + +span.badge[data-badge-caption]::after { + content: ' ' attr(data-badge-caption); +} + +.active span.badge { + color: var(--md-sys-color-on-primary); +} + +/* Special cases */ + +.navbar ul a span.badge { + display: inline-block; + float: none; + margin-left: 4px; + line-height: var(--badge-height); + height: var(--badge-height); + -webkit-font-smoothing: auto; +} + +/* Line height centering */ + +.collection-item span.badge { + margin-top: calc((1.5rem * 0.5) - (var(--badge-height) * 0.5)); +} + +.collapsible span.badge { + margin-left: auto; +} + +.collapsible span.badge.leading { + margin-right: 7px; + order: -1; +} + +.collapsible .active span.badge:not(.new) { + color: var(--md-sys-color-on-surface-variant); +} + +.sidenav span.badge { + margin-top: calc((var(--sidenav-line-height) * 0.5) - (22px * 0.5)); +} + +table span.badge { + display: inline-block; + float: none; + margin-left: auto; +} diff --git a/components/badge/badge.mjs b/src/badge/badge.mjs similarity index 100% rename from components/badge/badge.mjs rename to src/badge/badge.mjs diff --git a/src/bounding.ts b/src/bounding.ts deleted file mode 100644 index e73cf2315a..0000000000 --- a/src/bounding.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class Bounding { - left: number; // left offset coordinate - top: number; - width: number; - height: number; -} diff --git a/src/breadcrumb/README.md b/src/breadcrumb/README.md new file mode 100644 index 0000000000..1bffecd7ac --- /dev/null +++ b/src/breadcrumb/README.md @@ -0,0 +1,40 @@ +# Breadcrumb + +Good for navigating a tree structure. + +## Usage + +```html + +``` + +Deprecated, old: + +```html + +``` diff --git a/src/breadcrumb/breadcrumb.css b/src/breadcrumb/breadcrumb.css new file mode 100644 index 0000000000..f7541681c4 --- /dev/null +++ b/src/breadcrumb/breadcrumb.css @@ -0,0 +1,56 @@ +.mw-breadcrumb { + display: flex; + align-items: center; + padding: 0; + margin: 0; + list-style: none; + font: var(--md-sys-typescale-body-large); +} +/* List item */ +.mw-breadcrumb-item { + display: inline-flex; + align-items: center; + color: var(--md-sys-color-on-surface-variant); +} +/* Interactive Link */ +.mw-breadcrumb-link { + display: inline-flex; + align-items: center; + gap: 6px; + text-decoration: none; + color: var(--md-sys-color-on-surface-variant); + padding: 6px 8px; + border-radius: 8px; + transition: + background-color 0.2s ease, + color 0.2s ease; +} +/* Icon adjustments */ +.mw-breadcrumb-link .material-symbols-outlined { + font-size: 18px; +} +/* States: Hover & Focus */ +.mw-breadcrumb-link:hover { + background-color: rgba(103, 80, 164, 0.08); /* State layer */ + color: var(--md-sys-color-primary); +} +.mw-breadcrumb-link:focus-visible { + outline: 2px solid var(--md-sys-color-primary); + outline-offset: 2px; +} +/* Active / Current Page */ +.mw-breadcrumb-item[aria-current='page'] { + color: var(--md-sys-color-primary); + font-weight: 500; + padding: 6px 8px; +} +/* CSS Chevron Separator */ +.mw-breadcrumb-separator { + display: inline-block; + width: 6px; + height: 6px; + margin: 0 8px; + border-top: 2px solid var(--md-sys-color-outline); + border-right: 2px solid var(--md-sys-color-outline); + transform: rotate(45deg); +} diff --git a/src/breadcrumb/breadcrumb.mjs b/src/breadcrumb/breadcrumb.mjs new file mode 100644 index 0000000000..a5ade1f0e1 --- /dev/null +++ b/src/breadcrumb/breadcrumb.mjs @@ -0,0 +1,35 @@ +import { Component } from '../atomic/component.mjs'; + +class Breadcrumb extends Component { + #parts; + + constructor(options) { + super(options); + this.#parts = []; + this.setTagName('nav'); + this.addClassname('breadcrumb-wrapper'); + this.setAttribute('aria-label', 'Breadcrumb'); + } + + setCrumbs(parts) { + this.#parts = parts; + return this; + } + + toHTML() { + const seperatorHtml = ''; + + const itemsHtml = this.#parts + .map( + (p) => + `
  • ${p}
  • ` + ) + .join(seperatorHtml); + + const html = `
      ${itemsHtml}
    `; + this.setChildren(html); + return super.toHTML(); + } +} + +export { Breadcrumb }; diff --git a/src/button/README.md b/src/button/README.md new file mode 100644 index 0000000000..c02aa4a749 --- /dev/null +++ b/src/button/README.md @@ -0,0 +1,12 @@ +# Button + +Basic Action Elements. + +## Usage + +```html + + + + +``` diff --git a/components/button/_buttons.scss b/src/button/___buttons.scss similarity index 100% rename from components/button/_buttons.scss rename to src/button/___buttons.scss diff --git a/components/button/buttons.stories.ts b/src/button/___buttons.stories.mjs similarity index 90% rename from components/button/buttons.stories.ts rename to src/button/___buttons.stories.mjs index 9a2e856e4d..743578888d 100644 --- a/components/button/buttons.stories.ts +++ b/src/button/___buttons.stories.mjs @@ -1,9 +1,3 @@ -import type { Meta, StoryObj } from '@storybook/html-vite'; - -export default { - title: 'Components/Buttons' -} satisfies Meta; - const BTN_SIZES = { Small: ['btn-small'], 'Small disabled': ['btn-small', 'disabled'], @@ -21,7 +15,7 @@ const BTN_STYLES = { Text: ['text'] }; -export const Basic: StoryObj = { +export const Basic = { render(args) { const table = document.createElement('table'); const headerRow = table.insertRow(); @@ -64,7 +58,7 @@ export const Basic: StoryObj = { } }; -export const Elevated: StoryObj = { +export const Elevated = { ...Basic, args: { label: 'Button', @@ -72,7 +66,7 @@ export const Elevated: StoryObj = { } }; -export const FloatingWithText: StoryObj = { +export const FloatingWithText = { ...Basic, args: { label: 'B', @@ -80,7 +74,7 @@ export const FloatingWithText: StoryObj = { } }; -export const FloatingWithIcon: StoryObj = { +export const FloatingWithIcon = { ...Basic, args: { label: '', @@ -89,7 +83,7 @@ export const FloatingWithIcon: StoryObj = { } }; -export const IconLeft: StoryObj = { +export const IconLeft = { ...Basic, args: { label: 'Button', @@ -98,7 +92,7 @@ export const IconLeft: StoryObj = { } }; -export const IconRight: StoryObj = { +export const IconRight = { ...Basic, args: { label: 'Submit', @@ -107,7 +101,7 @@ export const IconRight: StoryObj = { } }; -export const FloatingActionButton: StoryObj = { +export const FloatingActionButton = { render() { return `
    diff --git a/components/button/fabSpec.js b/src/button/___fabSpec.js similarity index 100% rename from components/button/fabSpec.js rename to src/button/___fabSpec.js diff --git a/spec/taptargetSpec.js b/src/button/___taptargetSpec.js similarity index 100% rename from spec/taptargetSpec.js rename to src/button/___taptargetSpec.js diff --git a/src/button/button.css b/src/button/button.css new file mode 100644 index 0000000000..fa0001fa6a --- /dev/null +++ b/src/button/button.css @@ -0,0 +1,182 @@ +:root { + --btn-height: 40px; + --btn-font-size-icon: 16px; + --btn-padding: 24px; + --btn-padding-icon: 16px; + --btn-gap-icon: 8px; + --btn-border-radius: 4px; + --btn-font-size: 14px; +} + +/* ========================================== + Basis-Button (.btn) + ========================================== */ +.btn { + height: var(--btn-height); + border-radius: var(--btn-border-radius); + padding-left: var(--btn-padding); + padding-right: var(--btn-padding); + font-size: var(--btn-font-size); + font-weight: 500; + text-decoration: none; + display: inline-flex; + align-items: center; + border: none; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + white-space: nowrap; + outline: 0; + user-select: none; + transition: background-color 0.2s ease-out; +} +.btn:focus { + background-color: var(--md-sys-color-primary-container); +} +.btn:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; +} + +/* ========================================== + Button Varianten + ========================================== */ + +/* --- Filled --- */ +.btn-filled { + color: var(--md-sys-color-on-primary); + background-color: var(--md-sys-color-primary); +} +.btn-filled:hover, +.btn-filled:focus { + color: var(--md-sys-color-on-primary); +} +.btn-filled:hover { + /* Entspricht @extend .z-depth-1 */ + box-shadow: var(--z-depth-1, 0 2px 5px 0 rgba(0, 0, 0, 0.16)); + background-color: color-mix( + in srgb, + var(--md-sys-color-primary), + var(--md-sys-color-on-primary) 16% + ); +} +.btn-filled:focus { + /* Entspricht @extend .z-depth-0 */ + box-shadow: var(--z-depth-0, none); + background-color: color-mix( + in srgb, + var(--md-sys-color-primary), + var(--md-sys-color-on-primary) 20% + ); +} + +/* --- Tonal --- */ +.btn-tonal, +.btn.tonal { + color: var(--md-sys-color-on-secondary-container); + background-color: var(--md-sys-color-secondary-container); +} +.btn-tonal:hover, +.btn.tonal:hover, +.btn-tonal:focus, +.btn.tonal:focus { + color: var(--md-sys-color-on-secondary-container); +} +.btn-tonal:hover, +.btn.tonal:hover { + box-shadow: var(--z-depth-1, 0 2px 5px 0 rgba(0, 0, 0, 0.16)); + background-color: color-mix( + in srgb, + var(--md-sys-color-secondary-container), + var(--md-sys-color-on-secondary-container) 16% + ); +} +.btn-tonal:focus, +.btn.tonal:focus { + box-shadow: var(--z-depth-0, none); + background-color: color-mix( + in srgb, + var(--md-sys-color-secondary-container), + var(--md-sys-color-on-secondary-container) 20% + ); +} + +/* --- Elevated --- */ +.btn-elevated { + box-shadow: var(--z-depth-1, 0 2px 5px 0 rgba(0, 0, 0, 0.16)); +} +.btn-elevated:hover { + box-shadow: var(--z-depth-2, 0 5px 11px 0 rgba(0, 0, 0, 0.18)); +} +.btn-elevated:focus { + box-shadow: var(--z-depth-1, 0 2px 5px 0 rgba(0, 0, 0, 0.16)); +} + +/* --- Outlined --- */ +.btn-outlined, +.btn.outlined { + background-color: transparent; + color: var(--md-sys-color-primary); + border: 1px solid var(--md-sys-color-outline); +} + +.btn-outlined:hover, +.btn.outlined:hover, +.btn-outlined:focus, +.btn.outlined:focus { + color: var(--md-sys-color-primary); +} + +.btn-outlined:hover, +.btn.outlined:hover { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 16%); +} + +.btn-outlined:focus, +.btn.outlined:focus { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); + border: 1px solid var(--md-sys-color-primary); +} + +.btn-outlined.disabled, +.btn.outlined.disabled, +.btn-outlined:disabled, +.btn.outlined:disabled { + background-color: unset; + border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} + +/* --- Flat / Text --- */ +.btn-flat, +.btn.text { + box-shadow: var(--z-depth-0, none); + color: var(--md-sys-color-primary); + background-color: transparent; +} +.btn-flat:hover, +.btn.text:hover .btn-flat:focus, +.btn.text:focus { + color: var(--md-sys-color-primary); +} +.btn-flat:hover, +.btn.text:hover { + background-color: color-mix(in srgb, var(--md-sys-color-primary) 16%, transparent); +} +.btn-flat:focus, +.btn.text:focus { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); +} +.btn-flat.disabled, +.btn.text.disabled .btn-flat:disabled, +.btn.text:disabled { + background-color: unset; +} + +/* --- Disabled State --- */ +.btn-disabled, +.btn:disabled { + box-shadow: var(--z-depth-0, none); + color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); + background-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 12%); + pointer-events: none; + cursor: default; +} diff --git a/components/button/button.mjs b/src/button/button.mjs similarity index 100% rename from components/button/button.mjs rename to src/button/button.mjs diff --git a/components/button/buttons.ts b/src/button/floatingactionbutton.ts similarity index 99% rename from components/button/buttons.ts rename to src/button/floatingactionbutton.ts index 59437471b4..7ddb559a6d 100644 --- a/components/button/buttons.ts +++ b/src/button/floatingactionbutton.ts @@ -1,5 +1,5 @@ -import { Component, BaseOptions, InitElements, MElement, Openable } from '../../src/component'; -import { Utils } from '../../src/utils'; +import { Component, BaseOptions, InitElements, MElement, Openable } from '../component'; +import { Utils } from '../utils'; interface FloatingActionButtonOptions extends BaseOptions { /** diff --git a/src/card/_cards.scss b/src/card/_cards.scss new file mode 100644 index 0000000000..4b9082bfbf --- /dev/null +++ b/src/card/_cards.scss @@ -0,0 +1,170 @@ +// @use '../../sass/variables' as v; + +.card-panel { + transition: box-shadow 0.25s ease; + padding: 24px; + // margin-block: v.$element-top-margin v.$element-bottom-margin; + border-radius: 12px; + box-shadow: var(--md-sys-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.12)); + background-color: var(--md-sys-color-surface); +} + +.card { + --background-color: color-mix( + in srgb, + var(--md-sys-color-surface), + var(--md-sys-color-surface-tint) 17% + ); + + position: relative; + background-color: var(--background-color); + transition: box-shadow 0.25s ease; + border-radius: 12px; + box-shadow: var(--md-sys-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.12)); + + .card-title { + font-size: 1.5rem; + font-weight: 300; + } + + /* Aspect ratios instead of fixed pixel heights */ + &.small { + aspect-ratio: 4 / 3; + } + &.medium { + aspect-ratio: 1 / 1; + } + &.large { + aspect-ratio: 3 / 4; + } + + &.small, + &.medium, + &.large { + .card-image { + max-height: 60%; + overflow: hidden; + + & + .card-content { + max-height: 40%; + } + } + + .card-content { + max-height: 100%; + overflow: hidden; + } + + .card-action { + position: absolute; + inset-block-end: 0; + inset-inline: 0; + } + } + + /* Horizontal Layout */ + &.horizontal { + display: flex; + + .card-image { + max-width: 50%; + + img { + border-radius: 2px 0 0 2px; + max-width: 100%; + width: auto; + } + } + + .card-stacked { + display: flex; + flex-direction: column; + flex: 1; + position: relative; + + .card-content { + flex-grow: 1; + } + } + } + + /* Card Image */ + .card-image { + position: relative; + + img { + display: block; + border-radius: 2px 2px 0 0; + width: 100%; + } + + .card-title { + color: var(--md-sys-color-surface); + position: absolute; + inset-block-end: 0; + inset-inline-start: 0; + max-width: 100%; + padding: 24px; + } + + .activator { + position: absolute; + inset: 0; + cursor: pointer; + } + } + + /* Card Content */ + .card-content { + padding: 24px; + + p { + margin: 0; + } + + .card-title { + display: block; + line-height: 2rem; + margin-block-end: 8px; + + &.activator { + cursor: pointer; + } + } + } + + /* Card Action Buttons */ + .card-action { + padding-inline: 1.6rem; + + a { + // @include m.btn( + // var(--btn-height), + // var(--btn-border-radius), + // var(--btn-padding), + // var(--btn-padding), + // var(--btn-font-size) + // ); + + &:first-child { + margin-inline-start: -1.6rem; + } + &:last-child { + margin-inline-end: -1.6rem; + } + } + } + + /* Reveal Overlay */ + .card-reveal { + padding: 24px; + position: absolute; + background-color: var(--md-sys-color-surface); + inset-inline: 0; + top: 100%; + block-size: 100%; + z-index: 3; + display: none; + overflow-y: auto; + } +} diff --git a/src/card/card.css b/src/card/card.css new file mode 100644 index 0000000000..0528161197 --- /dev/null +++ b/src/card/card.css @@ -0,0 +1,163 @@ +.card-panel { + transition: box-shadow 0.25s ease; + padding: 24px; + border-radius: 12px; + box-shadow: var(--md-sys-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.12)); + background-color: var(--md-sys-color-surface); +} + +.card { + --background-color: color-mix( + in srgb, + var(--md-sys-color-surface), + var(--md-sys-color-surface-tint) 17% + ); + position: relative; + background-color: var(--background-color); + transition: box-shadow 0.25s ease; + border-radius: 12px; + box-shadow: var(--md-sys-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.12)); +} + +.card .card-title { + font-size: 1.5rem; + font-weight: 300; +} + +/* Aspect ratios instead of fixed pixel heights */ +.card.small { + aspect-ratio: 4 / 3; +} + +.card.medium { + aspect-ratio: 1 / 1; +} + +.card.large { + aspect-ratio: 3 / 4; +} + +.card.small .card-image, +.card.medium .card-image, +.card.large .card-image { + max-height: 60%; + overflow: hidden; +} + +.card.small .card-image + .card-content, +.card.medium .card-image + .card-content, +.card.large .card-image + .card-content { + max-height: 40%; +} + +.card.small .card-content, +.card.medium .card-content, +.card.large .card-content { + max-height: 100%; + overflow: hidden; +} + +.card.small .card-action, +.card.medium .card-action, +.card.large .card-action { + position: absolute; + inset-block-end: 0; + inset-inline: 0; +} + +/* Horizontal Layout */ +.card.horizontal { + display: flex; +} + +.card.horizontal .card-image { + max-width: 50%; +} + +.card.horizontal .card-image img { + border-radius: 2px 0 0 2px; + max-width: 100%; + width: auto; +} + +.card.horizontal .card-stacked { + display: flex; + flex-direction: column; + flex: 1; + position: relative; +} + +.card.horizontal .card-stacked .card-content { + flex-grow: 1; +} + +/* Card Image */ +.card .card-image { + position: relative; +} + +.card .card-image img { + display: block; + border-radius: 2px 2px 0 0; + width: 100%; +} + +.card .card-image .card-title { + color: var(--md-sys-color-surface); + position: absolute; + inset-block-end: 0; + inset-inline-start: 0; + max-width: 100%; + padding: 24px; +} + +.card .card-image .activator { + position: absolute; + inset: 0; + cursor: pointer; +} + +/* Card Content */ +.card .card-content { + padding: 24px; +} + +.card .card-content p { + margin: 0; +} + +.card .card-content .card-title { + display: block; + line-height: 2rem; + margin-block-end: 8px; +} + +.card .card-content .card-title.activator { + cursor: pointer; +} + +/* Card Action Buttons */ +.card .card-action { + padding-inline: 1.6rem; +} + +.card .card-action a:first-child { + margin-inline-start: -1.6rem; +} + +.card .card-action a:last-child { + margin-inline-end: -1.6rem; +} + +/* Reveal Overlay */ +.card .card-reveal { + padding: 24px; + position: absolute; + background-color: var(--md-sys-color-surface); + inset-inline: 0; + top: 100%; + block-size: 100%; + z-index: 3; + display: none; + overflow-y: auto; +} diff --git a/components/card/card.mjs b/src/card/card.mjs similarity index 100% rename from components/card/card.mjs rename to src/card/card.mjs diff --git a/components/card/cardsSpec.js b/src/card/cards.test.ts similarity index 69% rename from components/card/cardsSpec.js rename to src/card/cards.test.ts index e24b0d318e..fc9c877f65 100644 --- a/components/card/cardsSpec.js +++ b/src/card/cards.test.ts @@ -1,3 +1,14 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Cards } from './cards'; + +// Setup Materialize global object +const globalM = { Cards }; +global.M = globalM; +if (typeof window !== 'undefined') { + window.M = globalM; +} + describe('Cards', () => { const fixture = `
    @@ -115,6 +126,7 @@ describe('Cards', () => {
    `; + // Helper to safely fetch bounding dimensions in happy-dom const roundedRect = (el) => { const rect = el.getBoundingClientRect(); return { @@ -127,13 +139,41 @@ describe('Cards', () => { }; }; + // Helper function to simulate element click + const click = (el) => { + el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + }; + + // Helper to check element visibility under happy-dom rules + const isVisible = (el) => { + const style = window.getComputedStyle(el); + return style.display !== 'none' && style.visibility !== 'hidden'; + }; + + // Helper to mock element layout measurements for Happy DOM + const mockRect = (el, rect) => { + vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({ + top: rect.top || 0, + left: rect.left || 0, + width: rect.width || 0, + height: rect.height || 0, + right: (rect.left || 0) + (rect.width || 0), + bottom: (rect.top || 0) + (rect.height || 0), + x: rect.left || 0, + y: rect.top || 0, + toJSON: () => {} + }); + }; + beforeEach(() => { - XloadHtml(fixture); - M.Cards.init(document.querySelectorAll('.card')); + vi.useFakeTimers(); + document.body.innerHTML = fixture; + Cards.init(document.querySelectorAll('.card')); }); afterEach(() => { - XunloadFixtures(); + vi.useRealTimers(); + document.body.innerHTML = ''; }); describe('reveal cards', () => { @@ -146,40 +186,36 @@ describe('Cards', () => { }); it('should have a hidden card-reveal initially', () => { - expect(revealDiv).toBeHidden('reveal div should be hidden initially'); + expect(isVisible(revealDiv)).toBe(false); }); - it('should show card-reveal after clicking an activator', (done) => { + it('should show card-reveal after clicking an activator', () => { const activator = revealCard.querySelector('.activator'); click(activator); + vi.advanceTimersByTime(500); - setTimeout(() => { - expect(revealDiv).toBeVisible('reveal did not appear after activator was clicked.'); - done(); - }, 500); + expect(isVisible(revealDiv)).toBe(true); }); - it('should size and position card-reveal to cover the card when opened', (done) => { + it('should size and position card-reveal to cover the card when opened', () => { const activator = revealCard.querySelector('.activator'); + // Mock bounding box for happy-dom layout checks + mockRect(revealCard, { top: 10, left: 10, width: 300, height: 400 }); + mockRect(revealDiv, { top: 10, left: 10, width: 300, height: 400 }); + click(activator); + vi.advanceTimersByTime(500); - setTimeout(() => { - const revealRect = roundedRect(revealDiv); - const cardRect = roundedRect(revealCard); - - expect(revealDiv).toBeVisible('reveal did not appear after activator was clicked.'); - expect(revealRect.width).toEqual(cardRect.width, 'reveal width should match card width'); - expect(revealRect.height).toEqual( - cardRect.height, - 'reveal height should match card height' - ); - expect(revealRect.top).toEqual(cardRect.top, 'reveal top should align with card top'); - expect(revealRect.left).toEqual(cardRect.left, 'reveal left should align with card left'); - - done(); - }, 500); + const revealRect = roundedRect(revealDiv); + const cardRect = roundedRect(revealCard); + + expect(isVisible(revealDiv)).toBe(true); + expect(revealRect.width).toBe(cardRect.width); + expect(revealRect.height).toBe(cardRect.height); + expect(revealRect.top).toBe(cardRect.top); + expect(revealRect.left).toBe(cardRect.left); }); }); @@ -193,11 +229,14 @@ describe('Cards', () => { }); it('should have an image that fills the full width of the card', () => { + mockRect(imageCard, { top: 0, left: 0, width: 350, height: 450 }); + mockRect(image, { top: 0, left: 0, width: 350, height: 200 }); + const imageRect = roundedRect(image); const cardRect = roundedRect(imageCard); - expect(imageRect.width).toEqual(cardRect.width, 'image does not fill width of card'); - expect(imageRect.top).toEqual(cardRect.top, 'image top should align with card top'); + expect(imageRect.width).toBe(cardRect.width); + expect(imageRect.top).toBe(cardRect.top); }); }); @@ -213,26 +252,30 @@ describe('Cards', () => { const cardContent = card.querySelector('.card-content'); const cardAction = card.querySelector('.card-action'); + // Mock relative dimensions for Happy DOM layout calculation + mockRect(card, { top: 0, height: expectedHeight }); + mockRect(cardImage, { height: maxImageHeight - 10 }); + mockRect(cardContent, { height: maxContentHeight - 10 }); + mockRect(cardAction, { top: expectedHeight - 50, height: 50 }); + const cardRect = roundedRect(card); const imageRect = roundedRect(cardImage); const contentRect = roundedRect(cardContent); const actionRect = roundedRect(cardAction); - expect(cardRect.height).toEqual( - expectedHeight, - `${sizeName} card should be ${expectedHeight}px high` + expect(cardRect.height, `${sizeName} card should be ${expectedHeight}px high`).toBe( + expectedHeight ); - expect(imageRect.height).toBeLessThan( - maxImageHeight + 1, + expect( + imageRect.height, `${sizeName} image should be <= ${maxImageHeight}px high` - ); - expect(contentRect.height).toBeLessThan( - maxContentHeight + 1, + ).toBeLessThan(maxImageHeight + 1); + expect( + contentRect.height, `${sizeName} content should be <= ${maxContentHeight}px high` - ); - expect(actionRect.bottom).toEqual( - cardRect.bottom, - `${sizeName} action should be at bottom of card` + ).toBeLessThan(maxContentHeight + 1); + expect(actionRect.bottom, `${sizeName} action should be at bottom of card`).toBe( + cardRect.bottom ); }; diff --git a/components/card/cards.ts b/src/card/cards.ts similarity index 95% rename from components/card/cards.ts rename to src/card/cards.ts index 3e2a707cbd..ebce1b1b73 100644 --- a/components/card/cards.ts +++ b/src/card/cards.ts @@ -1,5 +1,5 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement, Openable } from '../../src/component'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement, Openable } from '../component'; interface CardsOptions extends BaseOptions { onOpen: (el: Element) => void; @@ -33,6 +33,7 @@ class Cards extends Component implements Openable { this.#cardReveal = this.el.querySelector('.card-reveal'); if (this.#cardReveal) { this.#initialOverflow = getComputedStyle(this.el).overflow; + this.#cardReveal.style.display = 'none'; this.#activators = Array.from(this.el.querySelectorAll('.activator')); this.#activators.forEach((el: HTMLElement) => { if (el) el.tabIndex = 0; @@ -119,8 +120,8 @@ class Cards extends Component implements Openable { }; #removeRevealCloseEventHandlers = () => { - this.#cardRevealClose.addEventListener('click', this.close); - this.#cardRevealClose.addEventListener('keypress', this.#handleKeypressCloseEvent); + this.#cardRevealClose.removeEventListener('click', this.close); + this.#cardRevealClose.removeEventListener('keypress', this.#handleKeypressCloseEvent); }; #handleKeypressCloseEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => { diff --git a/components/carousel/carouselSpec.js b/src/carousel/___carouselSpec.js similarity index 100% rename from components/carousel/carouselSpec.js rename to src/carousel/___carouselSpec.js diff --git a/components/carousel/sliderSpec.js b/src/carousel/___sliderSpec.js similarity index 100% rename from components/carousel/sliderSpec.js rename to src/carousel/___sliderSpec.js diff --git a/components/carousel/_slider.scss b/src/carousel/_carousel.scss similarity index 53% rename from components/carousel/_slider.scss rename to src/carousel/_carousel.scss index 7775171bb0..80dca7d68a 100644 --- a/components/carousel/_slider.scss +++ b/src/carousel/_carousel.scss @@ -1,3 +1,97 @@ +.carousel { + --carousel-height: 400px; + + overflow: hidden; + position: relative; + width: 100%; + height: var(--carousel-height); + perspective: 500px; + transform-style: preserve-3d; + transform-origin: 0% 50%; + + &.carousel-slider { + top: 0; + left: 0; + + .carousel-fixed-item { + position: absolute; + left: 0; + right: 0; + bottom: 20px; + z-index: 1; + + &.with-indicators { + bottom: 68px; + } + } + + .carousel-item { + width: 100%; + height: 100%; + min-height: var(--carousel-height); + position: absolute; + top: 0; + left: 0; + + h2 { + font-size: 24px; + font-weight: 500; + line-height: 32px; + } + + p { + font-size: 15px; + } + } + } + + .carousel-item { + visibility: hidden; + width: calc(var(--carousel-height) * 0.5); + height: calc(var(--carousel-height) * 0.5); + position: absolute; + top: 0; + left: 0; + + & > img { + width: 100%; + } + } + + .indicators { + padding-left: 0; + list-style-type: none; + position: absolute; + text-align: center; + left: 0; + right: 0; + bottom: 0; + margin: 0; + + .indicator-item { + display: inline-block; + position: relative; + cursor: pointer; + height: 8px; + width: 8px; + margin: 24px 4px; + background-color: rgba(255, 255, 255, 0.45); + transition: background-color 0.3s; + border-radius: 50%; + + &.active { + background-color: var(--md-ref-palette-primary100); + } + } + } + + // Materialbox compatibility + &.scrolling .carousel-item .materialboxed, + .carousel-item:not(.active) .materialboxed { + pointer-events: none; + } +} + .slider { position: relative; height: 400px; @@ -32,7 +126,6 @@ background-color: var(--md-ref-palette-primary100); } } - } } @@ -45,7 +138,7 @@ li { padding-left: 0; - list-style-type: none; + list-style-type: none; opacity: 0; position: absolute; top: 0; @@ -107,13 +200,13 @@ height: 100%; cursor: pointer; background-color: var(--md-sys-color-shadow-light); - transition: background-color .3s; + transition: background-color 0.3s; border-radius: 50%; border-width: 0; - + &.active { background-color: var(--md-sys-color-primary); } } } -} \ No newline at end of file +} diff --git a/components/carousel/carousel.ts b/src/carousel/carousel.ts similarity index 62% rename from components/carousel/carousel.ts rename to src/carousel/carousel.ts index 66b309cec2..4711f935dc 100644 --- a/components/carousel/carousel.ts +++ b/src/carousel/carousel.ts @@ -1,7 +1,9 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; -export interface CarouselOptions extends BaseOptions { +// TODO: Combine this into one class + +interface CarouselOptions extends BaseOptions { /** * Transition duration in milliseconds. * @default 200 @@ -49,7 +51,48 @@ export interface CarouselOptions extends BaseOptions { onCycleTo: (current: Element, dragged: boolean) => void; } -const _defaults: CarouselOptions = { +interface SliderOptions extends BaseOptions { + /** + * Set to false to hide slide indicators. + * @default true + */ + indicators: boolean; + /** + * Set height of slider. + * @default 400 + */ + height: number; + /** + * Set the duration of the transition animation in ms. + * @default 500 + */ + duration: number; + /** + * Set the duration between transitions in ms. + * @default 6000 + */ + interval: number; + /** + * If slider should pause when keyboard focus is received. + * @default true + */ + pauseOnFocus: boolean; + /** + * If slider should pause when is hovered by a pointer. + * @default true + */ + pauseOnHover: boolean; + /** + * Optional function used to generate ARIA label to indicators (for accessibility purposes). + * @param index Current index, starting from "1". + * @param current A which indicates whether it is the current element or not + * @returns a string to be used as label indicator. + * @default null + */ + indicatorLabelFunc: (index: number, current: boolean) => string; +} + +const _defaultsCarousel: CarouselOptions = { duration: 200, // ms dist: -100, // zoom scale TODO: make this more intuitive as an option shift: 0, // spacing for center image @@ -61,7 +104,7 @@ const _defaults: CarouselOptions = { onCycleTo: null // Callback for when a new slide is cycled to. }; -export class Carousel extends Component { +class Carousel extends Component { hasMultipleSlides: boolean; showIndicators: boolean; noWrap: boolean; @@ -164,7 +207,7 @@ export class Carousel extends Component { } static get defaults(): CarouselOptions { - return _defaults; + return _defaultsCarousel; } /** @@ -691,3 +734,394 @@ export class Carousel extends Component { this._cycleTo(n, callback); } } + +const _defaults: SliderOptions = { + indicators: true, + height: 400, + duration: 500, + interval: 6000, + pauseOnFocus: true, + pauseOnHover: true, + indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA) +}; + +class Slider extends Component { + /** Index of current slide. */ + activeIndex: number; + interval: string | number | NodeJS.Timeout; + eventPause: boolean; + _slider: HTMLUListElement; + _slides: HTMLLIElement[]; + _activeSlide: HTMLLIElement; + _indicators: HTMLLIElement[]; + _hovered: boolean; + _focused: boolean; + _focusCurrent: boolean; + _sliderId: string; + + constructor(el: HTMLElement, options: Partial) { + super(el, options, Slider); + this.el['M_Slider'] = this; + + this.options = { + ...Slider.defaults, + ...options + }; + + // init props + this.interval = null; + this.eventPause = false; + this._hovered = false; + this._focused = false; + this._focusCurrent = false; + + // setup + this._slider = this.el.querySelector('.slides'); + this._slides = Array.from(this._slider.querySelectorAll('li')); + this.activeIndex = this._slides.findIndex((li) => li.classList.contains('active')); + + if (this.activeIndex !== -1) { + this._activeSlide = this._slides[this.activeIndex]; + } + + this._setSliderHeight(); + + // Sets element id if it does not have one + if (this._slider.hasAttribute('id')) this._sliderId = this._slider.getAttribute('id'); + else { + this._sliderId = 'slider-' + Utils.guid(); + this._slider.setAttribute('id', this._sliderId); + } + + const placeholderBase64 = + 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + // Set initial positions of captions + this._slides.forEach((slide) => { + // Caption + //const caption = slide.querySelector('.caption'); + //if (caption) this._animateCaptionIn(caption, 0); + // Set Images as Background Images + const img = slide.querySelector('img'); + if (img) { + if (img.src !== placeholderBase64) { + img.style.backgroundImage = 'url(' + img.src + ')'; + img.src = placeholderBase64; + } + } + // Sets slide as focusable by code + if (!slide.hasAttribute('tabindex')) slide.setAttribute('tabindex', '-1'); + // Removes initial visibility from "inactive" slides + slide.style.visibility = 'hidden'; + }); + + this._setupIndicators(); + + // Show active slide + if (this._activeSlide) { + this._activeSlide.style.display = 'block'; + this._activeSlide.style.visibility = 'visible'; + } else { + this.activeIndex = 0; + this._slides[0].classList.add('active'); + this._slides[0].style.visibility = 'visible'; + this._activeSlide = this._slides[0]; + this._animateSlide(this._slides[0], true); + // Update indicators + if (this.options.indicators) { + this._indicators[this.activeIndex].children[0].classList.add('active'); + } + } + this._setupEventHandlers(); + // auto scroll + this.start(); + } + + static get defaults() { + return _defaults; + } + + /** + * Initializes instance of Slider. + * @param el HTML element. + * @param options Component options. + */ + static init(el: HTMLElement, options?: Partial): Slider; + /** + * Initializes instances of Slider. + * @param els HTML elements. + * @param options Component options. + */ + static init(els: InitElements, options?: Partial): Slider[]; + /** + * Initializes instances of Slider. + * @param els HTML elements. + * @param options Component options. + */ + static init( + els: HTMLElement | InitElements, + options: Partial = {} + ): Slider | Slider[] { + return super.init(els, options, Slider); + } + + static getInstance(el: HTMLElement): Slider { + return el['M_Slider']; + } + + destroy() { + this.pause(); + this._removeIndicators(); + this._removeEventHandlers(); + this.el['M_Slider'] = undefined; + } + + private _setupEventHandlers() { + if (this.options.pauseOnFocus) { + this.el.addEventListener('focusin', this._handleAutoPauseFocus); + this.el.addEventListener('focusout', this._handleAutoStartFocus); + } + if (this.options.pauseOnHover) { + this.el.addEventListener('mouseenter', this._handleAutoPauseHover); + this.el.addEventListener('mouseleave', this._handleAutoStartHover); + } + if (this.options.indicators) { + this._indicators.forEach((el) => { + el.addEventListener('click', this._handleIndicatorClick); + }); + } + } + + private _removeEventHandlers() { + if (this.options.pauseOnFocus) { + this.el.removeEventListener('focusin', this._handleAutoPauseFocus); + this.el.removeEventListener('focusout', this._handleAutoStartFocus); + } + if (this.options.pauseOnHover) { + this.el.removeEventListener('mouseenter', this._handleAutoPauseHover); + this.el.removeEventListener('mouseleave', this._handleAutoStartHover); + } + if (this.options.indicators) { + this._indicators.forEach((el) => { + el.removeEventListener('click', this._handleIndicatorClick); + }); + } + } + + private _handleIndicatorClick = (e: MouseEvent) => { + const el = (e.target).parentElement; + const currIndex = [...el.parentNode.children].indexOf(el); + this._focusCurrent = true; + this.set(currIndex); + }; + + private _handleAutoPauseHover = () => { + this._hovered = true; + if (this.interval != null) { + this._pause(true); + } + }; + + private _handleAutoPauseFocus = () => { + this._focused = true; + if (this.interval != null) { + this._pause(true); + } + }; + + private _handleAutoStartHover = () => { + this._hovered = false; + if (!(this.options.pauseOnFocus && this._focused) && this.eventPause) { + this.start(); + } + }; + + private _handleAutoStartFocus = () => { + this._focused = false; + if (!(this.options.pauseOnHover && this._hovered) && this.eventPause) { + this.start(); + } + }; + + private _handleInterval = () => { + const activeElem = this._slider.querySelector('.active'); + let newActiveIndex = [...activeElem.parentNode.children].indexOf(activeElem); + if (this._slides.length === newActiveIndex + 1) + newActiveIndex = 0; // loop to start + else newActiveIndex += 1; + this.set(newActiveIndex); + }; + + private _animateSlide(slide: HTMLElement, isDirectionIn: boolean): void { + let dx = 0, + dy = 0; + // from + slide.style.opacity = isDirectionIn ? '0' : '1'; + setTimeout(() => { + slide.style.transition = `opacity ${this.options.duration}ms ease`; + // to + slide.style.opacity = isDirectionIn ? '1' : '0'; + }, 1); + // Caption + const caption: HTMLElement = slide.querySelector('.caption'); + if (!caption) return; + if (caption.classList.contains('center-align')) dy = -100; + else if (caption.classList.contains('right-align')) dx = 100; + else if (caption.classList.contains('left-align')) dx = -100; + // from + caption.style.opacity = isDirectionIn ? '0' : '1'; + caption.style.transform = isDirectionIn ? `translate(${dx}px, ${dy}px)` : `translate(0, 0)`; + setTimeout(() => { + caption.style.transition = `opacity ${this.options.duration}ms ease, transform ${this.options.duration}ms ease`; + // to + caption.style.opacity = isDirectionIn ? '1' : '0'; + caption.style.transform = isDirectionIn ? `translate(0, 0)` : `translate(${dx}px, ${dy}px)`; + }, this.options.duration); // delay + } + + private _setSliderHeight() { + // If fullscreen, do nothing + if (!this.el.classList.contains('fullscreen')) { + if (this.options.indicators) { + // Add height if indicators are present + this.el.style.height = this.options.height + 40 + 'px'; //.css('height', this.options.height + 40 + 'px'); + } else { + this.el.style.height = this.options.height + 'px'; + } + this._slider.style.height = this.options.height + 'px'; + } + } + + private _setupIndicators() { + if (this.options.indicators) { + const ul = document.createElement('ul'); + ul.classList.add('indicators'); + + const arrLi = []; + this._slides.forEach((el, i) => { + const label = this.options.indicatorLabelFunc + ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) + : `${i + 1}`; + const li = document.createElement('li'); + li.classList.add('indicator-item'); + li.innerHTML = ``; + arrLi.push(li); + ul.append(li); + }); + + this.el.append(ul); + this._indicators = arrLi; + } + } + + private _removeIndicators() { + this.el.querySelector('ul.indicators').remove(); //find('ul.indicators').remove(); + } + + set(index: number) { + // Wrap around indices. + if (index >= this._slides.length) index = 0; + else if (index < 0) index = this._slides.length - 1; + + // Only do if index changes + if (this.activeIndex === index) return; + + this._activeSlide = this._slides[this.activeIndex]; + const _caption = this._activeSlide.querySelector('.caption'); + + this._activeSlide.classList.remove('active'); + // Enables every slide + this._slides.forEach((slide) => (slide.style.visibility = 'visible')); + + //--- Hide active Slide + Caption + this._activeSlide.style.opacity = '0'; + setTimeout(() => { + this._slides.forEach((slide) => { + if (slide.classList.contains('active')) return; + slide.style.opacity = '0'; + slide.style.transform = 'translate(0, 0)'; + // Disables invisible slides (for assistive technologies) + slide.style.visibility = 'hidden'; + }); + }, this.options.duration); + + // Hide active Caption + //this._animateCaptionIn(_caption, this.options.duration); + _caption.style.opacity = '0'; + + // Update indicators + if (this.options.indicators) { + const activeIndicator = this._indicators[this.activeIndex].children[0]; + const nextIndicator = this._indicators[index].children[0]; + activeIndicator.classList.remove('active'); + nextIndicator.classList.add('active'); + if (typeof this.options.indicatorLabelFunc === 'function') { + activeIndicator.ariaLabel = this.options.indicatorLabelFunc.call( + this, + this.activeIndex, + false + ); + nextIndicator.ariaLabel = this.options.indicatorLabelFunc.call(this, index, true); + } + } + + //--- Show new Slide + Caption + this._animateSlide(this._slides[index], true); + this._slides[index].classList.add('active'); + this.activeIndex = index; + + // Reset interval, if allowed. This check prevents autostart + // when slider is paused, since it can be changed though indicators. + if (this.interval != null) { + this.start(); + } + } + + _pause(fromEvent: boolean) { + clearInterval(this.interval); + this.eventPause = fromEvent; + this.interval = null; + } + + /** + * Pause slider autoslide. + */ + pause = () => { + this._pause(false); + }; + + /** + * Start slider autoslide. + */ + start = () => { + clearInterval(this.interval); + this.interval = setInterval( + this._handleInterval, + this.options.duration + this.options.interval + ); + this.eventPause = false; + }; + + /** + * Move to next slider. + */ + next = () => { + let newIndex = this.activeIndex + 1; + // Wrap around indices. + if (newIndex >= this._slides.length) newIndex = 0; + else if (newIndex < 0) newIndex = this._slides.length - 1; + this.set(newIndex); + }; + + /** + * Move to prev slider. + */ + prev = () => { + let newIndex = this.activeIndex - 1; + // Wrap around indices. + if (newIndex >= this._slides.length) newIndex = 0; + else if (newIndex < 0) newIndex = this._slides.length - 1; + this.set(newIndex); + }; +} + +export { Slider, SliderOptions, Carousel, CarouselOptions }; diff --git a/src/checkbox/README.md b/src/checkbox/README.md new file mode 100644 index 0000000000..622c465cb3 --- /dev/null +++ b/src/checkbox/README.md @@ -0,0 +1,30 @@ +# Checkbox + +Use checkboxes when looking for yes or no answers. The `for` attribute is necessary to bind our custom checkbox with the input. +Add the input's `id` as the value of the `for` attribute of the label. + +## Usage + +```html + +``` + +OLD: + +## Default + +```html +

    + +

    +``` + +## Filled-in Checkbox + +## Indeterminate Checkbox diff --git a/src/checkbox/checkbox.css b/src/checkbox/checkbox.css new file mode 100644 index 0000000000..f2a049b1af --- /dev/null +++ b/src/checkbox/checkbox.css @@ -0,0 +1,88 @@ +:root { + --checkbox-size: 18px; + --checkbox-shape: 2px; +} + +.checkbox-container { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; +} + +/* Dim label when checkbox is disabled */ +.checkbox-container:has(input:disabled) { + cursor: not-allowed; + opacity: 0.38; +} + +input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + width: var(--checkbox-size); + height: var(--checkbox-size); + margin: 0; + position: relative; + cursor: pointer; + display: inline-grid; + place-content: center; + border: 2px solid var(--md-sys-color-on-surface-variant); + border-radius: var(--checkbox-shape); + background-color: transparent; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +/* Checkmark / Dash Icon Container */ +input[type='checkbox']::before { + content: ''; + width: var(--checkbox-size); + height: var(--checkbox-size); + transform: scale(0); + transition: transform 0.12s ease-in-out; + background-repeat: no-repeat; + background-position: center; + background-size: 70%; +} + +/* Hover States */ +input[type='checkbox']:not(:disabled):hover { + border-color: var(--md-sys-color-primary); +} + +/* Checked State */ +input[type='checkbox']:checked { + background-color: var(--md-sys-color-primary); + border-color: var(--md-sys-color-primary); +} + +input[type='checkbox']:checked::before { + transform: scale(1); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); +} + +/* Indeterminate State (Mixed) */ +input[type='checkbox']:indeterminate { + background-color: var(--md-sys-color-primary); + border-color: var(--md-sys-color-primary); +} + +input[type='checkbox']:indeterminate::before { + transform: scale(1); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='4' stroke-linecap='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); +} + +/* Focus Ring */ +input[type='checkbox']:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; +} + +/* Disabled State */ +input[type='checkbox']:disabled { + cursor: not-allowed; + opacity: 0.38; +} diff --git a/src/checkbox/checkbox.mjs b/src/checkbox/checkbox.mjs new file mode 100644 index 0000000000..9c315c08b0 --- /dev/null +++ b/src/checkbox/checkbox.mjs @@ -0,0 +1,26 @@ +import { Component } from '../atomic/component.mjs'; + +class Checkbox extends Component { + /** + * + * @param {{ isChecked: boolean = false, isDisabled: boolean = false, text?: string }} options + */ + constructor(options) { + super(options); + this.setTagName('label'); + this.addClassname('checkbox-container'); + } + + toHTML() { + const text = typeof this.options === 'object' ? this.options.text || '' : this.options || ''; + const html = ` + + ${text}`; + this.setChildren(html); + return super.toHTML(); + } +} + +export { Checkbox }; diff --git a/components/chip/readme.md b/src/chip/README.md similarity index 81% rename from components/chip/readme.md rename to src/chip/README.md index b3b9874f0a..281f5c7c8e 100644 --- a/components/chip/readme.md +++ b/src/chip/README.md @@ -2,41 +2,22 @@ Chips can be used to represent small blocks of information. They are most commonly used either for contacts or for tags. - -
    - Contact Person Jane Doe -
    -
    - Tag - close -
    -
    - check - Filter - close -
    -
    Information
    -
    - check - Filter - close -
    - - ```html -
    - Contact Person Jane Doe -
    +
    Contact Person Jane Doe
    +
    Tag close
    +
    check Filter close
    +
    Information
    +
    check Filter @@ -44,34 +25,21 @@ Chips can be used to represent small blocks of information. They are most common
    ``` -## Contacts +## For Contacts To create a contact chip just add an img inside. - -
    - Contact Person Jane Doe -
    - - ```html
    - Contact Person + Contact Person Jane Doe
    ``` -## Tags +## For Tags To create a tag chip just add a close icon inside with the class `close`. - -
    - Tag - close -
    - - ```html
    Tag @@ -79,10 +47,10 @@ To create a tag chip just add a close icon inside with the class `close`.
    ``` -## Javascript Plugin +# Chip Set To add tags, just enter your tag text and press enter. You can delete them by clicking on the close icon or by - using your delete button. +using your delete button.

    ATTENTION: Data-Format has changed from version 1.X.X to 2.0.0! Please update option 'data'.

    @@ -119,26 +87,26 @@ Use autocomplete with chips:
    - +
    ``` ## Initialization ```javascript -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { const elems = document.querySelectorAll('.chips'); const instances = M.Chips.init(elems, { // specify options here autocompleteOptions: { data: [ - {id: 12, text: "Apple"}, - {id: 13, text: "Microsoft"}, - {id: 42, text: "Google", image: 'http://placehold.it/250x250'} + { id: 12, text: 'Apple' }, + { id: 13, text: 'Microsoft' }, + { id: 42, text: 'Google', image: 'http://placehold.it/250x250' } ] }, placeholder: 'Enter a tag', - secondaryPlaceholder: '+Tag', + secondaryPlaceholder: '+Tag' }); }); ``` @@ -152,7 +120,7 @@ const chip = { image: '', // optional Image-Url }; ``` - + ## Options Note: This could be generated from the JSDocs @@ -245,26 +213,28 @@ Use these methods to interact with chips. All the methods are called on the plug const instance = M.Chips.getInstance(elem); ``` - ### .addChip(); + Add chip to input. #### Arguments + Chip: Chip data object. ```javascript instance.addChip({ id: 1337, text: 'John Doe', // optional - image: '', // optional + image: '' // optional }); ``` - ### .deleteChip(); + Delete nth chip. #### Arguments + Integer: Index of chip. ```javascript @@ -272,9 +242,11 @@ instance.deleteChip(3); // Delete 3rd chip. ``` ### .selectChip(); + Select nth chip. #### Arguments + Integer: Index of chip. ```javascript diff --git a/src/chip/chip-set.mjs b/src/chip/chip-set.mjs new file mode 100644 index 0000000000..514ba03bf7 --- /dev/null +++ b/src/chip/chip-set.mjs @@ -0,0 +1,340 @@ +import { Utils } from '../utils'; +import { Autocomplete } from '../autocomplete/autocomplete'; +import { Component } from '../component'; + +const _defaults = { + data: [], + placeholder: '', + secondaryPlaceholder: '', + closeIconClass: 'material-icons', + autocompleteOptions: {}, + autocompleteOnly: false, + limit: Infinity, + allowUserInput: false, + onChipAdd: null, + onChipSelect: null, + onChipDelete: null +}; + +function gGetIndex(el) { + return [...el.parentNode.children].indexOf(el); +} + +class ChipSet extends Component { + chipsData; + hasAutocomplete; + autocomplete; + #input; + #label; + #chips; + static #keydown = false; + #selectedChip; + + constructor(el, options) { + super(el, options, ChipSet); + this.el['M_Chips'] = this; + this.options = { + ...ChipSet.defaults, + ...options + }; + this.el.classList.add('chips'); + this.chipsData = []; + this.#chips = []; + + // Render initial chips + if (this.options.data.length) { + this.chipsData = this.options.data; + this.#renderChips(); + } + + // Render input element, setup event handlers + if (this.options.allowUserInput) { + this.#setupLabel(); + this.el.classList.add('input-field'); + this.#setupInput(); + this.#setupEventHandlers(); + // move input to end + this.el.append(this.#input); + } + } + + static get defaults() { + return _defaults; + } + + /** + * Initializes instance(s) of Chips. + * @param {HTMLElement|Array} els HTML element(s). + * @param {Object} options Component options. + */ + static init(els, options = {}) { + return super.init(els, options, ChipSet); + } + + static getInstance(el) { + return el['M_Chips']; + } + + getData() { + return this.chipsData; + } + + destroy() { + if (this.options.allowUserInput) this.#removeEventHandlers(); + this.#chips.forEach((c) => c.remove()); + this.#chips = []; + this.el['M_Chips'] = undefined; + } + + #setupEventHandlers() { + this.el.addEventListener('click', this.#handleChipClick); + document.addEventListener('keydown', ChipSet.#handleChipsKeydown); + document.addEventListener('keyup', ChipSet.#handleChipsKeyup); + this.el.addEventListener('blur', ChipSet.#handleChipsBlur, true); + this.#input.addEventListener('focus', this.#handleInputFocus); + this.#input.addEventListener('blur', this.#handleInputBlur); + this.#input.addEventListener('keydown', this.#handleInputKeydown); + } + + #removeEventHandlers() { + this.el.removeEventListener('click', this.#handleChipClick); + document.removeEventListener('keydown', ChipSet.#handleChipsKeydown); + document.removeEventListener('keyup', ChipSet.#handleChipsKeyup); + this.el.removeEventListener('blur', ChipSet.#handleChipsBlur, true); + this.#input.removeEventListener('focus', this.#handleInputFocus); + this.#input.removeEventListener('blur', this.#handleInputBlur); + this.#input.removeEventListener('keydown', this.#handleInputKeydown); + } + + #handleChipClick = (e) => { + const _chip = e.target.closest('.chip'); + const clickedClose = e.target.classList.contains('close'); + if (_chip) { + const index = [..._chip.parentNode.children].indexOf(_chip); + if (clickedClose) { + this.deleteChip(index); + this.#input.focus(); + } else { + this.selectChip(index); + } + } else { + this.#input.focus(); + } + }; + + static #handleChipsKeydown(e) { + ChipSet.#keydown = true; + const chips = e.target.closest('.chips'); + const chipsKeydown = e.target && chips; + + // Don't handle keydown inputs on input and textarea + const tag = e.target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || !chipsKeydown) return; + + const currChips = chips['M_Chips']; + + if (Utils.keys.BACKSPACE.includes(e.key) || Utils.keys.DELETE.includes(e.key)) { + e.preventDefault(); + let selectIndex = currChips.chipsData.length; + if (currChips.#selectedChip) { + const index = gGetIndex(currChips.#selectedChip); + currChips.deleteChip(index); + currChips.#selectedChip = null; + selectIndex = Math.max(index - 1, 0); + } + if (currChips.chipsData.length) currChips.selectChip(selectIndex); + else currChips.#input.focus(); + } else if (Utils.keys.ARROW_LEFT.includes(e.key)) { + if (currChips.#selectedChip) { + const selectIndex = gGetIndex(currChips.#selectedChip) - 1; + if (selectIndex < 0) return; + currChips.selectChip(selectIndex); + } + } else if (Utils.keys.ARROW_RIGHT.includes(e.key)) { + if (currChips.#selectedChip) { + const selectIndex = gGetIndex(currChips.#selectedChip) + 1; + if (selectIndex >= currChips.chipsData.length) currChips.#input.focus(); + else currChips.selectChip(selectIndex); + } + } + } + + static #handleChipsKeyup() { + ChipSet.#keydown = false; + } + + static #handleChipsBlur(e) { + if (!ChipSet.#keydown && document.hidden) { + const chips = e.target.closest('.chips'); + const currChips = chips['M_Chips']; + currChips.#selectedChip = null; + } + } + + #handleInputFocus = () => { + this.el.classList.add('focus'); + }; + + #handleInputBlur = () => { + this.el.classList.remove('focus'); + }; + + #handleInputKeydown = (e) => { + ChipSet.#keydown = true; + if (Utils.keys.ENTER.includes(e.key)) { + if (this.hasAutocomplete && this.autocomplete && this.autocomplete.isOpen) { + return; + } + e.preventDefault(); + if (!this.hasAutocomplete || (this.hasAutocomplete && !this.options.autocompleteOnly)) { + this.addChip({ id: this.#input.value }); + } + this.#input.value = ''; + } else if ( + (Utils.keys.BACKSPACE.includes(e.key) || Utils.keys.ARROW_LEFT.includes(e.key)) && + this.#input.value === '' && + this.chipsData.length + ) { + e.preventDefault(); + this.selectChip(this.chipsData.length - 1); + } + }; + + #renderChip(chip) { + if (!chip.id) return; + const renderedChip = document.createElement('li'); + renderedChip.classList.add('chip'); + renderedChip.innerText = chip.text || chip.id; + if (chip.image) { + const img = document.createElement('img'); + img.setAttribute('src', chip.image); + renderedChip.insertBefore(img, renderedChip.firstChild); + } + if (this.options.allowUserInput) { + const closeButton = document.createElement('button'); + closeButton.classList.add(this.options.closeIconClass, 'close'); + closeButton.innerText = 'close'; + renderedChip.appendChild(closeButton); + } + return renderedChip; + } + + #renderChips() { + this.#chips = []; + for (let i = 0; i < this.chipsData.length; i++) { + const chipElem = this.#renderChip(this.chipsData[i]); + this.el.appendChild(chipElem); + this.#chips.push(chipElem); + } + } + + #setupAutocomplete() { + this.options.autocompleteOptions.onAutocomplete = (items) => { + if (items.length > 0) + this.addChip({ + id: items[0].id, + text: items[0].text, + image: items[0].image + }); + this.#input.value = ''; + this.#input.focus(); + }; + this.autocomplete = Autocomplete.init(this.#input, this.options.autocompleteOptions); + } + + #setupInput() { + this.#input = this.el.querySelector('input'); + if (!this.#input) { + this.#input = document.createElement('input'); + this.el.append(this.#input); + } + this.#input.classList.add('input'); + this.hasAutocomplete = Object.keys(this.options.autocompleteOptions).length > 0; + if (this.hasAutocomplete) this.#setupAutocomplete(); + this.#setPlaceholder(); + if (!this.#input.getAttribute('id')) this.#input.setAttribute('id', Utils.guid()); + } + + #setupLabel() { + this.#label = this.el.querySelector('label'); + if (this.#label) this.#label.setAttribute('for', this.#input.getAttribute('id')); + } + + #setPlaceholder() { + if (this.chipsData !== undefined && !this.chipsData.length && this.options.placeholder) { + this.#input.placeholder = this.options.placeholder; + } else if ( + (this.chipsData === undefined || !!this.chipsData.length) && + this.options.secondaryPlaceholder + ) { + this.#input.placeholder = this.options.secondaryPlaceholder; + } + } + + #isValidAndNotExist(chip) { + const isValid = !!chip.id; + const doesNotExist = !this.chipsData.some((item) => item.id == chip.id); + return isValid && doesNotExist; + } + + /** + * Add chip to input. + * @param {Object} chip Chip data object + */ + addChip(chip) { + if (!this.#isValidAndNotExist(chip) || this.chipsData.length >= this.options.limit) return; + const renderedChip = this.#renderChip(chip); + this.#chips.push(renderedChip); + this.chipsData.push(chip); + this.#input.before(renderedChip); + this.#setPlaceholder(); + if (typeof this.options.onChipAdd === 'function') { + this.options.onChipAdd(this.el, renderedChip); + } + } + + /** + * Delete nth chip. + * @param {number} chipIndex Index of chip + */ + deleteChip(chipIndex) { + const chip = this.#chips[chipIndex]; + this.#chips[chipIndex].remove(); + this.#chips.splice(chipIndex, 1); + this.chipsData.splice(chipIndex, 1); + this.#setPlaceholder(); + if (typeof this.options.onChipDelete === 'function') { + this.options.onChipDelete(this.el, chip); + } + } + + /** + * Select nth chip. + * @param {number} chipIndex Index of chip + */ + selectChip(chipIndex) { + const chip = this.#chips[chipIndex]; + this.#selectedChip = chip; + chip.focus(); + if (typeof this.options.onChipSelect === 'function') { + this.options.onChipSelect(this.el, chip); + } + } + + static Init() { + if (typeof document !== 'undefined') + document.addEventListener('DOMContentLoaded', () => { + const chips = document.querySelectorAll('.chips'); + chips.forEach((el) => { + el.addEventListener('click', (e) => { + if (e.target.classList.contains('close')) { + const chip = e.target.closest('.chip'); + if (chip) chip.remove(); + } + }); + }); + }); + } +} + +export { ChipSet }; diff --git a/components/chip/chips.css b/src/chip/chip.css similarity index 74% rename from components/chip/chips.css rename to src/chip/chip.css index a35d635e0f..ce096b08aa 100644 --- a/components/chip/chips.css +++ b/src/chip/chip.css @@ -1,24 +1,37 @@ -.chip { - --font-size: 14px; - --font-size-icon: 18px; - --padding: 8px; +:root { + --mw-chip-height: 32px; + --mw-chip-font-size: 14px; + --mw-chip-font-size-icon: 18px; + --mw-chip-padding: 8px; +} +.chip { color: var(--md-sys-color-on-surface-variant); - background-color: rgba(0, 0, 0, 0.09); + background-color: var(--md-sys-color-surface); display: inline-flex; white-space: nowrap; gap: 8px; margin: 0; - height: 32px; - padding-left: var(--padding); - padding-right: var(--padding); - font-size: var(--font-size); + height: var(--mw-chip-height); + padding-left: var(--mw-chip-padding); + padding-right: var(--mw-chip-padding); + font-size: var(--mw-chip-font-size); font-weight: 500; border-radius: 8px; align-items: center; user-select: none; vertical-align: top; } + +.chip:hover { + background-color: color-mix( + in srgb, + var(--md-sys-color-surface), + var(--md-sys-color-surface-tint) 16% + ); + color: var(--md-sys-color-on-surface); +} + .chip:focus { outline: none; background-color: var(--md-sys-color-primary); @@ -41,7 +54,7 @@ } .chip > .material-icons { - font-size: var(--font-size-icon); + font-size: var(--mw-chip-font-size-icon); color: var(--md-sys-color-on-surface-variant); } @@ -124,3 +137,9 @@ font-size: 0.8rem; transform: translateY(-140%); } + +/*------------------------------------------ Assist Chip */ +.assist-chip { + background-color: var(--md-sys-color-surface); + outline: 1px solid var(--md-sys-color-outline); +} diff --git a/components/chip/chip.mjs b/src/chip/chip.mjs similarity index 56% rename from components/chip/chip.mjs rename to src/chip/chip.mjs index 1c63e8dece..51c4869ed8 100644 --- a/components/chip/chip.mjs +++ b/src/chip/chip.mjs @@ -8,6 +8,16 @@ class Chip extends Component { constructor(options) { super(options); this.addClassname('chip'); + this.setAttribute('tabindex', '0'); + if (typeof options === 'string') { + this.#text = options; + } else { + if (options.name) this.#text = options.name; + if (options.href) { + this.setTagName('a'); + this.setAttribute('href', options.href); + } + } } setId(id) { @@ -34,4 +44,11 @@ class Chip extends Component { } } -export { Chip }; +class AssistChip extends Chip { + constructor(options) { + super(options); + this.addClassname('assist-chip'); + } +} + +export { Chip, AssistChip }; diff --git a/src/chip/chip.test.ts b/src/chip/chip.test.ts new file mode 100644 index 0000000000..a5eed9fc79 --- /dev/null +++ b/src/chip/chip.test.ts @@ -0,0 +1,229 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { Chips } from './chips.js'; +import { Dropdown } from '../dropdown/dropdown.ts'; // Adjust path as needed + +declare global { + var M: { + Chips: typeof Chips; + Dropdown?: typeof Dropdown; + }; +} + +const globalM = { Chips, Dropdown }; +globalThis.M = globalM; + +if (typeof window !== 'undefined') { + (window as unknown as { M: typeof globalM }).M = globalM; + (window as unknown as { Dropdown: typeof Dropdown }).Dropdown = Dropdown; +} + +// Utility helper to replace done() timeouts with async/await +const delay = (ms: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +// Event helpers to dispatch native DOM events compatible with Happy DOM +const keydown = (element: Element, keyCode: number): void => { + element.dispatchEvent( + new KeyboardEvent('keydown', { + keyCode, + which: keyCode, + key: keyCode === 13 ? 'Enter' : '', + code: keyCode === 13 ? 'Enter' : '', + bubbles: true, + cancelable: true + }) + ); +}; + +const click = (element: Element): void => { + element.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true + }) + ); +}; + +describe('Chips', () => { + const fixture = `
    +
    +
    +
    +
    +
    `; + + beforeEach(() => { + // Inject fixture HTML directly into the DOM + document.body.innerHTML = fixture; + + // Helper to query element safely + const select = (selector: string): HTMLElement => { + const el = document.querySelector(selector); + if (!el) throw new Error(`Element not found: ${selector}`); + return el; + }; + + // Initialize Components + globalThis.M.Chips.init(select('.chips')); + globalThis.M.Chips.init(select('.chips-initial'), { + data: [ + { id: 12, text: 'Apple' }, + { id: 13, text: 'Microsoft' }, + { + id: 42, + text: 'Google', + image: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' + } + ] + }); + globalThis.M.Chips.init(select('.chips.input-field'), { + allowUserInput: true + }); + globalThis.M.Chips.init(select('.chips-initial.input-field'), { + allowUserInput: true, + data: [ + { id: 12, text: 'Apple' }, + { id: 13, text: 'Microsoft' }, + { + id: 42, + text: 'Google', + image: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' + } + ] + }); + globalThis.M.Chips.init(select('.chips-placeholder.input-field'), { + allowUserInput: true, + placeholder: 'Enter a tag', + secondaryPlaceholder: '+Tag' + }); + globalThis.M.Chips.init(select('.chips-autocomplete.input-field'), { + allowUserInput: true, + autocompleteOptions: { + data: [ + { id: 12, text: 'Apple' }, + { id: 13, text: 'Microsoft' }, + { id: 42, text: 'Google' } + ] + } + }); + }); + + afterEach(() => { + // Clean up DOM after each test + document.body.innerHTML = ''; + }); + + describe('chips', () => { + it('should work with multiple initializations', () => { + const chips = document.querySelector('.chips') as HTMLElement; + globalThis.M.Chips.init(chips); + globalThis.M.Chips.init(chips); + globalThis.M.Chips.init(chips); + + const chipsUserInput = document.querySelector('.chips.input-field') as HTMLElement; + globalThis.M.Chips.init(chips, { allowUserInput: true }); + + const inputs = chipsUserInput?.querySelectorAll('input'); + expect(inputs?.length, 'Should dynamically generate chips structure.').toBe(1); + }); + + it('should be able to add chip', async () => { + const chips = document.querySelector('.chips.input-field'); + const inputEl = chips?.querySelector('input') as HTMLInputElement; + inputEl.value = 'one'; + keydown(inputEl, 13); + + await delay(100); + + const numChips = chips?.querySelectorAll('.chip').length; + const oneChip = chips?.querySelector('.chip') as HTMLElement; + expect(numChips, 'one chip should have been added').toBe(1); + + for (let i = oneChip.children.length - 1; i >= 0; i--) { + oneChip.children[i].remove(); + } + expect(oneChip.innerText, 'the chip should have value "one"').toBe('one'); + }); + + it('should be able to delete chip', async () => { + const chips = document.querySelector('.chips.chips-initial.input-field'); + let numChips = chips?.querySelectorAll('.chip').length; + expect(numChips, '3 initial chips should have been added').toBe(3); + + const chipCloseButton = chips?.querySelectorAll('.chip .close'); + expect(chipCloseButton?.length, 'expected all chips to have close button').toBe(3); + + if (chipCloseButton && chipCloseButton[0]) { + click(chipCloseButton[0]); + } + await delay(100); + + numChips = chips?.querySelectorAll('.chip').length; + expect(numChips, 'one chip should have been deleted').toBe(2); + }); + + it('should have working callbacks', async () => { + const chips = document.querySelector('.chips.input-field')!; + let chipWasAdded = false; + let chipAddedElem: HTMLElement | null = null; + let chipSelect = false; + let chipSelected: HTMLElement | null = null; + let chipDelete = false; + let chipDeleted: HTMLElement | null = null; + + globalThis.M.Chips.init(chips, { + allowUserInput: true, + data: [{ id: 'One' }, { id: 'Two' }, { id: 'Three' }], + onChipAdd: (_chipsEl: HTMLElement, chipEl: HTMLElement) => { + chipAddedElem = chipEl; + chipWasAdded = true; + }, + onChipSelect: (_chipsEl: HTMLElement, chipEl: HTMLElement) => { + chipSelected = chipEl; + chipSelect = true; + }, + onChipDelete: (_chipsEl: HTMLElement, chipEl: HTMLElement) => { + chipDeleted = chipEl; + chipDelete = true; + } + }); + + const inputEl = chips.querySelector('input') as HTMLInputElement; + inputEl.value = 'Four'; + + keydown(inputEl, 13); + await delay(100); + + const valueAdd = chipAddedElem ? (chipAddedElem as HTMLElement).firstChild?.nodeValue : ''; + expect(chipWasAdded, 'add callback fired').toBe(true); + expect(valueAdd, 'add callback provides correct chip element').toBe('Four'); + + const chipList = chips.querySelectorAll('.chip'); + if (chipList[1]) { + click(chipList[1]); + } + + await delay(100); + + const valueSel = chipSelected ? (chipSelected as HTMLElement).firstChild?.nodeValue : ''; + expect(chipSelect, 'select callback fired').toBe(true); + expect(valueSel, 'select callback provides correct chip element').toBe('Two'); + + const closeList = chips.querySelectorAll('.close'); + if (closeList[2]) { + click(closeList[2]); + } + + await delay(100); + + const valueDel = chipDeleted ? (chipDeleted as HTMLElement).firstChild?.nodeValue : ''; + expect(chipDelete, 'delete callback fired').toBe(true); + expect(valueDel, 'delete callback provides correct chip element').toBe('Three'); + }); + }); +}); diff --git a/components/chip/chips.ts b/src/chip/chips.ts similarity index 98% rename from components/chip/chips.ts rename to src/chip/chips.ts index 94697dd176..b81dc31fdd 100644 --- a/components/chip/chips.ts +++ b/src/chip/chips.ts @@ -1,6 +1,6 @@ -import { Utils } from '../../src/utils'; -import { Autocomplete, AutocompleteOptions } from '../search/autocomplete'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Utils } from '../utils'; +import { Autocomplete, AutocompleteOptions } from '../autocomplete/autocomplete'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; interface ChipData { /** Unique identifier. */ diff --git a/src/collapsible/README.md b/src/collapsible/README.md new file mode 100644 index 0000000000..4deb8c8b24 --- /dev/null +++ b/src/collapsible/README.md @@ -0,0 +1,5 @@ +# Collapsible + +or Accordion, ExpansibleTile, Expansion Panel + +not defined in the M3 Spec. diff --git a/components/collapsible/_collapsible.scss b/src/collapsible/_collapsible.scss similarity index 100% rename from components/collapsible/_collapsible.scss rename to src/collapsible/_collapsible.scss diff --git a/src/collapsible/collapsible.test.ts b/src/collapsible/collapsible.test.ts new file mode 100644 index 0000000000..42e683f123 --- /dev/null +++ b/src/collapsible/collapsible.test.ts @@ -0,0 +1,234 @@ +// @vitest-environment happy-dom +import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; +import { Collapsible } from './collapsible'; + +// Helper function to simulate clicking an element +function click(element: HTMLElement): void { + element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); +} + +// Helper to handle timeouts cleanly with async/await +const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('Collapsible Plugin:', () => { + let collapsible: NodeListOf; + let accordion: HTMLElement; + let popout: HTMLElement; + let expandable: HTMLElement; + let expandablePreselect: HTMLElement; + + const fixture = ` + + +
      +
    • +
      filter_dramaFirst
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      placeSecond
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      whatshotThird
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    + +
      +
    • +
      filter_dramaFirst
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      placeSecond
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      whatshotThird
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    + +
      +
    • +
      filter_dramaFirst
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      placeSecond
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    • +
      whatshotThird
      +

      Lorem ipsum dolor sit amet.

      +
    • +
    `; + + beforeEach(() => { + document.body.innerHTML = fixture; + + collapsible = document.querySelectorAll('.collapsible'); + expandable = document.querySelector('.expandable')!; + expandablePreselect = document.querySelector('.expandable-preselected')!; + accordion = document.querySelector('.accordion')!; + popout = document.querySelector('.popout')!; + + // Mock scrollHeight for virtual DOM environment + document.querySelectorAll('.collapsible-body').forEach((body) => { + Object.defineProperty(body, 'scrollHeight', { + configurable: true, + value: 100 + }); + }); + + Collapsible.init(Array.from(collapsible), { inDuration: 0, outDuration: 0 }); + Collapsible.init(expandable, { accordion: false, inDuration: 0, outDuration: 0 }); + Collapsible.init(expandablePreselect, { accordion: false, inDuration: 0, outDuration: 0 }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + describe('collapsible', () => { + it('should open all items, keeping all open', async () => { + const headers = expandable.querySelectorAll('.collapsible-header'); + const bodies = expandable.querySelectorAll('.collapsible-body'); + + // Collapsible body height should be 0 on start when hidden. + bodies.forEach((body) => { + expect(body.style.maxHeight).toBe(''); + }); + + // Open all items + headers.forEach((header) => click(header)); + + await wait(10); + + // Collapsible body height should be > 0 after being opened. + bodies.forEach((body) => { + expect(body.style.maxHeight).toBe('100px'); + }); + }); + + it('should allow preopened sections', () => { + const bodies = expandablePreselect.querySelectorAll('.collapsible-body'); + + bodies.forEach((body, i) => { + const headerLi = body.parentNode as HTMLElement; + + if (i === 1) { + expect(headerLi.classList.contains('active')).toBe(true); + expect(body.style.maxHeight).toBe('100px'); + } else { + expect(body.style.maxHeight).not.toBe('100px'); + } + }); + }); + + it('should open and close programmatically with callbacks', async () => { + const openCallback = vi.fn(); + const closeCallback = vi.fn(); + + Collapsible.init(expandable, { + accordion: false, + onOpenStart: openCallback, + onCloseStart: closeCallback, + inDuration: 0, + outDuration: 0 + }); + + const bodies = expandable.querySelectorAll('.collapsible-body'); + + expect(openCallback).not.toHaveBeenCalled(); + expect(closeCallback).not.toHaveBeenCalled(); + + bodies.forEach((body, i) => { + const instance = Collapsible.getInstance(body.closest('.collapsible') as HTMLElement); + instance.open(i); + }); + + expect(openCallback).toHaveBeenCalled(); + + await wait(10); + + bodies.forEach((body, i) => { + expect(body.style.maxHeight).toBe('100px'); + + const instance = Collapsible.getInstance(body.closest('.collapsible') as HTMLElement); + instance.close(i); + }); + + expect(closeCallback).toHaveBeenCalled(); + + await wait(10); + + bodies.forEach((body) => { + expect(body.style.maxHeight).toBe('0'); + }); + }); + }); + + describe('accordion', () => { + it('should open first and second items, keeping only second open', async () => { + const headers = accordion.querySelectorAll('.collapsible-header'); + const bodies = accordion.querySelectorAll('.collapsible-body'); + + expect(bodies[0].style.maxHeight).not.toBe('100px'); + expect(bodies[1].style.maxHeight).not.toBe('100px'); + + click(headers[0]); + + await wait(10); + + expect(bodies[0].style.maxHeight).toBe('100px'); + + click(headers[1]); + + await wait(10); + + expect(bodies[0].style.maxHeight).toBe('0'); + expect(bodies[1].style.maxHeight).toBe('100px'); + }); + }); + + describe('popout', () => { + it('should open first and popout', async () => { + const listItems = popout.querySelectorAll('li'); + const firstHeader = popout.querySelector('.collapsible-header')!; + const firstBody = popout.querySelector('.collapsible-body')!; + + expect(firstBody.style.maxHeight).not.toBe('100px'); + + // Closed popout items should have horizontal margins + listItems.forEach((li) => { + const style = getComputedStyle(li); + const marginLeft = parseInt(style.marginLeft || '0', 10); + const marginRight = parseInt(style.marginRight || '0', 10); + + expect(marginLeft).toBeGreaterThan(0); + expect(marginRight).toBeGreaterThan(0); + }); + + click(firstHeader); + + await wait(10); + + expect(firstBody.style.maxHeight).toBe('100px'); + }); + }); +}); diff --git a/components/collapsible/collapsible.ts b/src/collapsible/collapsible.ts similarity index 96% rename from components/collapsible/collapsible.ts rename to src/collapsible/collapsible.ts index 7d9226ba7b..c3afb3ad7b 100644 --- a/components/collapsible/collapsible.ts +++ b/src/collapsible/collapsible.ts @@ -1,7 +1,7 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; -export interface CollapsibleOptions extends BaseOptions { +interface CollapsibleOptions extends BaseOptions { /** * If accordion versus collapsible. * @default true @@ -49,7 +49,7 @@ const _defaults: CollapsibleOptions = { outDuration: 300 }; -export class Collapsible extends Component { +class Collapsible extends Component { private _headers: HTMLElement[]; constructor(el: HTMLElement, options: Partial) { @@ -228,3 +228,5 @@ export class Collapsible extends Component { } }; } + +export { CollapsibleOptions, Collapsible }; diff --git a/src/datepicker/README.md b/src/datepicker/README.md new file mode 100644 index 0000000000..2e1510cc40 --- /dev/null +++ b/src/datepicker/README.md @@ -0,0 +1,7 @@ +# Datepicker + +This is a component which is normally used on the client side. But it should be renderable from server-side too. + +## Mini Calendar + +There should be a seperate mini-calendar component, which can be wrapped by a datepicker component. This also allows for ranges, etc. diff --git a/components/datepicker/_datepicker.scss b/src/datepicker/___datepicker.scss similarity index 79% rename from components/datepicker/_datepicker.scss rename to src/datepicker/___datepicker.scss index 31eb678153..a8d6de1188 100644 --- a/components/datepicker/_datepicker.scss +++ b/src/datepicker/___datepicker.scss @@ -63,35 +63,27 @@ .month-next { display: inline-flex; align-items: center; + height: 49px; + margin-top: 4px; + cursor: pointer; + background-color: transparent; + border: none; @include btn($height: 49px); &:focus { background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); } - } - .month-prev > svg, - .month-next > svg { - fill: var(--md-sys-color-on-surface-variant); + > svg { + fill: var(--md-sys-color-on-surface-variant); + } } } -.month-prev, .month-next { - height: 49px; - margin-top: 4px; - cursor: pointer; - background-color: transparent; - border: none; -} - - /* Date Display */ .datepicker-date-display { flex: 1 auto; - // @removed since v2.2.1-dev regarding Material M3 standards - // background-color: var(--md-sys-color-primary); - // color: var(--md-sys-color-on-primary); padding: 20px 22px; border-bottom: 1px solid var(--md-sys-color-surface-variant-light); font-weight: 500; @@ -117,8 +109,7 @@ } } - -/* Calendar */ +/* Calendar Grid */ .datepicker-calendar-container { flex: 2.5 auto; } @@ -149,34 +140,20 @@ .datepicker-day { padding: 0; color: var(--md-sys-color-on-background); - // border-radius: 50%; &.is-today { color: var(--md-sys-color-primary); } - - /*&.is-selected button { - background-color: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - }*/ } } -// @todo -.datepicker-day.has-event {} -// @todo -.datepicker-day.is-inrange {} -// @todo -.datepicker-day.is-startrange {} -// @todo -.datepicker-day.is-endrange {} - +/* Date Range Styling */ .datepicker-day.is-daterange-start, .datepicker-day.is-daterange-end, .datepicker-day.is-daterange { position: relative; - &:before { + &::before { position: absolute; top: 5px; width: 100%; @@ -187,19 +164,20 @@ } } -.datepicker-day.is-daterange-start:before, -.datepicker-day.is-daterange-end:before { +.datepicker-day.is-daterange-start::before, +.datepicker-day.is-daterange-end::before { width: 50%; } -.datepicker-day.is-daterange-start:before { +.datepicker-day.is-daterange-start::before { left: 50%; } -.datepicker-day.is-daterange .datepicker-day-button:before { +.datepicker-day.is-daterange .datepicker-day-button::before { background-color: var(--md-sys-color-primary-container); } +/* Day Button */ .datepicker-day-button { background-color: transparent; border: none; @@ -232,9 +210,10 @@ } } - &.is-outside-current-month button, - &.is-disabled button { + &.is-outside-current-month, + &.is-disabled { color: var(--md-sys-color-on-surface); + opacity: 0.38; pointer-events: none; } } @@ -260,7 +239,7 @@ color: var(--md-sys-color-error); } -/* Display modes */ +/* Modal Variant */ .datepicker-modal { max-width: calc(325px + var(--modal-padding) * 2); max-height: none; diff --git a/components/datepicker/datepickerSpec.js b/src/datepicker/___datepickerSpec.js similarity index 100% rename from components/datepicker/datepickerSpec.js rename to src/datepicker/___datepickerSpec.js diff --git a/src/datepicker/datepicker.css b/src/datepicker/datepicker.css new file mode 100644 index 0000000000..3c7e2dab30 --- /dev/null +++ b/src/datepicker/datepicker.css @@ -0,0 +1,241 @@ +.datepicker-container { + display: flex; + flex-direction: column; + max-width: 325px; + padding: 0; + background-color: var(--md-sys-color-inverse-on-surface); +} + +.datepicker-controls { + display: flex; + justify-content: space-between; + width: 280px; + margin: 0 auto; + + & .selects-container { + display: flex; + } + + & .select-wrapper { + & input { + height: 49px; + margin: 4px 0 0 0; + border-bottom: none; + border-radius: 4px; + text-align: center; + + &:focus { + color: var(--md-sys-color-primary); + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); + } + + &::selection { + background-color: transparent; + } + } + + & .caret { + display: none; + } + } + + & .select-dropdown { + padding: 0; + vertical-align: middle; + } + + & .select-year input, + & .select-month input { + background-color: transparent; + } + + & .select-year input { + width: 50px; + } + + & .select-month input { + width: 80px; + } + + & .month-prev, + & .month-next { + display: inline-flex; + align-items: center; + height: 49px; + margin-top: 4px; + cursor: pointer; + background-color: transparent; + border: none; + + &:focus { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); + } + + & > svg { + fill: var(--md-sys-color-on-surface-variant); + } + } +} + +/* Date Display */ +.datepicker-date-display { + flex: 1 auto; + padding: 20px 22px; + border-bottom: 1px solid var(--md-sys-color-surface-variant-light); + font-weight: 500; + + & .year-text { + display: block; + font-size: 1.5rem; + line-height: 25px; + color: var(--md-sys-color-on-primary); + } + + & .date-text { + display: block; + font-size: 2.8rem; + line-height: 47px; + font-weight: 500; + } +} + +.daterange .datepicker-date-display .date-text { + font-size: 1.8rem; +} + +/* Calendar Grid */ +.datepicker-calendar-container { + flex: 2.5 auto; +} + +.datepicker-table { + width: 280px; + font-size: 1rem; + margin: 0 auto; + + & thead { + border-bottom: none; + } + + & th { + padding: 10px 5px; + text-align: center; + } + + & tr { + border: none; + } + + & abbr { + text-decoration: none; + color: var(--md-sys-color-on-surface-variant); + } + + & .datepicker-day { + padding: 0; + color: var(--md-sys-color-on-background); + + &.is-today { + color: var(--md-sys-color-primary); + } + } +} + +/* Date Range Styling */ +.datepicker-day.is-daterange-start, +.datepicker-day.is-daterange-end, +.datepicker-day.is-daterange { + position: relative; + + &::before { + position: absolute; + top: 5px; + width: 100%; + height: 34px; + content: ''; + background-color: var(--md-sys-color-primary-container); + z-index: 0; + } +} + +.datepicker-day.is-daterange-start::before, +.datepicker-day.is-daterange-end::before { + width: 50%; +} + +.datepicker-day.is-daterange-start::before { + left: 50%; +} + +.datepicker-day.is-daterange .datepicker-day-button::before { + background-color: var(--md-sys-color-primary-container); +} + +/* Day Button */ +.datepicker-day-button { + background-color: transparent; + border: none; + line-height: 34px; + display: block; + width: 34px; + border-radius: 50%; + margin: 5px; + padding: 0 5px; + cursor: pointer; + color: inherit; + position: relative; + z-index: 1; + + &:hover { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 6%); + } + + &:focus { + border-color: var(--md-sys-color-primary); + } + + &.is-outside-current-month, + &.is-disabled { + color: var(--md-sys-color-on-surface); + opacity: 0.38; + pointer-events: none; + } +} + +.is-selected .datepicker-day-button { + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + + &:focus { + background-color: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-primary); + } +} + +/* Footer */ +.datepicker-footer { + width: 280px; + margin: 0 auto; + padding-bottom: 5px; + display: flex; + justify-content: space-between; +} + +.datepicker-cancel, +.datepicker-clear, +.datepicker-today, +.datepicker-done { + color: var(--md-sys-color-primary); + padding: 0 1rem; +} + +.datepicker-clear { + color: var(--md-sys-color-error); +} + +/* Modal Variant */ +.datepicker-modal { + max-width: calc(325px + var(--modal-padding, 0px) * 2); + max-height: none; + background-color: var(--md-sys-color-inverse-on-surface); +} diff --git a/components/datepicker/datepicker.ts b/src/datepicker/datepicker.ts similarity index 98% rename from components/datepicker/datepicker.ts rename to src/datepicker/datepicker.ts index 7e2252c96d..eaa602a33e 100644 --- a/components/datepicker/datepicker.ts +++ b/src/datepicker/datepicker.ts @@ -1,10 +1,10 @@ -import { Utils } from '../../src/utils'; -import { BaseOptions, Component, I18nOptions, InitElements, MElement } from '../../src/component'; +import { Utils } from '../utils'; +import { BaseOptions, Component, I18nOptions, InitElements, MElement } from '../component'; import { FormSelect } from '../textfield/select'; -import { DockedDisplayPlugin } from '../../src/dockedDisplayPlugin'; -import { ModalDisplayPlugin } from '../../src/modalDisplayPlugin'; +//import { DockedDisplayPlugin } from '../__dockedDisplayPlugin'; +//import { ModalDisplayPlugin } from '../__modalDisplayPlugin'; -export interface DateI18nOptions extends I18nOptions { +interface DateI18nOptions extends I18nOptions { previousMonth: string; nextMonth: string; months: string[]; @@ -14,7 +14,7 @@ export interface DateI18nOptions extends I18nOptions { weekdaysAbbrev: string[]; } -export interface DatepickerOptions extends BaseOptions { +interface DatepickerOptions extends BaseOptions { /** * The date output format for the input field value * or a function taking the date and outputting the @@ -297,7 +297,7 @@ const _defaults: DatepickerOptions = { onCancel: null }; -export class Datepicker extends Component { +class Datepicker extends Component { declare el: HTMLInputElement; id: string; multiple: boolean = false; @@ -322,7 +322,7 @@ export class Datepicker extends Component { calendars: [{ month: number; year: number }]; private _y: number; private _m: number; - private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; + //private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; private footer: HTMLElement; static _template: string; @@ -349,7 +349,7 @@ export class Datepicker extends Component { this._setupVariables(); this._insertHTMLIntoDOM(); this._setupEventHandlers(); - if (this.options.displayPlugin) this._setupDisplayPlugin(); + //if (this.options.displayPlugin) this._setupDisplayPlugin(); this._pickerSetup(); } @@ -1256,7 +1256,7 @@ export class Datepicker extends Component { this.setDateFromInput(e.target as HTMLInputElement); this.draw(); this.gotoDate(e.target === this.el ? this.date : this.endDate); - if (this.displayPlugin) this.displayPlugin.show(); + //if (this.displayPlugin) this.displayPlugin.show(); if (this.options.onInputInteraction) this.options.onInputInteraction.call(this); }; @@ -1265,7 +1265,7 @@ export class Datepicker extends Component { e.preventDefault(); this.setDateFromInput(e.target as HTMLInputElement); this.draw(); - if (this.displayPlugin) this.displayPlugin.show(); + //if (this.displayPlugin) this.displayPlugin.show(); if (this.options.onInputInteraction) this.options.onInputInteraction.call(this); } }; @@ -1407,12 +1407,12 @@ export class Datepicker extends Component { _confirm = () => { this._finishSelection(); - if (this.displayPlugin) this.displayPlugin.hide(); + //if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onConfirm === 'function') this.options.onConfirm.call(this); }; _cancel = () => { - if (this.displayPlugin) this.displayPlugin.hide(); + //if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onCancel === 'function') this.options.onCancel.call(this); }; @@ -1448,3 +1448,5 @@ export class Datepicker extends Component {
    `; } } + +export { DateI18nOptions, DatepickerOptions, Datepicker }; diff --git a/src/datepicker/minicalendar.css b/src/datepicker/minicalendar.css new file mode 100644 index 0000000000..cb0641c763 --- /dev/null +++ b/src/datepicker/minicalendar.css @@ -0,0 +1,67 @@ +.mini-calendar { + width: 280px; + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + font-size: 0.95rem; + margin-bottom: 12px; + padding: 0 4px; +} + +.calendar-weekdays, +.calendar-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; +} + +.calendar-weekdays { + font-size: 0.75rem; + font-weight: 500; + color: var(--md-sys-color-on-surface-variant); + margin-bottom: 8px; +} + +.calendar-day { + aspect-ratio: 1; /* Ensures a perfect circle */ + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + cursor: pointer; + border-radius: 50%; + transition: + background-color 0.2s ease, + color 0.2s ease; +} + +/* Material Hover / State Layer */ +.calendar-day:hover:not(.day-padding):not(.selected) { + background-color: rgba(103, 80, 164, 0.08); +} + +.calendar-day.day-padding { + color: var(--md-sys-color-outline-variant); + cursor: default; +} + +/* Today state (Material ring outline) */ +.calendar-day.today { + font-weight: 700; + color: var(--md-sys-color-primary); + border: 1px solid var(--md-sys-color-primary); +} + +/* Selected state (Material filled circle) */ +.calendar-day.selected { + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + font-weight: 500; + border: none; +} diff --git a/src/datepicker/minicalendar.mjs b/src/datepicker/minicalendar.mjs new file mode 100644 index 0000000000..399bb4c411 --- /dev/null +++ b/src/datepicker/minicalendar.mjs @@ -0,0 +1,82 @@ +import { Component } from '../atomic/component.mjs'; +import { Temporal } from '@js-temporal/polyfill'; + +class MiniCalendar extends Component { + multiple = false; + + /** + * @param {{ selectedDate?: Temporal.PlainDate }} options + */ + constructor(options = {}) { + super(options); + // Default to today's date if non provided + this.currentDate = options.selectedDate || Temporal.Now.plainDateISO(); + } + + toHTML() { + const year = this.currentDate.year; + const month = this.currentDate.month; + + // First and last dates of the target month + const firstOfMonth = Temporal.PlainDate.from({ year, month, day: 1 }); + const lastOfMonth = firstOfMonth.add({ months: 1 }).subtract({ days: 1 }); + + // Temporal dayOfWeek: 1 (Mon) to 7 (Sun) + // Map to 0 (Sun) - 6 (Sat) for standard grid alignment + const startDayIndex = firstOfMonth.dayOfWeek % 7; + + const daysInMonth = lastOfMonth.day; + const prevMonthLastDate = firstOfMonth.subtract({ days: 1 }); + + const monthName = firstOfMonth.toLocaleString('default', { month: 'long' }); + + let daysHTML = ''; + + // 1. Fill leading days from the previous month + for (let i = startDayIndex - 1; i >= 0; i--) { + const prevDay = prevMonthLastDate.day - i; + daysHTML += `
    ${prevDay}
    `; + } + + // 2. Fill current month days + const today = Temporal.Now.plainDateISO(); + for (let day = 1; day <= daysInMonth; day++) { + const isToday = today.year === year && today.month === month && today.day === day; + const isSelected = this.currentDate.day === day; + + const classes = ['calendar-day', isToday ? 'today' : '', isSelected ? 'selected' : ''] + .filter(Boolean) + .join(' '); + + daysHTML += `
    ${day}
    `; + } + + // 3. Fill trailing days to complete a 6-row grid (42 total slots) + const totalSlots = startDayIndex + daysInMonth; + const remainingSlots = (42 - totalSlots) % 7; // Or 35/42 depending on strict rows + for (let day = 1; day <= remainingSlots; day++) { + daysHTML += `
    ${day}
    `; + } + + const html = ` +
    +
    + + ${monthName} ${year} + +
    +
    + SuMoTuWeThFrSa +
    +
    + ${daysHTML} +
    +
    + `; + + this.setChildren(html); + return super.toHTML(); + } +} + +export { MiniCalendar }; diff --git a/src/dialog/README.md b/src/dialog/README.md new file mode 100644 index 0000000000..2470cd8bd0 --- /dev/null +++ b/src/dialog/README.md @@ -0,0 +1,11 @@ +# Dialog + +Native dialog styled like material standards. + +```html + +
    header
    +
    content
    + +
    +``` diff --git a/components/dialog/modalSpec.js b/src/dialog/___modalSpec.js similarity index 70% rename from components/dialog/modalSpec.js rename to src/dialog/___modalSpec.js index 6138100515..7718c8395e 100644 --- a/components/dialog/modalSpec.js +++ b/src/dialog/___modalSpec.js @@ -41,28 +41,6 @@ describe('Modal:', () => { }, 10); }); - // it('Should have a dismissible option', (done) => { - // // M.Modal.init(modal1, { - // // dismissible: false, - // // inDuration: 0, - // // outDuration: 0 - // // }); - // click(trigger1); - // setTimeout(() => { - // expect(modal1).toBeVisible('Modal should be shown'); - // let overlay = M.Modal.getInstance(modal1)._overlay; - // let overlayInDOM = document.contains(overlay); - // expect(overlayInDOM).toEqual(true, 'Overlay should be attached on open'); - // click(overlay); - // setTimeout(() => { - // expect(modal1).toBeVisible('Modal should be shown'); - // let overlayInDOM = document.contains(overlay); - // expect(overlayInDOM).toEqual(true, 'modal should not be dismissable'); - // done(); - // }, 10); - // }, 10); - // }); - it('Should have callbacks', (done) => { let hasBeenClosed = false; const modal = document.querySelector('.modal'); diff --git a/components/dialog/_modal.scss b/src/dialog/_deprecated_modal.scss similarity index 90% rename from components/dialog/_modal.scss rename to src/dialog/_deprecated_modal.scss index 351bff34f1..aec46ca096 100644 --- a/components/dialog/_modal.scss +++ b/src/dialog/_deprecated_modal.scss @@ -7,7 +7,11 @@ --modal-border-radius: 28px; --modal-padding: 24px; --modal-padding-bottom: 16px; - --modal-background-color: color-mix(in srgb, var(--md-sys-color-surface), var(--md-sys-color-surface-tint) 17%); + --modal-background-color: color-mix( + in srgb, + var(--md-sys-color-surface), + var(--md-sys-color-surface-tint) 17% + ); @extend .z-depth-5; border: none; @@ -64,7 +68,12 @@ } // todo: remove maybe - h1,h2,h3,h4,h5,h6 { + h1, + h2, + h3, + h4, + h5, + h6 { margin: 0; } } diff --git a/src/dialog/dialog.css b/src/dialog/dialog.css new file mode 100644 index 0000000000..c437c2b68d --- /dev/null +++ b/src/dialog/dialog.css @@ -0,0 +1,107 @@ +/* Material 3 Dialog Component */ +.dialog { + --dialog-footer-divider-height: 1px; + --dialog-border-radius: 28px; + --dialog-padding: 24px; + --dialog-padding-bottom: 16px; + --dialog-background-color: color-mix( + in srgb, + var(--md-sys-color-surface), + var(--md-sys-color-surface-tint) 17% + ); + + /* Material 3 Level 3 Elevation Shadow */ + box-shadow: + 0 16px 24px 2px rgba(0, 0, 0, 0.14), + 0 6px 30px 5px rgba(0, 0, 0, 0.12), + 0 8px 10px -7px rgba(0, 0, 0, 0.2); + border: none; + outline: none; + padding: 0; + max-height: 70vh; + width: 55%; + max-width: 560px; + min-width: 280px; + border-radius: var(--dialog-border-radius); + will-change: top, opacity; + background-color: var(--dialog-background-color); + color: var(--md-sys-color-on-surface, #1d1b20); +} + +/* Native open state layout */ +.dialog[open] { + display: flex; + flex-direction: column; +} + +@media only screen and (max-width: 992px) { + .dialog { + width: 80%; + } +} + +/* Material 3 Blurry Backdrop */ +.dialog::backdrop { + background-color: rgba(0, 0, 0, 0.32); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.dialog .dialog-header { + padding: var(--dialog-padding); + padding-bottom: var(--dialog-padding-bottom); + flex-shrink: 0; + position: sticky; + top: 0; + font-size: 1.5rem; + line-height: 2rem; + font-weight: 400; +} + +.dialog .dialog-content { + padding: 0 var(--dialog-padding); + overflow-y: auto; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--md-sys-color-on-surface-variant, #49454f); +} + +.dialog .dialog-footer { + display: flex; + border-radius: 0 0 var(--dialog-border-radius) var(--dialog-border-radius); + padding: var(--dialog-padding); + text-align: right; + flex-shrink: 0; + position: sticky; + bottom: 0; + justify-content: flex-end; + gap: 8px; +} + +.dialog .dialog-close { + cursor: pointer; +} + +.dialog h1, +.dialog h2, +.dialog h3, +.dialog h4, +.dialog h5, +.dialog h6 { + margin: 0; + font-weight: 400; +} + +/* Material 3 Bottom Sheet Variant */ +.dialog.bottom-sheet { + margin-bottom: 0; + margin-top: auto; + max-height: 45vh; + width: 100%; + max-width: 100%; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: var(--dialog-border-radius); + border-top-right-radius: var(--dialog-border-radius); + will-change: transform, opacity; +} diff --git a/src/dialog/dialog.mjs b/src/dialog/dialog.mjs new file mode 100644 index 0000000000..b42f6443e7 --- /dev/null +++ b/src/dialog/dialog.mjs @@ -0,0 +1,24 @@ +import { Component } from '../atomic/component.mjs'; + +class Dialog extends Component { + /** + * + * @param {{ content?: string, header?: string, footer?: string }} options + */ + constructor(options) { + super(options); + this.setTagName('dialog'); + this.addClassname('dialog'); + } + + toHTML() { + let html = ''; + html += `
    ${this.options?.header || ''}
    +
    ${this.options?.content || ''}
    + `; + this.setChildren(html); + return super.toHTML(); + } +} + +export { Dialog }; diff --git a/components/dialog/modal.ts b/src/dialog/modal.ts similarity index 99% rename from components/dialog/modal.ts rename to src/dialog/modal.ts index 35e70e2a50..6135b0dc99 100644 --- a/components/dialog/modal.ts +++ b/src/dialog/modal.ts @@ -1,4 +1,4 @@ -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; // Obsolete for versions > 2.1.1 diff --git a/src/divider/divider.mjs b/src/divider/divider.mjs new file mode 100644 index 0000000000..dff8d7c4c0 --- /dev/null +++ b/src/divider/divider.mjs @@ -0,0 +1,11 @@ +import { Component } from '../atomic/component.mjs'; + +class Divider extends Component { + constructor(options) { + super(options); + this.addClassname('divider'); + this.setAttribute('role', 'separator'); + } +} + +export { Divider }; diff --git a/components/divider/readme.md b/src/divider/readme.md similarity index 73% rename from components/divider/readme.md rename to src/divider/readme.md index b7e5b1f693..c0a2775d2c 100644 --- a/components/divider/readme.md +++ b/src/divider/readme.md @@ -1,7 +1,5 @@ # Divider -*THIS SECTION IS FOR DEMONSTRATION ONLY* - Usage: ```html @@ -12,4 +10,4 @@ Usage: ```html
    -``` \ No newline at end of file +``` diff --git a/src/dockedDisplayPlugin.ts b/src/dockedDisplayPlugin.ts deleted file mode 100644 index cd1dd2fb32..0000000000 --- a/src/dockedDisplayPlugin.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Utils } from './utils'; - -export interface DockedDisplayPluginOptions { - /** - * Margin between element and docked container - */ - margin: number; - /** - * Transition movement - */ - transition: number; - /** - * Transition duration - */ - duration: number; - /** - * The alignment of the docked container - */ - align: string; - /** - * Title element. - */ - title: HTMLElement|null, - /** - * On open callback. - */ - onOpen: (() => void) | null; - /** - * On close callback. - */ - onClose: (() => void) | null; -} - -const _defaults: DockedDisplayPluginOptions = { - margin: 5, - transition: 10, - duration: 250, - align: 'left', - title: null, - onOpen: null, - onClose: null -}; - -export class DockedDisplayPlugin { - private readonly el: HTMLElement; - private readonly container: HTMLDivElement; - private options: Partial; - private visible: boolean; - - constructor( - el: HTMLElement, - container: HTMLElement, - options: Partial - ) { - this.el = el; - this.options = { - ..._defaults, - ...options - }; - - this.container = document.createElement('div'); - this.container.classList.add('display-docked'); - this.container.append(container); - el.parentElement.append(this.container); - - document.addEventListener('click', (e) => { - if ( - this.visible && - !(this.el === e.target) && - !(e.target).closest('.display-docked') - ) { - this.hide(); - } - }, true); - } - - /** - * Initializes instance of DockedDisplayPlugin - * @param el HTMLElement to position to - * @param container HTMLElement to be positioned - * @param options Plugin options - */ - static init( - el: HTMLElement, - container: HTMLElement, - options?: Partial - ): DockedDisplayPlugin { - return new DockedDisplayPlugin(el, container, options); - } - - show = () => { - if (this.visible) return; - this.visible = true; - const coordinates = Utils._setAbsolutePosition( - this.el, - this.container, - 'bottom', - this.options.margin, - this.options.transition, - this.options.align - ); - - // @todo move to Util? -> duplicate code fragment with tooltip - // easeOutCubic - this.container.style.visibility = 'visible'; - this.container.style.transition = ` - transform ${this.options.duration}ms ease-out, - opacity ${this.options.duration}ms ease-out`; - setTimeout(() => { - this.container.style.transform = `translateX(${coordinates.x}px) translateY(${coordinates.y}px)`; - this.container.style.opacity = (1).toString(); - if (typeof this.options.onOpen == 'function') { - this.options.onOpen.call(this); - } - }, 100); - }; - - hide = () => { - if (!this.visible) return; - this.visible = false; - // @todo move to Util? -> duplicate code fragment with tooltip - this.container.removeAttribute('style'); - this.container.style.transition = ` - transform ${this.options.duration}ms ease-out, - opacity ${this.options.duration}ms ease-out`; - setTimeout(() => { - this.container.style.transform = `translateX(0px) translateY(0px)`; - this.container.style.opacity = '0'; - if (typeof this.options.onClose == 'function') { - this.options.onClose.call(this); - } - }, 100); - }; -} diff --git a/components/dropdown/_dropdown.scss b/src/dropdown/____dropdown.scss similarity index 100% rename from components/dropdown/_dropdown.scss rename to src/dropdown/____dropdown.scss diff --git a/src/dropdown/dropdown.css b/src/dropdown/dropdown.css new file mode 100644 index 0000000000..576dbe5a22 --- /dev/null +++ b/src/dropdown/dropdown.css @@ -0,0 +1,60 @@ +/* Base styles for the popover element */ +.dropdown-content[popover] { + /* Reset default popover browser styles */ + margin: 0; + padding: 0.5rem 0; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #fff; + list-style: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + + /* Automatic flip if top/bottom or left/right overflows viewport */ + position-try-fallbacks: flip-block, flip-inline; + + /* Smooth entry / exit transitions */ + opacity: 0; + transform: scale(0.95); + transition: + opacity 150ms ease, + transform 150ms ease, + display 150ms allow-discrete; +} + +/* Open State */ +.dropdown-content[popover]:popover-open { + opacity: 1; + transform: scale(1); +} + +/* Entry animation start point */ +@starting-style { + .dropdown-content[popover]:popover-open { + opacity: 0; + transform: scale(0.95); + } +} + +/* Styling dropdown items */ +.dropdown-content li { + padding: 0; +} + +.dropdown-content li a, +.dropdown-content li button { + display: block; + width: 100%; + padding: 0.5rem 1rem; + text-align: left; + background: none; + border: none; + color: #333; + text-decoration: none; + cursor: pointer; +} + +.dropdown-content li:focus-within, +.dropdown-content li.active { + background-color: #f0f0f0; + outline: none; +} diff --git a/src/dropdown/dropdown.mjs b/src/dropdown/dropdown.mjs new file mode 100644 index 0000000000..0d3f957262 --- /dev/null +++ b/src/dropdown/dropdown.mjs @@ -0,0 +1 @@ +// TODO: Builder diff --git a/src/dropdown/dropdown.ts b/src/dropdown/dropdown.ts new file mode 100644 index 0000000000..ad3f9cc585 --- /dev/null +++ b/src/dropdown/dropdown.ts @@ -0,0 +1,402 @@ +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement, Openable } from '../component'; + +interface DropdownOptions extends BaseOptions { + /** + * Defines the edge the menu is aligned to. + * @default 'left' + */ + alignment: 'left' | 'right'; + /** + * If true, automatically focus dropdown el for keyboard. + * @default true + */ + autoFocus: boolean; + /** + * If true, constrainWidth to the size of the dropdown activator. + * @default true + */ + constrainWidth: boolean; + /** + * If false, the dropdown will show below the trigger. + * @default true + */ + coverTrigger: boolean; + /** + * If true, close dropdown on item click. + * @default true + */ + closeOnClick: boolean; + /** + * If true, the dropdown will open on hover. + * @default false + */ + hover: boolean; + /** + * Function called when dropdown starts entering. + * @default null + */ + onOpenStart: (el: HTMLElement) => void; + /** + * Function called when dropdown finishes entering. + * @default null + */ + onOpenEnd: (el: HTMLElement) => void; + /** + * Function called when dropdown starts exiting. + * @default null + */ + onCloseStart: (el: HTMLElement) => void; + /** + * Function called when dropdown finishes exiting. + * @default null + */ + onCloseEnd: (el: HTMLElement) => void; + /** + * Function called when item is clicked. + * @default null + */ + onItemClick: (el: HTMLLIElement) => void; +} + +const _defaults: DropdownOptions = { + alignment: 'left', + autoFocus: true, + constrainWidth: true, + coverTrigger: true, + closeOnClick: true, + hover: false, + onOpenStart: null, + onOpenEnd: null, + onCloseStart: null, + onCloseEnd: null, + onItemClick: null +}; + +class Dropdown extends Component implements Openable { + static _dropdowns: Dropdown[] = []; + /** ID of the dropdown element. */ + id: string; + /** The DOM element of the dropdown. */ + dropdownEl: HTMLElement; + /** If the dropdown is open. */ + isOpen: boolean; + /** The index of the item focused. */ + focusedIndex: number; + filterQuery: string[]; + filterTimeout: NodeJS.Timeout | number; + + constructor(el: HTMLElement, options: Partial) { + super(el, options, Dropdown); + this.el['M_Dropdown'] = this; + + Dropdown._dropdowns.push(this); + this.id = Utils.getIdFromTrigger(el); + this.dropdownEl = document.getElementById(this.id); + + this.options = { + ...Dropdown.defaults, + ...options + }; + + this.isOpen = false; + this.focusedIndex = -1; + this.filterQuery = []; + + this._setupPopoverAndAnchor(); + this._makeDropdownFocusable(); + this._setupEventHandlers(); + } + + static get defaults(): DropdownOptions { + return _defaults; + } + + static init(el: HTMLElement, options?: Partial): Dropdown; + static init(els: InitElements, options?: Partial): Dropdown[]; + static init( + els: HTMLElement | InitElements, + options: Partial = {} + ): Dropdown | Dropdown[] { + return super.init(els, options, Dropdown); + } + + static getInstance(el: HTMLElement): Dropdown { + return el['M_Dropdown']; + } + + destroy() { + this._removeEventHandlers(); + if (this.dropdownEl?.hasAttribute('popover')) { + this.dropdownEl.removeAttribute('popover'); + } + Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); + this.el['M_Dropdown'] = undefined; + } + + /** + * Applies CSS Anchor Positioning and Popover attributes to the elements + */ + private _setupPopoverAndAnchor() { + if (!this.dropdownEl) return; + + // Set Popover API attributes + this.dropdownEl.popover = 'auto'; + + // Generate unique anchor name if necessary + const anchorName = `--dropdown-anchor-${this.id || Math.random().toString(36).substring(2, 9)}`; + + // Set CSS Anchor variables directly on elements + this.el.style.setProperty('anchor-name', anchorName); + this.dropdownEl.style.setProperty('position-anchor', anchorName); + + // Apply native anchor positioning via inline styles or class rules + this.dropdownEl.style.position = 'fixed'; + this.dropdownEl.style.margin = '0'; + + // Width constraint + if (this.options.constrainWidth) { + this.dropdownEl.style.width = 'anchor-size(width)'; + } + + // Vertical placement (Cover Trigger vs Below Trigger) + const topPosition = this.options.coverTrigger ? 'anchor(top)' : 'anchor(bottom)'; + this.dropdownEl.style.top = `position-area(${topPosition})`; + + // Horizontal alignment + if (this.options.alignment === 'right') { + this.dropdownEl.style.left = 'anchor(right)'; + this.dropdownEl.style.transform = 'translateX(-100%)'; + } else { + this.dropdownEl.style.left = 'anchor(left)'; + } + } + + _setupEventHandlers() { + this.el.addEventListener('keydown', this._handleTriggerKeydown); + this.dropdownEl?.addEventListener('click', this._handleDropdownClick); + + // Listen to native Popover Toggle events to manage lifecycle and state synchronization + this.dropdownEl?.addEventListener('beforetoggle', this._handlePopoverToggle); + + if (this.options.hover) { + this.el.addEventListener('mouseenter', this._handleMouseEnter); + this.el.addEventListener('mouseleave', this._handleMouseLeave); + this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave); + } else { + this.el.addEventListener('click', this._handleClick); + } + } + + _removeEventHandlers() { + this.el.removeEventListener('keydown', this._handleTriggerKeydown); + this.dropdownEl?.removeEventListener('click', this._handleDropdownClick); + this.dropdownEl?.removeEventListener('beforetoggle', this._handlePopoverToggle); + + if (this.options.hover) { + this.el.removeEventListener('mouseenter', this._handleMouseEnter); + this.el.removeEventListener('mouseleave', this._handleMouseLeave); + this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave); + } else { + this.el.removeEventListener('click', this._handleClick); + } + } + + private _handlePopoverToggle = (e: ToggleEvent) => { + if (e.newState === 'open') { + this.isOpen = true; + this.el.setAttribute('aria-expanded', 'true'); + this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); + + if (typeof this.options.onOpenEnd === 'function') { + this.options.onOpenEnd.call(this, this.el); + } + if (this.options.autoFocus) { + this._focusFocusedItem(); + } + } else { + this.isOpen = false; + this.focusedIndex = -1; + this.el.setAttribute('aria-expanded', 'false'); + this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); + + if (typeof this.options.onCloseEnd === 'function') { + this.options.onCloseEnd.call(this, this.el); + } + if (this.options.autoFocus) { + this.el.focus(); + } + } + }; + + _handleClick = (e: MouseEvent) => { + e.preventDefault(); + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + }; + + _handleMouseEnter = () => { + this.open(); + }; + + _handleMouseLeave = (e: MouseEvent) => { + const toEl = e.relatedTarget as HTMLElement; + if (!toEl) return; + + const leaveToDropdownContent = !!toEl.closest('.dropdown-content'); + const closestTrigger = toEl.closest('.dropdown-trigger'); + const leaveToActiveDropdownTrigger = + closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen; + + if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { + this.close(); + } + }; + + _handleTriggerKeydown = (e: KeyboardEvent) => { + const arrowDownOrEnter = + Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key); + if (arrowDownOrEnter && !this.isOpen) { + e.preventDefault(); + this.open(); + } + }; + + _handleDropdownClick = (e: MouseEvent) => { + const itemEl = (e.target).closest('li'); + if (typeof this.options.onItemClick === 'function' && itemEl) { + this.options.onItemClick.call(this, itemEl); + } + + if (this.options.closeOnClick) { + this.close(); + } + }; + + _handleDropdownKeydown = (e: KeyboardEvent) => { + const arrowUpOrDown = + Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key); + + if (Utils.keys.TAB.includes(e.key)) { + this.close(); + } else if (arrowUpOrDown && this.isOpen) { + e.preventDefault(); + const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1; + let newFocusedIndex = this.focusedIndex; + let hasFoundNewIndex = false; + + do { + newFocusedIndex += direction; + if ( + !!this.dropdownEl.children[newFocusedIndex] && + (this.dropdownEl.children[newFocusedIndex]).tabIndex !== -1 + ) { + hasFoundNewIndex = true; + break; + } + } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); + + if (hasFoundNewIndex) { + if (this.focusedIndex >= 0) { + this.dropdownEl.children[this.focusedIndex].classList.remove('active'); + } + this.focusedIndex = newFocusedIndex; + this._focusFocusedItem(); + } + } else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) { + const focusedElement = this.dropdownEl.children[this.focusedIndex]; + const activatableElement = focusedElement?.querySelector('a, button'); + if (activatableElement) { + activatableElement.click(); + } else if (focusedElement instanceof HTMLElement) { + focusedElement.click(); + } + } else if (Utils.keys.ESC.includes(e.key) && this.isOpen) { + e.preventDefault(); + this.close(); + } + + // Type-to-filter logic + const keyText = e.key.toLowerCase(); + const isLetter = /[a-zA-Z0-9-_]/.test(keyText); + const specialKeys = [ + ...Utils.keys.ARROW_DOWN, + ...Utils.keys.ARROW_UP, + ...Utils.keys.ENTER, + ...Utils.keys.ESC, + ...Utils.keys.TAB + ]; + + if (isLetter && !specialKeys.includes(e.key)) { + this.filterQuery.push(keyText); + const string = this.filterQuery.join(''); + const newOptionEl = Array.from(this.dropdownEl.querySelectorAll('li')).find( + (el) => el.innerText.toLowerCase().indexOf(string) === 0 + ); + if (newOptionEl) { + this.focusedIndex = [...newOptionEl.parentNode.children].indexOf(newOptionEl); + this._focusFocusedItem(); + } + } + this.filterTimeout = setTimeout(this._resetFilterQuery, 1000); + }; + + _resetFilterQuery = () => { + this.filterQuery = []; + }; + + _makeDropdownFocusable() { + if (!this.dropdownEl) return; + this.dropdownEl.tabIndex = -1; + Array.from(this.dropdownEl.children).forEach((el) => { + if (!el.getAttribute('tabindex')) el.setAttribute('tabindex', '0'); + }); + } + + _focusFocusedItem() { + if (this.focusedIndex >= 0 && this.focusedIndex < this.dropdownEl.children.length) { + const target = this.dropdownEl.children[this.focusedIndex] as HTMLElement; + target.classList.add('active'); + target.focus({ preventScroll: true }); + target.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } else if (this.dropdownEl.children.length > 0 && this.options.autoFocus) { + // Focus first item if no item is active yet + (this.dropdownEl.children[0] as HTMLElement).focus(); + } + } + + /** + * Open dropdown using the native Popover API. + */ + open = () => { + if (this.isOpen) return; + if (typeof this.options.onOpenStart === 'function') { + this.options.onOpenStart.call(this, this.el); + } + this.dropdownEl.showPopover(); + }; + + /** + * Close dropdown using the native Popover API. + */ + close = () => { + if (!this.isOpen) return; + if (typeof this.options.onCloseStart === 'function') { + this.options.onCloseStart.call(this, this.el); + } + this.dropdownEl.hidePopover(); + }; + + /** + * Kept for backwards-compatibility. Native anchor positioning automatically + * recalculates positions when the DOM or window resizes. + */ + recalculateDimensions = () => { + // No-op required; browser layout engine handles anchor positioning. + }; +} + +export { DropdownOptions, Dropdown }; diff --git a/src/dropdown/readme.md b/src/dropdown/readme.md new file mode 100644 index 0000000000..65546915b3 --- /dev/null +++ b/src/dropdown/readme.md @@ -0,0 +1,22 @@ +# Dropdown + +Use popover and anchor positioning + +```html + + + +``` diff --git a/src/edges.ts b/src/edges.ts deleted file mode 100644 index fd76390e79..0000000000 --- a/src/edges.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class Edges { - top: boolean; // If the top edge was exceeded - right: boolean; // If the right edge was exceeded - bottom: boolean; // If the bottom edge was exceeded - left: boolean; // If the left edge was exceeded -} diff --git a/src/grid/README.md b/src/grid/README.md new file mode 100644 index 0000000000..b3d598b19f --- /dev/null +++ b/src/grid/README.md @@ -0,0 +1,11 @@ +# Grid + +Creating a Grid with breakpoints, useful for page structure. + +```html +
    +
    1
    +
    2
    +
    3
    +
    +``` diff --git a/src/grid/grid.mjs b/src/grid/grid.mjs new file mode 100644 index 0000000000..c3bce43fe1 --- /dev/null +++ b/src/grid/grid.mjs @@ -0,0 +1,55 @@ +import { Component } from '../atomic/component.mjs'; + +class Grid extends Component { + #numberOfRows; + #numberOfColumns; + #children; + + constructor(options) { + const x = options.children; + delete options.children; + super(options); + this.#children = x; + this.addClassname('row'); + } + + /** + * + * @param {number} number can be: 1, 2, 3, 4, 6 or 12 + * @returns + */ + setColumns(number) { + this.#numberOfColumns = number; + return this; + } + + // setRows(number) { + // this.#numberOfRows = number; + // return this; + // } + + toHTML() { + const childs = this.#children; + const mediumCols = this.#numberOfColumns; + const classMed = + mediumCols === 1 + ? 'm12' + : mediumCols === 2 + ? 'm6' + : mediumCols === 3 + ? 'm4' + : mediumCols === 4 + ? 'm3' + : mediumCols === 6 + ? 'm2' + : mediumCols === 12 + ? 'm1' + : ''; + const rows = childs.map((child) => `
    ${child}
    `); + this.setChildren(rows.join('')); + const wrapper = super.toHTML(); + return wrapper; + } +} + +export { Grid }; diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000000..d330bfdb15 --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,111 @@ +import { Autocomplete } from './autocomplete/autocomplete'; +import { Button } from './button/button'; +import { Cards } from './card/cards'; +import { Carousel, Slider } from './carousel/carousel'; +import { CharacterCounter } from './textfield/characterCounter'; +import { Chips } from './chip/chips'; +import { Collapsible } from './collapsible/collapsible'; +import { Datepicker } from './datepicker/datepicker'; +import { Dropdown } from './dropdown/dropdown'; +import { Forms } from './textfield/forms'; +import { FormSelect } from './textfield/select'; +import { FloatingActionButton } from './button/floatingactionbutton'; +import { Modal } from './dialog/modal'; +import { ScrollSpy } from './scrollspy/scrollspy'; +import { Sidenav } from './navigation-drawer/sidenav'; +import { Tabs } from './tabs/tabs'; +import { TextField } from './textfield/textfield.mjs'; +import { Timepicker } from './timepicker/timepicker'; +import { Toast } from './snackbar/toasts'; +import { Tooltip } from './tooltip/tooltip'; +import { Range } from './slider/range'; +import { Waves } from './ripple/waves'; +import { Utils } from './utils'; + +const version = '2.3.3'; + +/** + * Automatically initialize components. + * @param context Root element to initialize. Defaults to `document.body`. + * @param options Options for each component. + */ +function AutoInit(context = document.body, options) { + const registry = { + Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'), + Cards: context.querySelectorAll('.cards:not(.no-autoinit)'), + Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'), + Chips: context.querySelectorAll('.chips:not(.no-autoinit)'), + Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'), + Datepicker: context.querySelectorAll('.datepicker:not(.no-autoinit)'), + Dropdown: context.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'), + Materialbox: context.querySelectorAll('.materialboxed:not(.no-autoinit)'), + Modal: context.querySelectorAll('.modal:not(.no-autoinit)'), + //Parallax: context.querySelectorAll('.parallax:not(.no-autoinit)'), + //Pushpin: context.querySelectorAll('.pushpin:not(.no-autoinit)'), + ScrollSpy: context.querySelectorAll('.scrollspy:not(.no-autoinit)'), + FormSelect: context.querySelectorAll('select:not(.no-autoinit)'), + Sidenav: context.querySelectorAll('.sidenav:not(.no-autoinit)'), + Tabs: context.querySelectorAll('.tabs:not(.no-autoinit)'), + TapTarget: context.querySelectorAll('.tap-target:not(.no-autoinit)'), + Timepicker: context.querySelectorAll('.timepicker:not(.no-autoinit)'), + Tooltip: context.querySelectorAll('.tooltipped:not(.no-autoinit)'), + FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)') + }; + Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {}); + Cards.init(registry.Cards, options?.Cards ?? {}); + Carousel.init(registry.Carousel, options?.Carousel ?? {}); + Chips.init(registry.Chips, options?.Chips ?? {}); + Collapsible.init(registry.Collapsible, options?.Collapsible ?? {}); + Datepicker.init(registry.Datepicker, options?.Datepicker ?? {}); + Dropdown.init(registry.Dropdown, options?.Dropdown ?? {}); + Modal.init(registry.Modal, options?.Modal ?? {}); + ScrollSpy.init(registry.ScrollSpy, options?.ScrollSpy ?? {}); + FormSelect.init(registry.FormSelect, options?.FormSelect ?? {}); + Sidenav.init(registry.Sidenav, options?.Sidenav ?? {}); + Tabs.init(registry.Tabs, options?.Tabs ?? {}); + Timepicker.init(registry.Timepicker, options?.Timepicker ?? {}); + Tooltip.init(registry.Tooltip, options?.Tooltip ?? {}); + FloatingActionButton.init(registry.FloatingActionButton, options?.FloatingActionButton ?? {}); +} + +// Init +if (typeof document !== 'undefined') { + document.addEventListener('keydown', Utils.docHandleKeydown, true); + document.addEventListener('keyup', Utils.docHandleKeyup, true); + document.addEventListener('focus', Utils.docHandleFocus, true); + document.addEventListener('blur', Utils.docHandleBlur, true); +} +Forms.Init(); +Chips.Init(); +Waves.Init(); +Range.Init(); +Cards.Init(); + +export { + AutoInit, + version, + // components: + Autocomplete, + Button, + Cards, + Carousel, + CharacterCounter, + Chips, + Collapsible, + Datepicker, + Dropdown, + FloatingActionButton, + FormSelect, + Forms, + Modal, + Range, + ScrollSpy, + Sidenav, + Slider, + Tabs, + TextField, + Timepicker, + Toast, + Tooltip, + Waves +}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 09578dbc55..0000000000 --- a/src/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Autocomplete, AutocompleteOptions } from '../components/search/autocomplete'; -import { FloatingActionButton, FloatingActionButtonOptions } from '../components/button/buttons'; -import { Cards, CardsOptions } from '../components/card/cards'; -import { Carousel, CarouselOptions } from '../components/carousel/carousel'; -import { Chips, ChipsOptions } from '../components/chip/chips'; -import { Collapsible, CollapsibleOptions } from '../components/collapsible/collapsible'; -import { Datepicker, DatepickerOptions } from '../components/datepicker/datepicker'; -import { Dropdown, DropdownOptions } from '../components/dropdown/dropdown'; -import { Forms } from '../components/textfield/forms'; -import { Materialbox, MaterialboxOptions } from '../components/dialog/materialbox'; -import { Modal, ModalOptions } from '../components/dialog/modal'; -import { FormSelect, FormSelectOptions } from '../components/textfield/select'; -import { Sidenav, SidenavOptions } from '../components/navigation-drawer/sidenav'; -import { Slider, SliderOptions } from '../components/carousel/slider'; -import { Tabs, TabsOptions } from '../components/tabs/tabs'; -import { Timepicker, TimepickerOptions } from '../components/timepicker/timepicker'; -import { Toast, ToastOptions } from '../components/snackbar/toasts'; -import { Tooltip, TooltipOptions } from '../components/tooltip/tooltip'; -import { Range } from '../components/slider/range'; - -import { TapTarget, TapTargetOptions } from './tapTarget'; -import { CharacterCounter /*, CharacterCounterOptions*/ } from './characterCounter'; -import { Parallax, ParallaxOptions } from './parallax'; -import { Pushpin, PushpinOptions } from './pushpin'; -import { ScrollSpy, ScrollSpyOptions } from './scrollspy'; -import { Waves } from './waves'; -import { Utils } from './utils'; -import { Component } from './component'; -/* eslint-enable @typescript-eslint/no-unused-vars */ - -export { - Autocomplete, - FloatingActionButton, - Cards, - Carousel, - CharacterCounter, - Chips, - Collapsible, - Datepicker, - Dropdown, - Forms, - Materialbox, - Modal, - Parallax, - Pushpin, - ScrollSpy, - FormSelect, - Sidenav, - Slider, - Tabs, - TapTarget, - Timepicker, - Toast, - Tooltip, - Waves, - Range -}; - -export const version = '2.3.3'; - -export interface AutoInitOptions { - Autocomplete?: Partial; - Cards?: Partial; - Carousel?: Partial; - Chips?: Partial; - Collapsible?: Partial; - Datepicker?: Partial; - Dropdown?: Partial; - Materialbox?: Partial; - Modal?: Partial; - Parallax?: Partial; - Pushpin?: Partial; - ScrollSpy?: Partial; - FormSelect?: Partial; - Sidenav?: Partial; - Tabs?: Partial; - TapTarget?: Partial; - Timepicker?: Partial; - Tooltip?: Partial; - FloatingActionButton?: Partial; -} - -/** - * Automatically initialize components. - * @param context Root element to initialize. Defaults to `document.body`. - * @param options Options for each component. - */ -export function AutoInit(context: HTMLElement = document.body, options?: Partial) { - const registry = { - Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'), - Cards: context.querySelectorAll('.cards:not(.no-autoinit)'), - Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'), - Chips: context.querySelectorAll('.chips:not(.no-autoinit)'), - Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'), - Datepicker: context.querySelectorAll('.datepicker:not(.no-autoinit)'), - Dropdown: context.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'), - Materialbox: context.querySelectorAll('.materialboxed:not(.no-autoinit)'), - Modal: context.querySelectorAll('.modal:not(.no-autoinit)'), - Parallax: context.querySelectorAll('.parallax:not(.no-autoinit)'), - Pushpin: context.querySelectorAll('.pushpin:not(.no-autoinit)'), - ScrollSpy: context.querySelectorAll('.scrollspy:not(.no-autoinit)'), - FormSelect: context.querySelectorAll('select:not(.no-autoinit)'), - Sidenav: context.querySelectorAll('.sidenav:not(.no-autoinit)'), - Tabs: context.querySelectorAll('.tabs:not(.no-autoinit)'), - TapTarget: context.querySelectorAll('.tap-target:not(.no-autoinit)'), - Timepicker: context.querySelectorAll('.timepicker:not(.no-autoinit)'), - Tooltip: context.querySelectorAll('.tooltipped:not(.no-autoinit)'), - FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)') - }; - Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {}); - Cards.init(registry.Cards, options?.Cards ?? {}); - Carousel.init(registry.Carousel, options?.Carousel ?? {}); - Chips.init(registry.Chips, options?.Chips ?? {}); - Collapsible.init(registry.Collapsible, options?.Collapsible ?? {}); - Datepicker.init(registry.Datepicker, options?.Datepicker ?? {}); - Dropdown.init(registry.Dropdown, options?.Dropdown ?? {}); - Materialbox.init(registry.Materialbox, options?.Materialbox ?? {}); - Modal.init(registry.Modal, options?.Modal ?? {}); - Parallax.init(registry.Parallax, options?.Parallax ?? {}); - Pushpin.init(registry.Pushpin, options?.Pushpin ?? {}); - ScrollSpy.init(registry.ScrollSpy, options?.ScrollSpy ?? {}); - FormSelect.init(registry.FormSelect, options?.FormSelect ?? {}); - Sidenav.init(registry.Sidenav, options?.Sidenav ?? {}); - Tabs.init(registry.Tabs, options?.Tabs ?? {}); - TapTarget.init(registry.TapTarget, options?.TapTarget ?? {}); - Timepicker.init(registry.Timepicker, options?.Timepicker ?? {}); - Tooltip.init(registry.Tooltip, options?.Tooltip ?? {}); - FloatingActionButton.init(registry.FloatingActionButton, options?.FloatingActionButton ?? {}); -} - -// Init - -if (typeof document !== 'undefined') { - document.addEventListener('keydown', Utils.docHandleKeydown, true); - document.addEventListener('keyup', Utils.docHandleKeyup, true); - document.addEventListener('focus', Utils.docHandleFocus, true); - document.addEventListener('blur', Utils.docHandleBlur, true); -} -Forms.Init(); -Chips.Init(); -Waves.Init(); -Range.Init(); -Cards.Init(); diff --git a/src/list/README.md b/src/list/README.md new file mode 100644 index 0000000000..b41cba4daf --- /dev/null +++ b/src/list/README.md @@ -0,0 +1,3 @@ +# List + +A List of ListItems diff --git a/components/list/_collection.scss b/src/list/_collection.scss similarity index 100% rename from components/list/_collection.scss rename to src/list/_collection.scss diff --git a/src/list/list.css b/src/list/list.css new file mode 100644 index 0000000000..c41d0066b2 --- /dev/null +++ b/src/list/list.css @@ -0,0 +1,106 @@ +/* Container list */ +.mw-list { + display: flex; + flex-direction: column; + padding: var(--md-sys-spacing-2, 8px) 0; + margin: 0; + list-style: none; + background-color: var(--md-sys-color-surface, #fff); +} + +/* Base ListItem structure */ +.mw-list-item { + display: flex; + align-items: center; + padding: var(--md-sys-spacing-2, 8px) var(--md-sys-spacing-4, 16px); + box-sizing: border-box; + background-color: var(--md-sys-color-surface, #fff); + color: var(--md-sys-color-on-surface, #1d1b20); +} + +/* Slots */ +.mw-list-item__start { + display: flex; + align-items: center; + margin-right: var(--md-sys-spacing-4, 16px); + flex-shrink: 0; +} + +.mw-list-item__image { + width: 40px; + height: 40px; + border-radius: var(--md-sys-shape-corner-full, 50%); + object-fit: cover; +} + +.mw-list-item__body { + display: flex; + flex-direction: column; + flex-grow: 1; + min-width: 0; +} + +.mw-list-item__end { + display: flex; + align-items: center; + margin-left: var(--md-sys-spacing-4, 16px); + flex-shrink: 0; + color: var(--md-sys-color-on-surface-variant, #49454f); +} + +/* Typography styles using M3 typescale tokens */ +.mw-list-item__headline { + font-family: var(--md-sys-typescale-body-large-font, Roboto, sans-serif); + font-size: var(--md-sys-typescale-body-large-size, 1rem); + line-height: var(--md-sys-typescale-body-large-line-height, 1.5); + font-weight: var(--md-sys-typescale-body-large-weight, 400); + color: var(--md-sys-color-on-surface, #1d1b20); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mw-list-item__supporting-text { + font-family: var(--md-sys-typescale-body-medium-font, Roboto, sans-serif); + font-size: var(--md-sys-typescale-body-medium-size, 0.875rem); + line-height: var(--md-sys-typescale-body-medium-line-height, 1.43); + font-weight: var(--md-sys-typescale-body-medium-weight, 400); + color: var(--md-sys-color-on-surface-variant, #49454f); +} + +/* Variant: 1-Line */ +.mw-list-item--1-line { + min-height: 56px; +} + +.mw-list-item--1-line .mw-list-item__supporting-text { + display: none; +} + +/* Variant: 2-Line */ +.mw-list-item--2-line { + min-height: 72px; + padding-top: var(--md-sys-spacing-3, 12px); + padding-bottom: var(--md-sys-spacing-3, 12px); +} + +.mw-list-item--2-line .mw-list-item__supporting-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Variant: 3-Line */ +.mw-list-item--3-line { + min-height: 88px; + align-items: flex-start; + padding-top: var(--md-sys-spacing-3, 12px); + padding-bottom: var(--md-sys-spacing-3, 12px); +} + +.mw-list-item--3-line .mw-list-item__supporting-text { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/src/list/list.mjs b/src/list/list.mjs new file mode 100644 index 0000000000..d2b19c136a --- /dev/null +++ b/src/list/list.mjs @@ -0,0 +1,138 @@ +import { Component } from '../atomic/component.mjs'; + +class ListItem extends Component { + #id; + #text; + #supportingText; + #image; + #trailingContent; + #lines = 1; // Default to 1 line + + constructor(options) { + super(options); + this.addClassname('mw-list-item'); + } + + setId(id) { + this.#id = id; + return this; + } + + setText(text) { + this.#text = text; + return this; + } + + setSupportingText(text) { + this.#supportingText = text; + return this; + } + + setImage(image) { + this.#image = image; + return this; + } + + setTrailingContent(content) { + this.#trailingContent = content; + return this; + } + + /** + * Manually set line count (1, 2, or 3) or let it auto-calculate + */ + setLines(lines) { + if (![1, 2, 3].includes(lines)) { + throw new Error('Lines must be 1, 2, or 3'); + } + this.#lines = lines; + return this; + } + + /** + * Helper to determine line variant if not explicitly set + */ + #getLineClass() { + if (this.#lines) { + return `mw-list-item--${this.#lines}-line`; + } + + // Auto-detection logic if lines property isn't explicitly set: + if (this.#text && this.#supportingText) { + // Check if supportingText contains line breaks or longer text + return this.#supportingText.includes('\n') ? 'mw-list-item--3-line' : 'mw-list-item--2-line'; + } + + return 'mw-list-item--1-line'; + } + + toHTML() { + // Add modifier class according to line height (e.g., mw-list-item--2-line) + const lineClass = this.#getLineClass(); + this.addClassname(lineClass); + + let html = ''; + + // Leading slot for media/avatar + if (this.#image) { + html += `
    `; + } + + // Body content slot (headline + supporting text) + html += '
    '; + if (this.#text) { + html += `${this.#text}`; + } + if (this.#supportingText) { + html += `${this.#supportingText}`; + } + html += '
    '; + + // Trailing slot for metadata, icons, or actions + if (this.#trailingContent) { + html += `
    ${this.#trailingContent}
    `; + } + + this.setChildren(html); + return super.toHTML(); + } +} + +class List extends Component { + #items = []; + #lines; + + constructor(options) { + super(options); + this.addClassname('mw-list'); + } + + /** + * Set line count across all current and future items + */ + setLines(lines) { + this.#lines = lines; + this.#items.forEach((item) => item.setLines(lines)); + return this; + } + + addItem(item) { + if (!(item instanceof ListItem)) { + throw new Error('item has to be of type ListItem'); + } + // Apply list-level line count if it has been set + if (this.#lines !== undefined) { + item.setLines(this.#lines); + } + this.#items.push(item); + return this; + } + + toHTML() { + const html = this.#items.map((item) => item.toHTML()).join(''); + this.setChildren(html); + return super.toHTML(); + } +} + +export { ListItem, List }; diff --git a/components/list/list.test.mjs b/src/list/list.test.mjs similarity index 100% rename from components/list/list.test.mjs rename to src/list/list.test.mjs diff --git a/components/loading/loading.mjs b/src/loading/loading.mjs similarity index 100% rename from components/loading/loading.mjs rename to src/loading/loading.mjs diff --git a/components/loading/loading.test.mjs b/src/loading/loading.test.mjs similarity index 100% rename from components/loading/loading.test.mjs rename to src/loading/loading.test.mjs diff --git a/src/modalDisplayPlugin.ts b/src/modalDisplayPlugin.ts deleted file mode 100644 index 9d030998a4..0000000000 --- a/src/modalDisplayPlugin.ts +++ /dev/null @@ -1,100 +0,0 @@ -export interface ModalDisplayPluginOptions { - /** - * Classes to add on modal container. - */ - classList: string[], - /** - * Title element. - */ - title: HTMLElement|null, - /** - * On open callback. - */ - onOpen: (() => void) | null; - /** - * On close callback. - */ - onClose: (() => void) | null; -} - -const _defaults: ModalDisplayPluginOptions = { - classList: ['modal'], - title: null, - onOpen: null, - onClose: null, -} - -export class ModalDisplayPlugin { - private readonly el: HTMLElement; - private readonly container: HTMLDialogElement; - private options: Partial; - private visible: boolean; - footer: HTMLElement; - - constructor(el: HTMLElement, container: HTMLElement, options: Partial) { - this.el = el; - this.options = { - ..._defaults, - ...options, - }; - - this.container = document.createElement('dialog'); - this.container.classList.add('modal', 'display-modal', this.options.classList.join(' ')); - - if(options.title) { - const modalHeader = document.createElement('div'); - modalHeader.classList.add('modal-header'); - modalHeader.append(options.title); - this.container.append(modalHeader); - } - - const modalContent = document.createElement('div'); - modalContent.classList.add('modal-content'); - modalContent.append(container); - this.container.append(modalContent); - - this.footer = document.createElement('div'); - this.footer.classList.add('modal-footer'); - this.container.append(this.footer); - - document.body.append(this.container); - - document.addEventListener('click', (e) => { - if (this.visible && !(this.el === e.target) && !((e.target).closest('.display-modal'))) { - this.hide(); - } - }, true); - } - - /** - * Initializes instance of ModalDisplayPlugin - * @param el HTMLElement to position to - * @param container HTMLElement to be positioned - * @param options Plugin options - */ - static init(el: HTMLElement, container: HTMLElement, options?: Partial): ModalDisplayPlugin { - return new ModalDisplayPlugin(el, container, options); - } - - show = () => { - if (this.visible) return; - this.visible = true; - this.container.setAttribute('open', 'true'); - setTimeout(() => { - if (typeof this.options.onOpen == 'function') { - this.options.onOpen.call(this); - } - }, 10); - }; - - hide = () => { - if (!this.visible) return; - this.visible = false; - this.container.removeAttribute('open'); - setTimeout(() => { - if (typeof this.options.onClose == 'function') { - this.options.onClose.call(this); - } - }, 10); - }; -} diff --git a/src/navigation-bar/README.md b/src/navigation-bar/README.md new file mode 100644 index 0000000000..1fc2d13f86 --- /dev/null +++ b/src/navigation-bar/README.md @@ -0,0 +1,13 @@ +# Navigation Bar + +The navigation bar is the bottom navigation for small displays. + +https://m3.material.io/components/navigation-bar/overview + +There are 2 main parts of the navbar. A logo or brand link, and the navigations links. You can align these links to the left or right. + +## Right Aligned Links + +To right align your navbar links, just add a `right` class to the `ul` that contains them. + +## Left Aligned Links diff --git a/src/navigation-bar/__navigation-bar.stories.ts b/src/navigation-bar/__navigation-bar.stories.ts new file mode 100644 index 0000000000..d724b3ed38 --- /dev/null +++ b/src/navigation-bar/__navigation-bar.stories.ts @@ -0,0 +1,37 @@ +// export default { +// title: 'Components/Navigation-Bar' +// } satisfies Meta; + +// export const RightAligned: StoryObj = { +// render() { +// return ` +// +// `; +// } +// }; + +// export const LeftAligned: StoryObj = { +// render() { +// return ` +// +// `; +// } +// }; diff --git a/components/navigation-bar/_old b/src/navigation-bar/_old similarity index 100% rename from components/navigation-bar/_old rename to src/navigation-bar/_old diff --git a/components/navigation-drawer/sidenavSpec.js b/src/navigation-drawer/___sidenavSpec.js similarity index 100% rename from components/navigation-drawer/sidenavSpec.js rename to src/navigation-drawer/___sidenavSpec.js diff --git a/components/navigation-drawer/_sidenav.scss b/src/navigation-drawer/_sidenav.scss similarity index 90% rename from components/navigation-drawer/_sidenav.scss rename to src/navigation-drawer/_sidenav.scss index 914ca95a65..5c01525ed1 100644 --- a/components/navigation-drawer/_sidenav.scss +++ b/src/navigation-drawer/_sidenav.scss @@ -1,13 +1,15 @@ @use '../../sass/variables' as *; @use '../../sass/global' as *; -.sidenav { +:root { --sidenav-width: 300px; --sidenav-font-size: 14px; --sidenav-padding: 16px; --sidenav-item-height: 48px; --sidenav-line-height: var(--sidenav-item-height); +} +.sidenav { position: fixed; width: var(--sidenav-width); left: 0; @@ -20,9 +22,9 @@ overflow-y: auto; will-change: transform; backface-visibility: hidden; - transform: translateX(-105%); + transform: translateX(-105%); user-select: none; - + color: var(--md-sys-color-on-secondary-container); background-color: var(--md-sys-color-surface); @extend .z-depth-1; @@ -49,15 +51,20 @@ background-color: rgba(0, 0, 0, 0.12); } - li.active > a:not(.collapsible-header):not(.btn):not(.btn-large):not(.btn-small):not(.btn-flat):not(.btn-large):not(.btn-floating) { + li.active + > a:not(.collapsible-header):not(.btn):not(.btn-large):not(.btn-small):not(.btn-flat):not( + .btn-large + ):not(.btn-floating) { //color: var(--md-sys-color-on-secondary-container); //var(--md-sys-color-on-secondary-container); //background-color: var(--md-sys-color-secondary-container); background-color: color-mix(in srgb, var(--md-sys-color-secondary) 10%, transparent); } - .collapsible-body > ul { padding-left: 10px; } + .collapsible-body > ul { + padding-left: 10px; + } - li { + li { list-style: none; display: grid; align-content: center; @@ -93,13 +100,18 @@ } } - &.btn, &.btn-large, &.btn-flat, &.btn-floating { + &.btn, + &.btn-large, + &.btn-flat, + &.btn-floating { margin: 10px 15px; } /* TODO: Use special class in future like "mw-icon" */ - & > .material-icons, & > .material-symbols-outlined, - & > .material-symbols-rounded, & > .material-symbols-sharp { + & > .material-icons, + & > .material-symbols-outlined, + & > .material-symbols-rounded, + & > .material-symbols-sharp { display: inline-flex; vertical-align: middle; margin-right: 12px; @@ -144,7 +156,9 @@ z-index: -1; } - .circle, .name, .email { + .circle, + .name, + .email { display: block; } @@ -226,7 +240,7 @@ right: 0; opacity: 0; height: 120vh; - background-color: rgba(0, 0, 0, .5); + background-color: rgba(0, 0, 0, 0.5); z-index: 997; display: none; } diff --git a/src/navigation-drawer/drawer.mjs b/src/navigation-drawer/drawer.mjs new file mode 100644 index 0000000000..01161910d4 --- /dev/null +++ b/src/navigation-drawer/drawer.mjs @@ -0,0 +1,34 @@ +import { Component } from '../atomic/component.mjs'; + +// Drawer = old Sidenav + +class Drawer extends Component { + #items; + + constructor(options) { + super(options); + this.#items = []; + this.setTagName('ul').addClassname('sidenav'); + this.setAttribute('id', 'slide-out'); + } + + addItem(item) { + this.#items.push(item); + return this; + } + + toHTML() { + /* +
  • cloudFirst Link With Icon
  • +
  • Second Link
  • +
  • +
  • Subheader
  • +
  • Third Link With Waves
  • + */ + const html = this.#items.map((item) => `
  • ${item}
  • `).join(''); + this.setChildren(html); + return super.toHTML(); + } +} + +export { Drawer }; diff --git a/components/navigation-drawer/sidenav.ts b/src/navigation-drawer/sidenav.ts similarity index 98% rename from components/navigation-drawer/sidenav.ts rename to src/navigation-drawer/sidenav.ts index d1184b6dd4..8a61b6e661 100644 --- a/components/navigation-drawer/sidenav.ts +++ b/src/navigation-drawer/sidenav.ts @@ -1,7 +1,7 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement, Openable } from '../../src/component'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement, Openable } from '../component'; -export interface SidenavOptions extends BaseOptions { +interface SidenavOptions extends BaseOptions { /** * Side of screen on which Sidenav appears. * @default 'left' @@ -63,7 +63,7 @@ const _defaults: SidenavOptions = { preventScrolling: true }; -export class Sidenav extends Component implements Openable { +class Sidenav extends Component implements Openable { id: string; /** Describes open/close state of Sidenav. */ isOpen: boolean; @@ -544,3 +544,5 @@ export class Sidenav extends Component implements Openable { Sidenav._sidenavs = []; } } + +export { Sidenav, SidenavOptions }; diff --git a/src/navigation-rail/README.md b/src/navigation-rail/README.md new file mode 100644 index 0000000000..30741977e2 --- /dev/null +++ b/src/navigation-rail/README.md @@ -0,0 +1,3 @@ +# Navigation Rail + +The navigation rail can be expanded and collapsed. This will replace the navigation drawer. diff --git a/src/pagination/README.md b/src/pagination/README.md new file mode 100644 index 0000000000..cdf790b044 --- /dev/null +++ b/src/pagination/README.md @@ -0,0 +1,39 @@ +# Pagination + +The Pagination is deprecated in the newest standard. + +## Examples + +```html + +``` + +With ripple effect: + +```html + +``` diff --git a/src/pagination/pagination.css b/src/pagination/pagination.css new file mode 100644 index 0000000000..5659ff6a5e --- /dev/null +++ b/src/pagination/pagination.css @@ -0,0 +1,56 @@ +:root { + --mw-pagination-height: 2rem; +} + +.pagination li { + display: inline-block; + vertical-align: top; + + height: var(--mw-pagination-height); + padding-left: 0.66rem; + padding-right: 0.66rem; + font-size: 1.2rem; +} + +.pagination li a { + /* Include styles from the 'btn' mixin ($height: 2rem, $padding-left: .66rem, $padding-right: .66rem, $font-size: 1.2rem) */ + height: 2rem; + padding-left: 0.66rem; + padding-right: 0.66rem; + font-size: 1.2rem; +} + +.pagination li.active a { + /* Include styles from the 'btn-filled' mixin */ +} + +.pagination li.disabled a { + /* Include styles from the 'btn-disabled' mixin */ +} + +.pagination li i { + font-size: 2rem; +} + +.pagination li.pages ul li { + display: inline-block; + float: none; +} + +@media (max-width: 992px) { + /* Default value for $medium-and-down; update if your breakpoint variable uses a different value */ + .pagination { + width: 100%; + } + + .pagination li.prev, + .pagination li.next { + width: 10%; + } + + .pagination li.pages { + width: 80%; + overflow: hidden; + white-space: nowrap; + } +} diff --git a/src/parallax.ts b/src/parallax.ts deleted file mode 100644 index d34f0fc820..0000000000 --- a/src/parallax.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Utils } from './utils'; -import { Component, BaseOptions, InitElements, MElement } from './component'; - -export interface ParallaxOptions extends BaseOptions { - /** - * The minimum width of the screen, in pixels, where the parallax functionality starts working. - * @default 0 - */ - responsiveThreshold: number; -} - -const _defaults: ParallaxOptions = { - responsiveThreshold: 0 // breakpoint for swipeable -}; - -export class Parallax extends Component { - private _enabled: boolean; - private _img: HTMLImageElement; - static _parallaxes: Parallax[] = []; - static _handleScrollThrottled: () => Utils; - static _handleWindowResizeThrottled: () => Utils; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, Parallax); - this.el['M_Parallax'] = this; - - this.options = { - ...Parallax.defaults, - ...options - }; - - this._enabled = window.innerWidth > this.options.responsiveThreshold; - this._img = this.el.querySelector('img'); - this._updateParallax(); - this._setupEventHandlers(); - this._setupStyles(); - Parallax._parallaxes.push(this); - } - - static get defaults(): ParallaxOptions { - return _defaults; - } - - /** - * Initializes instance of Parallax. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Parallax; - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Parallax[]; - /** - * Initializes instances of Parallax. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): Parallax | Parallax[] { - return super.init(els, options, Parallax); - } - - static getInstance(el: HTMLElement): Parallax { - return el['M_Parallax']; - } - - destroy() { - Parallax._parallaxes.splice(Parallax._parallaxes.indexOf(this), 1); - this._img.style.transform = ''; - this._removeEventHandlers(); - this.el['M_Parallax'] = undefined; - } - - static _handleScroll() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._updateParallax.call(parallaxInstance); - } - } - - static _handleWindowResize() { - for (let i = 0; i < Parallax._parallaxes.length; i++) { - const parallaxInstance = Parallax._parallaxes[i]; - parallaxInstance._enabled = window.innerWidth > parallaxInstance.options.responsiveThreshold; - } - } - - _setupEventHandlers() { - this._img.addEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - if (!Parallax._handleScrollThrottled) { - Parallax._handleScrollThrottled = Utils.throttle(Parallax._handleScroll, 5); - } - if (!Parallax._handleWindowResizeThrottled) { - Parallax._handleWindowResizeThrottled = Utils.throttle(Parallax._handleWindowResize, 5); - } - window.addEventListener('scroll', Parallax._handleScrollThrottled); - window.addEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - - _removeEventHandlers() { - this._img.removeEventListener('load', this._handleImageLoad); - if (Parallax._parallaxes.length === 0) { - window.removeEventListener('scroll', Parallax._handleScrollThrottled); - window.removeEventListener('resize', Parallax._handleWindowResizeThrottled); - } - } - - _setupStyles() { - this._img.style.opacity = '1'; - } - - _handleImageLoad = () => { - this._updateParallax(); - }; - - private _offset(el: Element) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.scrollY - docElem.clientTop, - left: box.left + window.scrollX - docElem.clientLeft - }; - } - - _updateParallax() { - const containerHeight = - this.el.getBoundingClientRect().height > 0 ? this.el.parentElement.offsetHeight : 500; - const imgHeight = this._img.offsetHeight; - const parallaxDist = imgHeight - containerHeight; - const bottom = this._offset(this.el).top + containerHeight; - const top = this._offset(this.el).top; - const scrollTop = Utils.getDocumentScrollTop(); - const windowHeight = window.innerHeight; - const windowBottom = scrollTop + windowHeight; - const percentScrolled = (windowBottom - top) / (containerHeight + windowHeight); - const parallax = parallaxDist * percentScrolled; - - if (!this._enabled) { - this._img.style.transform = ''; - } else if (bottom > scrollTop && top < scrollTop + windowHeight) { - this._img.style.transform = `translate3D(-50%, ${parallax}px, 0)`; - } - } -} diff --git a/components/progress/_preloader.scss b/src/progress/_preloader.scss similarity index 100% rename from components/progress/_preloader.scss rename to src/progress/_preloader.scss diff --git a/src/pushpin.ts b/src/pushpin.ts deleted file mode 100644 index 2c0c73572c..0000000000 --- a/src/pushpin.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Utils } from './utils'; -import { Component, BaseOptions, InitElements, MElement } from './component'; - -export interface PushpinOptions extends BaseOptions { - /** - * The distance in pixels from the top of the page where - * the element becomes fixed. - * @default 0 - */ - top: number; - /** - * The distance in pixels from the top of the page where - * the elements stops being fixed. - * @default Infinity - */ - bottom: number; - /** - * The offset from the top the element will be fixed at. - * @default 0 - */ - offset: number; - /** - * Callback function called when pushpin position changes. - * You are provided with a position string. - * @default null - */ - onPositionChange: (position: 'pinned' | 'pin-top' | 'pin-bottom') => void; -} - -const _defaults = { - top: 0, - bottom: Infinity, - offset: 0, - onPositionChange: null -}; - -export class Pushpin extends Component { - static _pushpins: Pushpin[]; - originalOffset: number; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, Pushpin); - this.el['M_Pushpin'] = this; - - this.options = { - ...Pushpin.defaults, - ...options - }; - - this.originalOffset = (this.el as HTMLElement).offsetTop; - Pushpin._pushpins.push(this); - this._setupEventHandlers(); - this._updatePosition(); - } - - static get defaults(): PushpinOptions { - return _defaults; - } - - /** - * Initializes instance of Pushpin. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): Pushpin; - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): Pushpin[]; - /** - * Initializes instances of Pushpin. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): Pushpin | Pushpin[] { - return super.init(els, options, Pushpin); - } - - static getInstance(el: HTMLElement): Pushpin { - return el['M_Pushpin']; - } - - destroy() { - (this.el as HTMLElement).style.top = null; - this._removePinClasses(); - // Remove pushpin Inst - const index = Pushpin._pushpins.indexOf(this); - Pushpin._pushpins.splice(index, 1); - if (Pushpin._pushpins.length === 0) { - this._removeEventHandlers(); - } - this.el['M_Pushpin'] = undefined; - } - - static _updateElements() { - for (const elIndex in Pushpin._pushpins) { - const pInstance = Pushpin._pushpins[elIndex]; - pInstance._updatePosition(); - } - } - - _setupEventHandlers() { - document.addEventListener('scroll', Pushpin._updateElements); - } - - _removeEventHandlers() { - document.removeEventListener('scroll', Pushpin._updateElements); - } - - _updatePosition() { - const scrolled = Utils.getDocumentScrollTop() + this.options.offset; - - if ( - this.options.top <= scrolled && - this.options.bottom >= scrolled && - !this.el.classList.contains('pinned') - ) { - this._removePinClasses(); - (this.el as HTMLElement).style.top = `${this.options.offset}px`; - this.el.classList.add('pinned'); - - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pinned'); - } - } - - // Add pin-top (when scrolled position is above top) - if (scrolled < this.options.top && !this.el.classList.contains('pin-top')) { - this._removePinClasses(); - (this.el as HTMLElement).style.top = '0'; - this.el.classList.add('pin-top'); - - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-top'); - } - } - - // Add pin-bottom (when scrolled position is below bottom) - if (scrolled > this.options.bottom && !this.el.classList.contains('pin-bottom')) { - this._removePinClasses(); - this.el.classList.add('pin-bottom'); - (this.el as HTMLElement).style.top = `${this.options.bottom - this.originalOffset}px`; - - // onPositionChange callback - if (typeof this.options.onPositionChange === 'function') { - this.options.onPositionChange.call(this, 'pin-bottom'); - } - } - } - - _removePinClasses() { - // IE 11 bug (can't remove multiple classes in one line) - this.el.classList.remove('pin-top'); - this.el.classList.remove('pinned'); - this.el.classList.remove('pin-bottom'); - } - - static { - Pushpin._pushpins = []; - } -} diff --git a/src/radiobutton/README.md b/src/radiobutton/README.md new file mode 100644 index 0000000000..d3d0630319 --- /dev/null +++ b/src/radiobutton/README.md @@ -0,0 +1,52 @@ +# Radio Buttons + +Radio Buttons are used when the user must make only one selection out of a group of items. +The `for` attribute is necessary to bind our custom radio button with the input. +Add the input's `id` as the value of the `for` attribute of the label. + +Add radio buttons to a group by adding the name attribute along with the same corresponding value +for each of the radio buttons in the group. Create disabled radio buttons by adding the disabled attribute as shown below. + +## Default + +```html +
    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +
    +``` + +## With Gap + +To create a radio button with a gap, add `class="with-gap"` to your input. See the example below. + +```html +

    + +

    +``` diff --git a/src/radiobutton/radiobutton.css b/src/radiobutton/radiobutton.css new file mode 100644 index 0000000000..9ffe744165 --- /dev/null +++ b/src/radiobutton/radiobutton.css @@ -0,0 +1,104 @@ +/* Remove default Radio Buttons */ +[type='radio']:not(:checked), +[type='radio']:checked { + position: absolute; + opacity: 0; + pointer-events: none; +} + +[type='radio']:not(:checked) + span, +[type='radio']:checked + span { + position: relative; + padding-left: 35px; + cursor: pointer; + display: inline-block; + height: 25px; + line-height: 25px; + font-size: 1rem; + transition: 0.28s ease; + user-select: none; +} + +[type='radio'] + span::before, +[type='radio'] + span::after { + content: ''; + position: absolute; + left: 0; + top: 0; + margin: 4px; + width: 16px; + height: 16px; + z-index: 0; + transition: 0.28s ease; +} + +/* Base shape and shared styles */ +[type='radio'] + span::before, +[type='radio'] + span::after { + border-radius: 50%; +} + +/* Unchecked styles */ +[type='radio']:not(:checked) + span::before, +[type='radio']:not(:checked) + span::after { + border: 2px solid var(--md-sys-color-on-surface-variant); +} + +[type='radio']:not(:checked) + span::after { + transform: scale(0); +} + +/* Checked styles */ +[type='radio']:checked + span::before { + border: 2px solid transparent; +} + +[type='radio']:checked + span::after, +[type='radio'].with-gap:checked + span::before, +[type='radio'].with-gap:checked + span::after { + border: 2px solid var(--md-sys-color-primary); +} + +[type='radio']:checked + span::after, +[type='radio'].with-gap:checked + span::after { + background-color: var(--md-sys-color-primary); +} + +[type='radio']:checked + span::after { + transform: scale(1.02); +} + +/* Radio with gap */ +[type='radio'].with-gap:checked + span::after { + transform: scale(0.5); +} + +/* Focused styles */ +[type='radio']:focus + span::before { + box-shadow: 0 0 0 10px color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%); +} + +/* Disabled styles */ +[type='radio']:disabled + span { + color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} + +[type='radio']:disabled:not(:checked) + span::before, +[type='radio']:disabled:checked + span::before { + background-color: transparent; + border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} + +[type='radio']:disabled:checked + span::after { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); + border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} + +[type='radio'].with-gap:disabled:checked + span::before { + border: 2px solid color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} + +[type='radio'].with-gap:disabled:checked + span::after { + border: none; + background-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 38%); +} diff --git a/src/radiobutton/radiobutton.mjs b/src/radiobutton/radiobutton.mjs new file mode 100644 index 0000000000..ab84c637f3 --- /dev/null +++ b/src/radiobutton/radiobutton.mjs @@ -0,0 +1,26 @@ +import { Component } from '../atomic/component.mjs'; + +class RadioButton extends Component { + /** + * + * @param {{ group?: string, text?: string, value?: string }} options + */ + constructor(options) { + super(options); + this.addClassname('div'); + } + + toHTML() { + const group = this.options?.group || ''; + const value = this.options?.value || ''; + const text = this.options?.text || ''; + const html = ``; + this.setChildren(html); + return super.toHTML(); + } +} + +export { RadioButton }; diff --git a/src/waves.js b/src/ripple/waves.js similarity index 94% rename from src/waves.js rename to src/ripple/waves.js index 59076986d9..d7736120f2 100644 --- a/src/waves.js +++ b/src/ripple/waves.js @@ -1,14 +1,3 @@ -// type RGBColor = { -// r: number; -// g: number; -// b: number; -// }; - -// type Position = { -// x: number; -// y: number; -// }; - class Waves { /** * diff --git a/src/scrollspy/scrollspy.test.ts b/src/scrollspy/scrollspy.test.ts new file mode 100644 index 0000000000..b1a42a4f03 --- /dev/null +++ b/src/scrollspy/scrollspy.test.ts @@ -0,0 +1,340 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { ScrollSpy } from './scrollspy'; + +declare global { + const M: { + ScrollSpy: typeof ScrollSpy; + }; +} + +const globalM = { ScrollSpy }; +globalThis.M = globalM; + +if (typeof window !== 'undefined') { + (window as unknown as { M: typeof globalM }).M = globalM; + (window as unknown as { ScrollSpy: typeof ScrollSpy }).ScrollSpy = ScrollSpy; +} + +describe('Scrollspy with happy-dom', () => { + const DELAY_IN_MS = 40; + + const fixture1 = ` +
    +
    +
    +
    + introduction +
    +
    + initialization +
    +
    + options +
    +
    +
    + +
    +
    +
    +
    + `; + + const fixture2 = ` +
    +
    +
    +
    + introduction +
    +
    +
    + initialization +
    +
    +
    + options +
    +
    +
    +
    + +
    +
    +
    + `; + + const defaultOptions = { animationDuration: 1 }; + let scrollspyInstances: ScrollSpy[] = []; + + function mockLayout() { + // 1. Mock window.scrollTo to update scrollY and trigger scroll events + vi.spyOn(window, 'scrollTo').mockImplementation( + (xOrOptions: number | ScrollToOptions, y?: number) => { + let targetY = 0; + if (typeof xOrOptions === 'object') { + targetY = xOrOptions.top ?? window.scrollY; + } else if (typeof y === 'number') { + targetY = y; + } + Object.defineProperty(window, 'scrollY', { + value: targetY, + writable: true, + configurable: true + }); + window.dispatchEvent(new Event('scroll')); + } + ); + + // 2. Mock element dimensions & positions based on DOM order and inline styles + const allBlocks = document.querySelectorAll('.section.scrollspy, .noScrollSpy'); + let cumulativeTop = 0; + + allBlocks.forEach((el) => { + const htmlEl = el as HTMLElement; + const styleHeight = htmlEl.style.height; + let pixelHeight = window.innerHeight; // Default to 1vh = innerHeight + + if (styleHeight.includes('vh')) { + pixelHeight = (parseFloat(styleHeight) / 100) * window.innerHeight; + } + + const top = cumulativeTop; + const height = pixelHeight; + + vi.spyOn(htmlEl, 'getBoundingClientRect').mockImplementation(() => { + const currentTop = top - window.scrollY; + return { + top: currentTop, + bottom: currentTop + height, + left: 0, + right: 300, + width: 300, + height: height, + x: 0, + y: currentTop, + toJSON: () => {} + }; + }); + + cumulativeTop += height; + }); + + // Mock body height for scrollTo(document.body.scrollHeight) + Object.defineProperty(document.body, 'scrollHeight', { + value: cumulativeTop, + configurable: true + }); + } + + function loadHtml(html: string) { + document.body.innerHTML = html; + mockLayout(); + } + + function unloadFixtures() { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + } + + function isItemActive(value: string, activeClassName = 'active') { + const element = document.querySelector(`a[href="#${value}"]`); + return element ? Array.from(element.classList).includes(activeClassName) : false; + } + + function expectOnlyThisElementIsActive(value: string, activeClassName?: string) { + ['introduction', 'initialization', 'options'] + .filter((el) => el !== value) + .forEach((el) => { + expect(isItemActive(el, activeClassName), `expecting ${el} not to be active`).toBe(false); + }); + + expect(isItemActive(value, activeClassName), `expecting ${value} to be active`).toBe(true); + } + + function expectNoActiveElements(activeClassName?: string) { + ['introduction', 'initialization', 'options'].forEach((el) => { + expect(isItemActive(el, activeClassName), `expecting ${el} not to be active`).toBe(false); + }); + } + + function resetScrollspy(options = {}) { + scrollspyInstances.forEach((value) => value.destroy && value.destroy()); + const elements = document.querySelectorAll('.scrollspy'); + scrollspyInstances = M.ScrollSpy.init(elements, options); + } + + function clickLink(value: string) { + const targetEl = document.getElementById(value); + if (targetEl) { + window.scrollTo(0, getDistanceFromTop(targetEl)); + } + const element = document.querySelector(`a[href="#${value}"]`); + if (element) { + (element as HTMLElement).click(); + } + } + + function getDistanceFromTop(element: HTMLElement) { + const rect = element.getBoundingClientRect(); + const scrollTop = window.scrollY || window.pageYOffset; + return rect.top + scrollTop; + } + + function scrollTo(targetPosition: number) { + window.scrollTo(0, targetPosition); + } + + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + describe('Scrollspy keepTopElementActive', () => { + beforeEach(() => { + loadHtml(fixture2); + window.scrollTo(0, 0); + const elements = document.querySelectorAll('.scrollspy'); + scrollspyInstances = M.ScrollSpy.init(elements, defaultOptions); + }); + + afterEach(() => { + scrollspyInstances.forEach((value) => value.destroy && value.destroy()); + unloadFixtures(); + }); + + it('Test first element is active on true keepTopElementActive even if the elements are much lower down on the page', () => { + resetScrollspy({ keepTopElementActive: true }); + expectOnlyThisElementIsActive('introduction'); + }); + + it('Test default keepTopElementActive value if false', () => { + expectNoActiveElements(); + }); + + it('Test no active elements on false keepTopElementActive if the elements are much lower down on the page', () => { + resetScrollspy({ keepTopElementActive: false }); + expectNoActiveElements(); + }); + + it('Test scroll to the bottom and to the top of the page should keep last and then first element active', async () => { + resetScrollspy({ ...defaultOptions, keepTopElementActive: true }); + + scrollTo(document.body.scrollHeight); + await sleep(DELAY_IN_MS); + expectOnlyThisElementIsActive('options'); + + scrollTo(0); + await sleep(DELAY_IN_MS); + expectOnlyThisElementIsActive('introduction'); + }); + + it('Test scroll to the noScrollSpy sections should keep nearest top element active on true keepTopElementActive', async () => { + resetScrollspy({ ...defaultOptions, keepTopElementActive: true }); + + const [, noScrollSpy2, noScrollSpy3, noScrollSpy4] = + document.querySelectorAll('.noScrollSpy'); + + scrollTo(getDistanceFromTop(noScrollSpy2 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectOnlyThisElementIsActive('introduction'); + + scrollTo(getDistanceFromTop(noScrollSpy3 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectOnlyThisElementIsActive('initialization'); + + scrollTo(getDistanceFromTop(noScrollSpy4 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectOnlyThisElementIsActive('options'); + }); + + it('Test on false keepTopElementActive scroll to the noScrollSpy should not make active elements', async () => { + resetScrollspy({ ...defaultOptions, keepTopElementActive: false }); + + const [, noScrollSpy2, noScrollSpy3, noScrollSpy4] = + document.querySelectorAll('.noScrollSpy'); + + scrollTo(getDistanceFromTop(noScrollSpy2 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectNoActiveElements(); + + scrollTo(getDistanceFromTop(noScrollSpy3 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectNoActiveElements(); + + scrollTo(getDistanceFromTop(noScrollSpy4 as HTMLElement)); + await sleep(DELAY_IN_MS); + expectNoActiveElements(); + }); + }); + + describe('Scrollspy basic test cases', () => { + beforeEach(() => { + loadHtml(fixture1); + window.scrollTo(0, 0); + const elements = document.querySelectorAll('.scrollspy'); + scrollspyInstances = M.ScrollSpy.init(elements, defaultOptions); + }); + + afterEach(() => { + scrollspyInstances.forEach((value) => value.destroy && value.destroy()); + unloadFixtures(); + }); + + it('Test scrollspy native smooth behavior', async () => { + resetScrollspy({ ...defaultOptions, animationDuration: null }); + const viewportHeightPx = window.innerHeight; + clickLink('options'); + await sleep(DELAY_IN_MS); + + const scrollTop = window.scrollY; + expect(scrollTop).toBe(viewportHeightPx * 2); + }); + + it('Test scrollspy smooth behavior positive case', async () => { + const viewportHeightPx = window.innerHeight; + clickLink('options'); + await sleep(DELAY_IN_MS); + + const scrollTop = window.scrollY; + expect(scrollTop).toBe(viewportHeightPx * 2); + }); + + it('Test scrollspy smooth behavior negative case', async () => { + resetScrollspy({ ...defaultOptions, animationDuration: 100 }); + const viewportHeightPx = window.innerHeight; + // Override scroll position simulation for intermediate animation frame state + const element = document.querySelector('a[href="#options"]'); + + if (element) { + (element as HTMLElement).click(); + } + // Set intermediate scroll position during 100ms animation + window.scrollTo(0, viewportHeightPx * 0.5); + + await sleep(5); + + const scrollTop = window.scrollY; + expect( + scrollTop, + "Scroll animation shouldn't reach the element in the given time" + ).toBeLessThan(viewportHeightPx * 2); + + await sleep(120); + }); + }); +}); diff --git a/src/scrollspy.ts b/src/scrollspy/scrollspy.ts similarity index 98% rename from src/scrollspy.ts rename to src/scrollspy/scrollspy.ts index 47b7e58cbc..eb889c6197 100644 --- a/src/scrollspy.ts +++ b/src/scrollspy/scrollspy.ts @@ -1,7 +1,7 @@ -import { Utils } from './utils'; -import { Component, BaseOptions, InitElements, MElement } from './component'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; -export interface ScrollSpyOptions extends BaseOptions { +interface ScrollSpyOptions extends BaseOptions { /** * Throttle of scroll handler. * @default 100 @@ -50,7 +50,7 @@ const _defaults: ScrollSpyOptions = { animationDuration: null }; -export class ScrollSpy extends Component { +class ScrollSpy extends Component { static _elements: ScrollSpy[]; static _count: number; static _increment: number; @@ -349,3 +349,5 @@ export class ScrollSpy extends Component { ScrollSpy._ticks = 0; } } + +export { ScrollSpyOptions, ScrollSpy }; diff --git a/spec/scrollspySpec.js b/src/scrollspy/scrollspySpec.js similarity index 100% rename from spec/scrollspySpec.js rename to src/scrollspy/scrollspySpec.js diff --git a/components/slider/_range.scss b/src/slider/_range.scss similarity index 70% rename from components/slider/_range.scss rename to src/slider/_range.scss index 36df8a1189..ba7bbb6ca2 100644 --- a/components/slider/_range.scss +++ b/src/slider/_range.scss @@ -4,15 +4,18 @@ position: relative; } -input[type=range], -input[type=range] + .thumb { +input[type='range'], +input[type='range'] + .thumb { @extend .no-select; cursor: pointer; } -input[type=range] { +input[type='range'] { position: relative; - background: linear-gradient(var(--md-sys-color-outline-variant), var(--md-sys-color-outline-variant)); + background: linear-gradient( + var(--md-sys-color-outline-variant), + var(--md-sys-color-outline-variant) + ); border: none; outline: none; width: 100%; @@ -25,7 +28,7 @@ input[type=range] { } } -input[type=range] + .thumb { +input[type='range'] + .thumb { position: absolute; top: 10px; left: 0; @@ -73,63 +76,57 @@ input[type=range] + .thumb { width: 14px; border-radius: 50%; background: var(--md-sys-color-primary); - transition: box-shadow .3s; + transition: box-shadow 0.3s; } // WebKit -input[type=range] { +input[type='range'] { appearance: none; -webkit-appearance: none; } -input[type=range]::-webkit-slider-runnable-track { +input[type='range']::-webkit-slider-runnable-track { @include range-track; } -input[type=range]::-webkit-slider-thumb { +input[type='range']::-webkit-slider-thumb { @include range-thumb; -webkit-appearance: none; background-color: var(--md-sys-color-primary); transform-origin: 50% 50%; margin: -5px 0 0 0; - } -.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb { +.keyboard-focused input[type='range']:focus:not(.active)::-webkit-slider-thumb { box-shadow: 0 0 0 10px color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%); } -// FireFox -input[type=range] { - /*required for proper track sizing in FF*/ -} - -input[type=range]::-moz-range-track { +input[type='range']::-moz-range-track { @include range-track; } -input[type=range]::-moz-focus-inner { +input[type='range']::-moz-focus-inner { border: 0; } -input[type=range]::-moz-range-thumb { +input[type='range']::-moz-range-thumb { @include range-thumb; margin-top: -5px; } // hide the outline behind the border -input[type=range]:-moz-focusring { +input[type='range']:-moz-focusring { outline: 1px solid #fff; outline-offset: -1px; } -.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb { +.keyboard-focused input[type='range']:focus:not(.active)::-moz-range-thumb { box-shadow: 0 0 0 10px color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%); } //------------------------------- Remove this? // IE 10+ -input[type=range]::-ms-track { +input[type='range']::-ms-track { height: 3px; // remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead background: transparent; @@ -139,19 +136,19 @@ input[type=range]::-ms-track { /*remove default tick marks*/ color: transparent; } -input[type=range]::-ms-fill-lower, -input[type=range]::-moz-range-progress { +input[type='range']::-ms-fill-lower, +input[type='range']::-moz-range-progress { background: var(--md-sys-color-primary); } -input[type=range]::-ms-fill-upper, -input[type=range]::-moz-range-track { +input[type='range']::-ms-fill-upper, +input[type='range']::-moz-range-track { background: var(--md-sys-color-shadow-light); } -input[type=range]::-ms-thumb { +input[type='range']::-ms-thumb { @include range-thumb; } //-------------------------------- -.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb { +.keyboard-focused input[type='range']:focus:not(.active)::-ms-thumb { box-shadow: 0 0 0 10px color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%); } diff --git a/components/slider/range.ts b/src/slider/range.ts similarity index 98% rename from components/slider/range.ts rename to src/slider/range.ts index 5a79c01e7d..7b62b398f2 100644 --- a/components/slider/range.ts +++ b/src/slider/range.ts @@ -1,13 +1,11 @@ -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface RangeOptions extends BaseOptions {} +interface RangeOptions extends BaseOptions {} const _defaults: RangeOptions = {}; -// TODO: !!!!! - -export class Range extends Component { +class Range extends Component { declare el: HTMLInputElement; private _mousedown: boolean; value: HTMLElement; @@ -214,3 +212,5 @@ export class Range extends Component { ); } } + +export { RangeOptions, Range }; diff --git a/src/snackbar/snackbar.test.ts b/src/snackbar/snackbar.test.ts new file mode 100644 index 0000000000..5ad2710668 --- /dev/null +++ b/src/snackbar/snackbar.test.ts @@ -0,0 +1,82 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Toast } from './toasts'; + +describe('Toasts:', () => { + beforeEach(() => { + vi.useFakeTimers(); + document.body.innerHTML = ''; + }); + + afterEach(() => { + Toast.dismissAll(); + Toast._removeContainer(); + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('Toast', () => { + it('displays and removes toast', () => { + const instance = new Toast({ + text: 'Test toast', + inDuration: 20, + displayLength: 100, + outDuration: 20 + }); + + const wrapperWasCreated = document.querySelectorAll('#toast-container').length === 1; + expect(wrapperWasCreated).toBe(true); + + const toast = instance.el; + + // Advance timers past inDuration animation trigger + vi.advanceTimersByTime(10); + expect(toast.getAttribute('role')).toBe('alert'); + expect(toast.getAttribute('aria-live')).toBe('assertive'); + expect(toast.getAttribute('aria-atomic')).toBe('true'); + expect(toast.innerText).toBe('Test toast'); + expect(document.body.contains(toast)).toBe(true); + + // Advance timers to middle of display duration + vi.advanceTimersByTime(30); + expect(document.body.contains(toast)).toBe(true); + + // Advance timers past displayLength + outDuration to trigger removal + vi.advanceTimersByTime(150); + expect(document.body.contains(toast)).toBe(false); + }); + + it('calls callback function when dismissed', () => { + let wasCalled = false; + const callback = () => { + wasCalled = true; + }; + + new Toast({ + text: 'I am a toast', + inDuration: 10, + displayLength: 50, + outDuration: 10, + completeCallback: callback + }); + + // Advance past total duration (50 + 10 = 60ms) + vi.advanceTimersByTime(100); + expect(wasCalled).toBe(true); + }); + + it('applies classes to toast', () => { + new Toast({ + text: 'Hi', + displayLength: 100, + inDuration: 10, + outDuration: 10, + classes: 'round flat' + }); + + vi.advanceTimersByTime(20); + const toastFlat = document.querySelectorAll('.toast.round.flat'); + expect(toastFlat.length).toBe(1); + }); + }); +}); diff --git a/components/snackbar/_toast.scss b/src/snackbar/toasts.css similarity index 54% rename from components/snackbar/_toast.scss rename to src/snackbar/toasts.css index 72d518ab6f..d2e0b39dfa 100644 --- a/components/snackbar/_toast.scss +++ b/src/snackbar/toasts.css @@ -1,21 +1,26 @@ -@use '../../sass/variables' as *; -@use '../../sass/global' as *; - #toast-container { - display:block; + display: block; position: fixed; z-index: 10000; +} - @media #{$small-and-down} { +@media only screen and (max-width: 600px) { + #toast-container { min-width: 100%; bottom: 0%; } - @media #{$medium-only} { +} + +@media only screen and (min-width: 601px) and (max-width: 992px) { + #toast-container { left: 5%; bottom: 7%; max-width: 90%; } - @media #{$large-and-up} { +} + +@media only screen and (min-width: 993px) { + #toast-container { top: 10%; right: 7%; max-width: 86%; @@ -23,7 +28,10 @@ } .toast { - @extend .z-depth-3; + box-shadow: + 0 8px 17px 2px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), + 0 5px 5px -3px rgba(0, 0, 0, 0.2); border-radius: 4px; top: 35px; width: auto; @@ -32,36 +40,32 @@ max-width: 100%; height: auto; min-height: 48px; - - //line-height: 1.5em; - //padding: 10px 25px; padding-left: 16px; padding-right: 12px; - font-size: 14px; font-weight: 500; line-height: 20px; - color: var(--md-sys-color-inverse-on-surface); background-color: var(--md-sys-color-inverse-surface); - display: flex; align-items: center; justify-content: space-between; cursor: default; +} - .toast-action { - color: var(--md-sys-color-inverse-primary); - font-weight: 500; - margin-right: -25px; - margin-left: 3rem; - } +.toast .toast-action { + color: var(--md-sys-color-inverse-primary); + font-weight: 500; + margin-right: -25px; + margin-left: 3rem; +} - &.rounded{ - border-radius: 24px; - } +.toast.rounded { + border-radius: 24px; +} - @media #{$small-and-down} { +@media only screen and (max-width: 600px) { + .toast { width: 100%; border-radius: 0; } diff --git a/components/snackbar/toasts.ts b/src/snackbar/toasts.ts similarity index 73% rename from components/snackbar/toasts.ts rename to src/snackbar/toasts.ts index 40b22c189c..6726d04c84 100644 --- a/components/snackbar/toasts.ts +++ b/src/snackbar/toasts.ts @@ -1,4 +1,4 @@ -import { BaseOptions } from '../../src/component'; +import { BaseOptions } from '../component'; export interface ToastOptions extends BaseOptions { /** @@ -7,7 +7,7 @@ export interface ToastOptions extends BaseOptions { */ text: string; /** - * Element Id for the tooltip. + * Element Id for the tooltip/template. * @default "" */ toastId?: string; @@ -35,9 +35,9 @@ export interface ToastOptions extends BaseOptions { * Callback function called when toast is dismissed. * @default null */ - completeCallback: () => void; + completeCallback?: (() => void) | null; /** - * The percentage of the toast's width it takes fora drag + * The percentage of the toast's width it takes for a drag * to dismiss a Toast. * @default 0.8 */ @@ -46,6 +46,7 @@ export interface ToastOptions extends BaseOptions { const _defaults: ToastOptions = { text: '', + toastId: '', displayLength: 4000, inDuration: 300, outDuration: 375, @@ -56,7 +57,7 @@ const _defaults: ToastOptions = { export class Toast { /** The toast element. */ - el: HTMLElement; + el!: HTMLElement; /** * The remaining amount of time in ms that the toast * will stay before dismissal. @@ -68,19 +69,19 @@ export class Toast { panning: boolean; options: ToastOptions; message: string; - counterInterval: NodeJS.Timeout | number; - wasSwiped: boolean; - startingXPos: number; - xPos: number; - time: number; - deltaX: number; - velocityX: number; - - static _toasts: Toast[]; - private static _container: HTMLElement | null; - static _draggedToast: Toast; - - constructor(options: Partial) { + counterInterval?: ReturnType; + wasSwiped: boolean = false; + startingXPos: number = 0; + xPos: number = 0; + time: number = 0; + deltaX: number = 0; + velocityX: number = 0; + + static _toasts: Toast[] = []; + private static _container: HTMLElement | null = null; + static _draggedToast: Toast | null = null; + + constructor(options?: Partial) { this.options = { ...Toast.defaults, ...options @@ -88,9 +89,11 @@ export class Toast { this.message = this.options.text; this.panning = false; this.timeRemaining = this.options.displayLength; + if (Toast._toasts.length === 0) { Toast._createContainer(); } + // Create new toast Toast._toasts.push(this); const toastElement = this._createToast(); @@ -111,13 +114,15 @@ export class Toast { static _createContainer() { const container = document.createElement('div'); container.setAttribute('id', 'toast-container'); - // Add event handler + + // Add event handlers container.addEventListener('touchstart', Toast._onDragStart); container.addEventListener('touchmove', Toast._onDragMove); container.addEventListener('touchend', Toast._onDragEnd); container.addEventListener('mousedown', Toast._onDragStart); document.addEventListener('mousemove', Toast._onDragMove); document.addEventListener('mouseup', Toast._onDragEnd); + document.body.appendChild(container); Toast._container = container; } @@ -132,9 +137,11 @@ export class Toast { } static _onDragStart(e: TouchEvent | MouseEvent) { - if (e.target && (e.target).closest('.toast')) { - const toastElem = (e.target).closest('.toast'); + if (e.target && (e.target as HTMLElement).closest('.toast')) { + const toastElem = (e.target as HTMLElement).closest('.toast') as HTMLElement; const toast: Toast = toastElem['M_Toast']; + if (!toast) return; + toast.panning = true; Toast._draggedToast = toast; toast.el.classList.add('panning'); @@ -146,23 +153,26 @@ export class Toast { } static _onDragMove(e: TouchEvent | MouseEvent) { - if (!!Toast._draggedToast) { + if (Toast._draggedToast) { e.preventDefault(); const toast = Toast._draggedToast; - toast.deltaX = Math.abs(toast.xPos - Toast._xPos(e)); - toast.xPos = Toast._xPos(e); - toast.velocityX = toast.deltaX / (Date.now() - toast.time); + const currentX = Toast._xPos(e); + + toast.deltaX = Math.abs(toast.xPos - currentX); + toast.xPos = currentX; + toast.velocityX = toast.deltaX / (Date.now() - toast.time || 1); toast.time = Date.now(); const totalDeltaX = toast.xPos - toast.startingXPos; const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; + toast.el.style.transform = `translateX(${totalDeltaX}px)`; toast.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString(); } } static _onDragEnd() { - if (!!Toast._draggedToast) { + if (Toast._draggedToast) { const toast = Toast._draggedToast; toast.panning = false; toast.el.classList.remove('panning'); @@ -171,12 +181,11 @@ export class Toast { const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || toast.velocityX > 1; - // Remove toast if (shouldBeDismissed) { toast.wasSwiped = true; toast.dismiss(); - // Animate toast back to original position } else { + // Animate toast back to original position toast.el.style.transition = 'transform .2s, opacity .2s'; toast.el.style.transform = ''; toast.el.style.opacity = ''; @@ -185,40 +194,58 @@ export class Toast { } } - static _xPos(e: TouchEvent | MouseEvent) { - if (e.type.startsWith('touch') && (e as TouchEvent).targetTouches.length >= 1) { - return (e as TouchEvent).targetTouches[0].clientX; + static _xPos(e: TouchEvent | MouseEvent): number { + if (e.type.startsWith('touch')) { + const touchEvent = e as TouchEvent; + if (touchEvent.targetTouches && touchEvent.targetTouches.length >= 1) { + return touchEvent.targetTouches[0].clientX; + } + if (touchEvent.changedTouches && touchEvent.changedTouches.length >= 1) { + return touchEvent.changedTouches[0].clientX; + } } - // mouse event return (e as MouseEvent).clientX; } /** - * dismiss all toasts. + * Dismiss all active toasts. */ static dismissAll() { - for (const toastIndex in Toast._toasts) { - Toast._toasts[toastIndex].dismiss(); + const toastsCopy = [...Toast._toasts]; + for (const toast of toastsCopy) { + toast.dismiss(); } } - _createToast() { + _createToast(): HTMLElement { let toast: HTMLElement = this.options.toastId - ? document.getElementById(this.options.toastId) + ? (document.getElementById(this.options.toastId) as HTMLElement) : document.createElement('div'); + if (toast instanceof HTMLTemplateElement) { - const node = (toast as HTMLTemplateElement).content.cloneNode(true); + const node = toast.content.cloneNode(true); toast = (node as HTMLElement).firstElementChild as HTMLElement; + } else if (this.options.toastId && toast) { + toast = toast.cloneNode(true) as HTMLElement; + toast.removeAttribute('id'); + } + + if (!toast) { + toast = document.createElement('div'); } + toast.classList.add('toast'); toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); + // Add custom classes onto toast if (this.options.classes.length > 0) { - toast.classList.add(...this.options.classes.split(' ')); + toast.classList.add(...this.options.classes.split(' ').filter(Boolean)); } + if (this.message) toast.innerText = this.message; + if (Toast._container) { Toast._container.appendChild(toast); } @@ -226,10 +253,8 @@ export class Toast { } _animateIn() { - // Animate toast in this.el.style.display = ''; this.el.style.opacity = '0'; - // easeOutCubic this.el.style.transition = ` top ${this.options.inDuration}ms ease, opacity ${this.options.inDuration}ms ease @@ -241,17 +266,14 @@ export class Toast { } /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. + * Create setInterval which automatically removes toast when timeRemaining <= 0 */ _setTimer() { if (this.timeRemaining !== Infinity) { this.counterInterval = setInterval(() => { - // If toast is not being dragged, decrease its time remaining if (!this.panning) { this.timeRemaining -= 20; } - // Animate toast out if (this.timeRemaining <= 0) { this.dismiss(); } @@ -263,7 +285,10 @@ export class Toast { * Dismiss toast with animation. */ dismiss() { - clearInterval(this.counterInterval); + if (this.counterInterval) { + clearInterval(this.counterInterval); + } + const activationDistance = this.el.offsetWidth * this.options.activationPercent; if (this.wasSwiped) { @@ -272,7 +297,6 @@ export class Toast { this.el.style.opacity = '0'; } - // easeOutExpo this.el.style.transition = ` margin ${this.options.outDuration}ms ease, opacity ${this.options.outDuration}ms ease`; @@ -287,13 +311,17 @@ export class Toast { if (typeof this.options.completeCallback === 'function') { this.options.completeCallback(); } - // Remove toast from DOM - if (this.el.id != this.options.toastId) { - this.el.remove(); - Toast._toasts.splice(Toast._toasts.indexOf(this), 1); - if (Toast._toasts.length === 0) { - Toast._removeContainer(); - } + + // Remove toast element and clean up internal state + this.el.remove(); + + const index = Toast._toasts.indexOf(this); + if (index !== -1) { + Toast._toasts.splice(index, 1); + } + + if (Toast._toasts.length === 0) { + Toast._removeContainer(); } }, this.options.outDuration); } diff --git a/src/switch/README.md b/src/switch/README.md new file mode 100644 index 0000000000..5b1367b71e --- /dev/null +++ b/src/switch/README.md @@ -0,0 +1,12 @@ +# Switch + +Create a switch + +```html +
    + +
    +``` diff --git a/src/switch/switch.css b/src/switch/switch.css new file mode 100644 index 0000000000..3667b0a7b8 --- /dev/null +++ b/src/switch/switch.css @@ -0,0 +1,118 @@ +.switch { + --track-height: 32px; + --track-width: 52px; + --border-width: 2px; + --size-off: 16px; + --size-on: 24px; + --icon-size: 16px; + --gap-on: calc(((var(--track-height) - var(--size-on)) / 2) - var(--border-width)); + --gap-off: calc(((var(--track-height) - var(--size-off)) / 2) - var(--border-width)); +} + +.switch, +.switch * { + -webkit-tap-highlight-color: transparent; + user-select: none; +} + +.switch label { + cursor: pointer; +} + +.switch label input[type='checkbox'] { + opacity: 0; + width: 0; + height: 0; +} + +/* Checked Track */ +.switch label input[type='checkbox']:checked + .lever { + background-color: var(--md-sys-color-primary); + border-color: var(--md-sys-color-primary); +} + +/* Checked Dot Dimensions & Position */ +.switch label input[type='checkbox']:checked + .lever:before, +.switch label input[type='checkbox']:checked + .lever:after { + top: var(--gap-on); + left: calc(var(--track-width) - var(--size-on) - var(--gap-on) - 2 * var(--border-width)); + width: var(--size-on); + height: var(--size-on); +} + +.switch label input[type='checkbox']:checked + .lever:after { + background-color: var(--md-sys-color-surface); +} + +.switch label .lever { + content: ''; + display: inline-block; + position: relative; + width: var(--track-width); + height: var(--track-height); + border-style: solid; + border-width: 2px; + border-color: var(--md-sys-color-outline); + background-color: var(--md-sys-color-surface-variant); + border-radius: 15px; + transition: background 0.3s ease; + vertical-align: middle; + margin: 0; +} + +/* Dot Pseudo-elements Base */ +.switch label .lever:before, +.switch label .lever:after { + content: ''; + position: absolute; + display: inline-block; + width: var(--size-off); + height: var(--size-off); + border-radius: 50%; + left: var(--gap-off); + top: var(--gap-off); + transition: + left 0.3s ease, + background 0.3s ease, + box-shadow 0.1s ease, + transform 0.1s ease; +} + +/* Unchecked Dot Specifics */ +.switch label .lever:after { + background-color: var(--md-sys-color-outline); + height: var(--size-off); + width: var(--size-off); +} + +/* Hover, Active, and Focus States */ +input[type='checkbox']:not(:disabled) ~ .lever:active:before, +input[type='checkbox']:not(:disabled):focus ~ .lever::before, +input[type='checkbox']:not(:disabled) ~ .lever:hover::before { + transform: scale(2.4); +} + +input[type='checkbox']:not(:disabled) ~ .lever:hover::before { + background-color: rgba(0, 0, 0, 0.04); +} + +input[type='checkbox']:not(:disabled) ~ .lever:active:before, +input[type='checkbox']:not(:disabled):focus ~ .lever::before { + background-color: rgba(0, 0, 0, 0.12); +} + +input[type='checkbox']:checked:not(:disabled) ~ .lever:hover::before { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 6%); +} + +/* Switch Active/Checked State */ +input[type='checkbox']:checked:not(:disabled) ~ .lever:active::before, +input[type='checkbox']:checked:not(:disabled):focus ~ .lever::before { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%); +} + +/* Disabled Styles */ +.switch input[type='checkbox'][disabled] + .lever { + cursor: default; + opacity: 0.5; +} diff --git a/src/switch/switch.mjs b/src/switch/switch.mjs new file mode 100644 index 0000000000..90d0934bf6 --- /dev/null +++ b/src/switch/switch.mjs @@ -0,0 +1,19 @@ +import { Component } from '../atomic/component.mjs'; + +class Switch extends Component { + constructor(options) { + super(options); + this.addClassname('switch'); + } + + toHTML() { + const html = ``; + this.setChildren(html); + return super.toHTML(); + } +} + +export { Switch }; diff --git a/src/tabs/README.md b/src/tabs/README.md new file mode 100644 index 0000000000..151ff55dbd --- /dev/null +++ b/src/tabs/README.md @@ -0,0 +1 @@ +# Tabs diff --git a/components/tabs/_tabs.scss b/src/tabs/_tabs.scss similarity index 100% rename from components/tabs/_tabs.scss rename to src/tabs/_tabs.scss diff --git a/components/tabs/tabs.mjs b/src/tabs/tabs.mjs similarity index 100% rename from components/tabs/tabs.mjs rename to src/tabs/tabs.mjs diff --git a/src/tabs/tabs.test.ts b/src/tabs/tabs.test.ts new file mode 100644 index 0000000000..031a940386 --- /dev/null +++ b/src/tabs/tabs.test.ts @@ -0,0 +1,195 @@ +// @vitest-environment happy-dom +import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; +import { Tabs } from './tabs.js'; + +// Helper utility for async delays +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('Tabs Plugin', () => { + const fixture = `
    + +
    Test 1
    +
    Test 2
    +
    Test 3
    +
    Test 4
    +
    Test 5
    +
    Test 6
    +
    Test 7
    +
    Test 8
    +
    `; + + let container: HTMLDivElement; + + beforeEach(() => { + // Mount fixture + container = document.createElement('div'); + container.innerHTML = fixture; + document.body.appendChild(container); + + const normalTabs = document.querySelector('.tabs.normal')!; + Tabs.init(normalTabs, { duration: 0 }); + window.location.hash = ''; + + // Force selection of test2 as active tab + Tabs.getInstance(normalTabs)?.select('test2'); + }); + + afterEach(() => { + const normalTabs = document.querySelector('.tabs.normal'); + if (normalTabs) { + Tabs.getInstance(normalTabs)?.destroy(); + } + document.body.innerHTML = ''; + }); + + describe('Tabs', () => { + it('should open to active tab', () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const tabLinks = normalTabs.querySelectorAll('.tab a'); + + tabLinks.forEach((tabLink) => { + const tabHash = tabLink.getAttribute('href')!; + const contentEl = document.querySelector(tabHash)!; + + if (tabHash === activeTabHash) { + expect(contentEl.style.display).not.toBe('none'); + } else { + expect(contentEl.style.display).toBe('none'); + } + }); + + const indicator = normalTabs.querySelector('.indicator'); + expect(indicator).not.toBeNull(); + }); + + it('should switch to clicked tab', async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const disabledTab = normalTabs.querySelector('.disabled a')!; + const disabledTabHash = disabledTab.getAttribute('href')!; + const firstTab = normalTabs.querySelector('.tab a')!; + const firstTabHash = firstTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator')!; + + expect(indicator).not.toBeNull(); + + // Click disabled tab + disabledTab.click(); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + const disabledContent = document.querySelector(disabledTabHash)!; + + expect(activeContent.style.display).not.toBe('none'); + expect(disabledContent.style.display).toBe('none'); + + // Click first tab + firstTab.click(); + await delay(10); + + const firstContent = document.querySelector(firstTabHash)!; + + expect(activeContent.style.display).toBe('none'); + expect(firstContent.style.display).not.toBe('none'); + expect(indicator.offsetLeft).toBe(firstTab.offsetLeft); + }); + + it("shouldn't hide active tab if clicked while active", async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator'); + + expect(indicator).not.toBeNull(); + + activeTab.click(); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + expect(activeContent.style.display).not.toBe('none'); + }); + + it('should horizontally scroll when too many tabs', () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const tabsInstance = Tabs.getInstance(normalTabs); + // Mock container visible width (400px) and total scroll width (800px) + vi.spyOn(normalTabs, 'getBoundingClientRect').mockReturnValue({ + width: 400, + height: 50, + top: 0, + left: 0, + bottom: 50, + right: 400, + x: 0, + y: 0, + toJSON: () => {} + }); + Object.defineProperty(normalTabs, 'scrollWidth', { + configurable: true, + value: 800 + }); + // Trigger recalculation + tabsInstance._setTabsAndTabWidth(); + // Verify internal width state + expect(normalTabs.scrollWidth).toBeGreaterThan(tabsInstance._tabsWidth); + expect(tabsInstance._tabWidth).toBe(100); // 800px scrollWidth / 8 tabs + }); + + it('should programmatically switch tabs', async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const firstTab = normalTabs.querySelector('li a')!; + const firstTabHash = firstTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator')!; + const tabs = normalTabs.querySelectorAll('.tab a'); + + tabs.forEach((tab) => { + const tabHash = tab.getAttribute('href')!; + const contentEl = document.querySelector(tabHash)!; + + if (tabHash === activeTabHash) { + expect(contentEl.style.display).not.toBe('none'); + } else { + expect(contentEl.style.display).toBe('none'); + } + }); + + Tabs.getInstance(normalTabs)?.select('test1'); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + const firstContent = document.querySelector(firstTabHash)!; + + expect(activeContent.style.display).toBe('none'); + expect(firstContent.style.display).not.toBe('none'); + expect(indicator.offsetLeft).toBe(firstTab.offsetLeft); + }); + + it("shouldn't error if tab has no associated content", async () => { + document.querySelector('#test8')?.remove(); + + const tabNoContent = document.querySelector('[href="#test8"]')!; + expect(tabNoContent.classList.contains('active')).toBe(false); + + tabNoContent.click(); + await delay(10); + + expect(tabNoContent.classList.contains('active')).toBe(true); + }); + }); +}); diff --git a/components/tabs/tabs.ts b/src/tabs/tabs.ts similarity index 66% rename from components/tabs/tabs.ts rename to src/tabs/tabs.ts index 47fa9afb02..8541f0cac9 100644 --- a/components/tabs/tabs.ts +++ b/src/tabs/tabs.ts @@ -1,7 +1,7 @@ import { Carousel } from '../carousel/carousel'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; -export interface TabsOptions extends BaseOptions { +interface TabsOptions extends BaseOptions { /** * Transition duration in milliseconds. * @default 300 @@ -21,7 +21,7 @@ export interface TabsOptions extends BaseOptions { /** * The maximum width of the screen, in pixels, * where the swipeable functionality initializes. - * @default infinity + * @default Infinity */ responsiveThreshold: number; } @@ -30,10 +30,10 @@ const _defaults: TabsOptions = { duration: 300, onShow: null, swipeable: false, - responsiveThreshold: Infinity // breakpoint for swipeable + responsiveThreshold: Infinity }; -export class Tabs extends Component { +class Tabs extends Component { _tabLinks: NodeListOf; _index: number; _indicator: HTMLLIElement; @@ -55,11 +55,13 @@ export class Tabs extends Component { this._tabLinks = this.el.querySelectorAll('li.tab > a'); this._index = 0; this._setupActiveTabLink(); + if (this.options.swipeable) { this._setupSwipeableTabs(); } else { this._setupNormalTabs(); } + // Setup tabs indicator after content to ensure accurate widths this._setTabsAndTabWidth(); this._createIndicator(); @@ -70,23 +72,8 @@ export class Tabs extends Component { return _defaults; } - /** - * Initializes instance of Tabs. - * @param el HTML element. - * @param options Component options. - */ static init(el: HTMLElement, options?: Partial): Tabs; - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ static init(els: InitElements, options?: Partial): Tabs[]; - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ static init( els: HTMLElement | InitElements, options: Partial = {} @@ -100,7 +87,9 @@ export class Tabs extends Component { destroy() { this._removeEventHandlers(); - this._indicator.parentNode.removeChild(this._indicator); + if (this._indicator?.parentNode) { + this._indicator.parentNode.removeChild(this._indicator); + } if (this.options.swipeable) { this._teardownSwipeableTabs(); } else { @@ -109,9 +98,6 @@ export class Tabs extends Component { this.el['M_Tabs'] = undefined; } - /** - * The index of tab that is currently shown. - */ get index() { return this._index; } @@ -119,23 +105,25 @@ export class Tabs extends Component { _setupEventHandlers() { window.addEventListener('resize', this._handleWindowResize); this.el.addEventListener('click', this._handleTabClick); - // map vertical scrolling to horizontal scrolling because no scrollbar is shown on desktop - this.el.addEventListener('wheel', (event) => { - if (event.deltaY !== 0) { - event.preventDefault(); - this.el.scrollLeft += event.deltaY * 1; - } - }); + this.el.addEventListener('wheel', this._handleWheel); } _removeEventHandlers() { window.removeEventListener('resize', this._handleWindowResize); this.el.removeEventListener('click', this._handleTabClick); + this.el.removeEventListener('wheel', this._handleWheel); } + _handleWheel = (event: WheelEvent) => { + if (event.deltaY !== 0) { + event.preventDefault(); + this.el.scrollLeft += event.deltaY; + } + }; + _handleWindowResize = () => { this._setTabsAndTabWidth(); - if (this._tabWidth !== 0 && this._tabsWidth !== 0) { + if (this._tabWidth !== 0 && this._tabsWidth !== 0 && this._activeTabLink) { this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; } @@ -151,49 +139,46 @@ export class Tabs extends Component { tab = tab.parentElement; } - // Handle click on tab link only - if (!tabLink || !tab.classList.contains('tab')) return; - // is disabled? + if (!tabLink || !tab?.classList.contains('tab')) return; if (tab.classList.contains('disabled')) { e.preventDefault(); return; } - // Act as regular link if target attribute is specified. if (tabLink.hasAttribute('target')) return; - // Make the old tab inactive. - this._activeTabLink.classList.remove('active'); + + this._activeTabLink?.classList.remove('active'); const _oldContent = this._content; - // Update the variables with the new link and content this._activeTabLink = tabLink; if (tabLink.hash) this._content = document.querySelector(tabLink.hash); this._tabLinks = this.el.querySelectorAll('li.tab > a'); - // Make the tab active + this._activeTabLink.classList.add('active'); const prevIndex = this._index; this._index = Math.max(Array.from(this._tabLinks).indexOf(tabLink), 0); - // Swap content if (this.options.swipeable) { if (this._tabsCarousel) { this._tabsCarousel.set(this._index, () => { - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } }); } } else { if (this._content) { this._content.style.display = 'block'; this._content.classList.add('active'); - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } if (_oldContent && _oldContent !== this._content) { _oldContent.style.display = 'none'; _oldContent.classList.remove('active'); } } } - // Update widths after content is swapped (scrollbar bugfix) + this._setTabsAndTabWidth(); this._animateIndicator(prevIndex); e.preventDefault(); @@ -204,16 +189,17 @@ export class Tabs extends Component { indicator.classList.add('indicator'); this.el.appendChild(indicator); this._indicator = indicator; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; + if (this._activeTabLink) { + this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; + this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; + } } _setupActiveTabLink() { - // If the location.hash matches one of the links, use that as the active tab. this._activeTabLink = Array.from(this._tabLinks).find( (a: HTMLAnchorElement) => a.getAttribute('href') === location.hash ); - // If no match is found, use the first link or any with class 'active' as the initial active tab. + if (!this._activeTabLink) { let activeTabLink = this.el.querySelector('li.tab a.active'); if (!activeTabLink) { @@ -221,43 +207,49 @@ export class Tabs extends Component { } this._activeTabLink = activeTabLink as HTMLAnchorElement; } + Array.from(this._tabLinks).forEach((a: HTMLAnchorElement) => a.classList.remove('active')); - this._activeTabLink.classList.add('active'); - this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); - if (this._activeTabLink && this._activeTabLink.hash) { - this._content = document.querySelector(this._activeTabLink.hash); - if (this._content) this._content.classList.add('active'); + if (this._activeTabLink) { + this._activeTabLink.classList.add('active'); + this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); + if (this._activeTabLink.hash) { + this._content = document.querySelector(this._activeTabLink.hash); + if (this._content) this._content.classList.add('active'); + } } } _setupSwipeableTabs() { - // Change swipeable according to responsive threshold - if (window.innerWidth > this.options.responsiveThreshold) this.options.swipeable = false; + if (window.innerWidth > this.options.responsiveThreshold) { + this.options.swipeable = false; + return; + } - const tabsContent = []; + const tabsContent: HTMLElement[] = []; this._tabLinks.forEach((a) => { if (a.hash) { - const currContent = document.querySelector(a.hash); - currContent.classList.add('carousel-item'); - tabsContent.push(currContent); + const currContent = document.querySelector(a.hash) as HTMLElement; + if (currContent) { + currContent.classList.add('carousel-item'); + tabsContent.push(currContent); + } } }); - // Create Carousel-Wrapper around Tab-Contents + if (tabsContent.length === 0) return; + const tabsWrapper = document.createElement('div'); tabsWrapper.classList.add('tabs-content', 'carousel', 'carousel-slider'); - // Wrap around tabsContent[0].parentElement.insertBefore(tabsWrapper, tabsContent[0]); tabsContent.forEach((tabContent) => { tabsWrapper.appendChild(tabContent); tabContent.style.display = ''; }); - // Keep active tab index to set initial carousel slide - const tab = this._activeTabLink.parentElement; - const activeTabIndex = Array.from(tab.parentNode.children).indexOf(tab); + const tab = this._activeTabLink?.parentElement; + const activeTabIndex = tab ? Array.from(tab.parentNode.children).indexOf(tab) : 0; this._tabsCarousel = Carousel.init(tabsWrapper, { fullWidth: true, @@ -265,39 +257,42 @@ export class Tabs extends Component { onCycleTo: (item) => { const prevIndex = this._index; this._index = Array.from(item.parentNode.children).indexOf(item); - this._activeTabLink.classList.remove('active'); + this._activeTabLink?.classList.remove('active'); this._activeTabLink = Array.from(this._tabLinks)[this._index]; - this._activeTabLink.classList.add('active'); + this._activeTabLink?.classList.add('active'); this._animateIndicator(prevIndex); - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } } }); - // Set initial carousel slide to active tab + this._tabsCarousel.set(activeTabIndex); } _teardownSwipeableTabs() { + if (!this._tabsCarousel) return; const tabsWrapper = this._tabsCarousel.el; this._tabsCarousel.destroy(); - // Unwrap - tabsWrapper.append(tabsWrapper.parentElement); + + // Move children back out to the parent container + while (tabsWrapper.firstChild) { + tabsWrapper.parentElement.insertBefore(tabsWrapper.firstChild, tabsWrapper); + } tabsWrapper.remove(); } _setupNormalTabs() { - // Hide Tabs Content Array.from(this._tabLinks).forEach((a) => { if (a === this._activeTabLink) return; - if ((a).hash) { - const currContent = document.querySelector((a).hash); - if (currContent) (currContent).style.display = 'none'; + if (a.hash) { + const currContent = document.querySelector(a.hash) as HTMLElement; + if (currContent) currContent.style.display = 'none'; } }); } _teardownNormalTabs() { - // show Tabs Content this._tabLinks.forEach((a) => { if (a.hash) { const currContent = document.querySelector(a.hash) as HTMLElement; @@ -308,7 +303,10 @@ export class Tabs extends Component { _setTabsAndTabWidth() { this._tabsWidth = this.el.getBoundingClientRect().width; - this._tabWidth = Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length; + this._tabWidth = + this._tabLinks.length > 0 + ? Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length + : 0; } _calcRightPos(el: HTMLElement) { @@ -319,24 +317,21 @@ export class Tabs extends Component { return Math.floor(el.offsetLeft); } - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ updateTabIndicator() { this._setTabsAndTabWidth(); this._animateIndicator(this._index); } _animateIndicator(prevIndex: number) { - let leftDelay = 0, - rightDelay = 0; + if (!this._indicator || !this._activeTabLink) return; + + let leftDelay = 0; + let rightDelay = 0; const isMovingLeftOrStaying = this._index - prevIndex >= 0; if (isMovingLeftOrStaying) leftDelay = 90; else rightDelay = 90; - // in v1: easeOutQuad this._indicator.style.transition = ` left ${this.options.duration}ms ease-out ${leftDelay}ms, right ${this.options.duration}ms ease-out ${rightDelay}ms`; @@ -345,14 +340,12 @@ export class Tabs extends Component { this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; } - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ select(tabId: string) { const tab = Array.from(this._tabLinks).find( (a: HTMLAnchorElement) => a.getAttribute('href') === '#' + tabId ); - if (tab) (tab).click(); + if (tab) tab.click(); } } + +export { Tabs, TabsOptions }; diff --git a/src/tapTarget.ts b/src/tapTarget.ts deleted file mode 100644 index 6aabed3a1d..0000000000 --- a/src/tapTarget.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { Utils } from './utils'; -import { Component, BaseOptions, InitElements, MElement, Openable } from './component'; - -export interface TapTargetOptions extends BaseOptions { - /** - * Callback function called when Tap Target is opened. - * @default null - */ - onOpen: (origin: HTMLElement) => void; - /** - * Callback function called when Tap Target is closed. - * @default null - */ - onClose: (origin: HTMLElement) => void; -} - -const _defaults: TapTargetOptions = { - onOpen: null, - onClose: null -}; - -export class TapTarget extends Component implements Openable { - /** - * If the tap target is open. - */ - isOpen: boolean; - - static _taptargets: TapTarget[]; - private wrapper: HTMLElement; - // private _origin: HTMLElement; - private originEl: HTMLElement; - private waveEl: HTMLElement & Element & Node; - private contentEl: HTMLElement; - - constructor(el: HTMLElement, options: Partial) { - super(el, options, TapTarget); - this.el['M_TapTarget'] = this; - - this.options = { - ...TapTarget.defaults, - ...options - }; - - this.isOpen = false; - // setup - this.originEl = document.querySelector(`#${el.dataset.target}`); - this.originEl.tabIndex = 0; - - this._setup(); - this._calculatePositioning(); - this._setupEventHandlers(); - - TapTarget._taptargets.push(this); - } - - static get defaults(): TapTargetOptions { - return _defaults; - } - - /** - * Initializes instance of TapTarget. - * @param el HTML element. - * @param options Component options. - */ - static init(el: HTMLElement, options?: Partial): TapTarget; - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init(els: InitElements, options?: Partial): TapTarget[]; - /** - * Initializes instances of TapTarget. - * @param els HTML elements. - * @param options Component options. - */ - static init( - els: HTMLElement | InitElements, - options: Partial = {} - ): TapTarget | TapTarget[] { - return super.init(els, options, TapTarget); - } - - static getInstance(el: HTMLElement): TapTarget { - return el['M_TapTarget']; - } - - destroy() { - this._removeEventHandlers(); - this.el['M_TapTarget'] = undefined; - const index = TapTarget._taptargets.indexOf(this); - if (index >= 0) { - TapTarget._taptargets.splice(index, 1); - } - } - - _setupEventHandlers() { - this.originEl.addEventListener('click', this._handleTargetToggle); - this.originEl.addEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.addEventListener('click', this._handleOriginClick); - // Resize - window.addEventListener('resize', this._handleThrottledResize); - } - - _removeEventHandlers() { - this.originEl.removeEventListener('click', this._handleTargetToggle); - this.originEl.removeEventListener('keypress', this._handleKeyboardInteraction, true); - // this.originEl.removeEventListener('click', this._handleOriginClick); - window.removeEventListener('resize', this._handleThrottledResize); - } - - _handleThrottledResize = (): void => Utils.throttle(this._handleResize, 200).bind(this); - - _handleKeyboardInteraction = (e: KeyboardEvent) => { - if (Utils.keys.ENTER.includes(e.key)) { - this._handleTargetToggle(); - } - }; - - _handleTargetToggle = () => { - if (!this.isOpen) this.open(); - else this.close(); - }; - - /*_handleOriginClick = () => { - this.close(); - }*/ - - _handleResize = () => { - this._calculatePositioning(); - }; - - _handleDocumentClick = (e: MouseEvent | TouchEvent | KeyboardEvent) => { - if ( - (e.target as HTMLElement).closest(`#${this.el.dataset.target}`) !== this.originEl && - !(e.target as HTMLElement).closest('.tap-target-wrapper') - ) { - this.close(); - // e.preventDefault(); - // e.stopPropagation(); - } - }; - - _setup() { - // Creating tap target - this.wrapper = this.el.parentElement; - this.waveEl = this.wrapper.querySelector('.tap-target-wave'); - this.el.parentElement.ariaExpanded = 'false'; - this.originEl.style.zIndex = '1002'; - // this.originEl = this.wrapper.querySelector('.tap-target-origin'); - this.contentEl = this.el.querySelector('.tap-target-content'); - // Creating wrapper - if (!this.wrapper.classList.contains('.tap-target-wrapper')) { - this.wrapper = document.createElement('div'); - this.wrapper.classList.add('tap-target-wrapper'); - this.el.before(this.wrapper); - this.wrapper.append(this.el); - } - // Creating content - if (!this.contentEl) { - this.contentEl = document.createElement('div'); - this.contentEl.classList.add('tap-target-content'); - this.el.append(this.contentEl); - } - // Creating foreground wave - if (!this.waveEl) { - this.waveEl = document.createElement('div'); - this.waveEl.classList.add('tap-target-wave'); - // Creating origin - /*if (!this.originEl) { - this.originEl = this._origin.cloneNode(true); // .clone(true, true); - this.originEl.classList.add('tap-target-origin'); - this.originEl.removeAttribute('id'); - this.originEl.removeAttribute('style'); - this.waveEl.append(this.originEl); - }*/ - this.wrapper.append(this.waveEl); - } - } - - private _offset(el: HTMLElement) { - const box = el.getBoundingClientRect(); - const docElem = document.documentElement; - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; - } - - _calculatePositioning() { - // Element or parent is fixed position? - let isFixed = getComputedStyle(this.originEl).position === 'fixed'; - if (!isFixed) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let currentElem: any = this.originEl; - const parents = []; - while ((currentElem = currentElem.parentNode) && currentElem !== document) - parents.push(currentElem); - - for (let i = 0; i < parents.length; i++) { - isFixed = getComputedStyle(parents[i]).position === 'fixed'; - if (isFixed) break; - } - } - // Calculating origin - const originWidth = this.originEl.offsetWidth; - const originHeight = this.originEl.offsetHeight; - const originTop = isFixed - ? this._offset(this.originEl).top - Utils.getDocumentScrollTop() - : this._offset(this.originEl).top; - const originLeft = isFixed - ? this._offset(this.originEl).left - Utils.getDocumentScrollLeft() - : this._offset(this.originEl).left; - - // Calculating screen - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - const scrollBarWidth = windowWidth - document.documentElement.clientWidth; - const centerX = windowWidth / 2; - const centerY = windowHeight / 2; - const isLeft = originLeft <= centerX; - const isRight = originLeft > centerX; - const isTop = originTop <= centerY; - const isBottom = originTop > centerY; - const isCenterX = originLeft >= windowWidth * 0.25 && originLeft <= windowWidth * 0.75; - - // Calculating tap target - const tapTargetWidth = this.el.offsetWidth; - const tapTargetHeight = this.el.offsetHeight; - const tapTargetTop = originTop + originHeight / 2 - tapTargetHeight / 2; - const tapTargetLeft = originLeft + originWidth / 2 - tapTargetWidth / 2; - const tapTargetPosition = isFixed ? 'fixed' : 'absolute'; - - // Calculating content - const tapTargetTextWidth = isCenterX ? tapTargetWidth : tapTargetWidth / 2 + originWidth; - const tapTargetTextHeight = tapTargetHeight / 2; - const tapTargetTextTop = isTop ? tapTargetHeight / 2 : 0; - const tapTargetTextBottom = 0; - const tapTargetTextLeft = isLeft && !isCenterX ? tapTargetWidth / 2 - originWidth : 0; - const tapTargetTextRight = 0; - const tapTargetTextPadding = originWidth; - const tapTargetTextAlign = isBottom ? 'bottom' : 'top'; - - // Calculating wave - const tapTargetWaveWidth = originWidth > originHeight ? originWidth * 2 : originWidth * 2; - const tapTargetWaveHeight = tapTargetWaveWidth; - const tapTargetWaveTop = tapTargetHeight / 2 - tapTargetWaveHeight / 2; - const tapTargetWaveLeft = tapTargetWidth / 2 - tapTargetWaveWidth / 2; - - // Setting tap target - this.wrapper.style.top = isTop ? tapTargetTop + 'px' : ''; - this.wrapper.style.right = isRight - ? windowWidth - tapTargetLeft - tapTargetWidth - scrollBarWidth + 'px' - : ''; - this.wrapper.style.bottom = isBottom - ? windowHeight - tapTargetTop - tapTargetHeight + 'px' - : ''; - this.wrapper.style.left = isLeft ? tapTargetLeft + 'px' : ''; - this.wrapper.style.position = tapTargetPosition; - - // Setting content - this.contentEl.style.width = tapTargetTextWidth + 'px'; - this.contentEl.style.height = tapTargetTextHeight + 'px'; - this.contentEl.style.top = tapTargetTextTop + 'px'; - this.contentEl.style.right = tapTargetTextRight + 'px'; - this.contentEl.style.bottom = tapTargetTextBottom + 'px'; - this.contentEl.style.left = tapTargetTextLeft + 'px'; - this.contentEl.style.padding = tapTargetTextPadding + 'px'; - this.contentEl.style.verticalAlign = tapTargetTextAlign; - - // Setting wave - this.waveEl.style.top = tapTargetWaveTop + 'px'; - this.waveEl.style.left = tapTargetWaveLeft + 'px'; - this.waveEl.style.width = tapTargetWaveWidth + 'px'; - this.waveEl.style.height = tapTargetWaveHeight + 'px'; - } - - /** - * Open Tap Target. - */ - open = () => { - if (this.isOpen) return; - // onOpen callback - if (typeof this.options.onOpen === 'function') { - this.options.onOpen.call(this, this.originEl); - } - this.isOpen = true; - this.wrapper.classList.add('open'); - this.wrapper.ariaExpanded = 'true'; - document.body.addEventListener('click', this._handleDocumentClick, true); - document.body.addEventListener('keypress', this._handleDocumentClick, true); - document.body.addEventListener('touchend', this._handleDocumentClick); - }; - - /** - * Close Tap Target. - */ - close = () => { - if (!this.isOpen) return; - // onClose callback - if (typeof this.options.onClose === 'function') { - this.options.onClose.call(this, this.originEl); - } - this.isOpen = false; - this.wrapper.classList.remove('open'); - this.wrapper.ariaExpanded = 'false'; - document.body.removeEventListener('click', this._handleDocumentClick, true); - document.body.removeEventListener('keypress', this._handleDocumentClick, true); - document.body.removeEventListener('touchend', this._handleDocumentClick); - }; - - static { - TapTarget._taptargets = []; - } -} diff --git a/src/textfield/README.md b/src/textfield/README.md new file mode 100644 index 0000000000..019060c6d9 --- /dev/null +++ b/src/textfield/README.md @@ -0,0 +1,28 @@ +new approach for floating labels aka "form-fields" (css only) + +- also use for selects, autocomplete, etc. + +```html +
    + +
    + Given Name + + +
    + + +
    + Family Name + + +
    + + +
    + Birthmonth + + +
    +
    +``` diff --git a/components/textfield/selectSpec.js b/src/textfield/___selectSpec.js similarity index 100% rename from components/textfield/selectSpec.js rename to src/textfield/___selectSpec.js diff --git a/components/textfield/_file-input.scss b/src/textfield/_file-input.scss similarity index 100% rename from components/textfield/_file-input.scss rename to src/textfield/_file-input.scss diff --git a/components/textfield/_input-fields.scss b/src/textfield/_input-fields.scss similarity index 100% rename from components/textfield/_input-fields.scss rename to src/textfield/_input-fields.scss diff --git a/components/textfield/_select.scss b/src/textfield/_select.scss similarity index 100% rename from components/textfield/_select.scss rename to src/textfield/_select.scss diff --git a/src/characterCounter.ts b/src/textfield/characterCounter.ts similarity index 94% rename from src/characterCounter.ts rename to src/textfield/characterCounter.ts index c238c4fabe..5253efba8d 100644 --- a/src/characterCounter.ts +++ b/src/textfield/characterCounter.ts @@ -1,12 +1,12 @@ -import { Component, InitElements, MElement } from './component'; +import { Component, InitElements, MElement } from '../component'; -export interface BaseOptions {} +interface BaseOptions {} const _defaults = Object.freeze({}); type InputElement = HTMLInputElement | HTMLTextAreaElement; -export class CharacterCounter extends Component { +class CharacterCounter extends Component { declare el: InputElement; /** Stores the reference to the counter HTML element. */ counterEl: HTMLSpanElement; @@ -119,3 +119,5 @@ export class CharacterCounter extends Component { } } } + +export { BaseOptions, CharacterCounter }; diff --git a/components/textfield/formfields.css b/src/textfield/formfields.css similarity index 86% rename from components/textfield/formfields.css rename to src/textfield/formfields.css index b60244d126..2f4f5cffd8 100644 --- a/components/textfield/formfields.css +++ b/src/textfield/formfields.css @@ -1,29 +1,3 @@ -/** -new approach for floating labels aka "form-fields" (css only) -- also use for selects, autocomplete, etc. - -
    - -
    - Given Name - - -
    - -
    - Family Name - - -
    - -
    - Birthmonth - - -
    -
    -*/ - :root { --mw-font-size: 16px; --mw-border-radius: 8px; @@ -74,7 +48,6 @@ fieldset.form-field legend { margin: 0; margin-left: calc(var(--mw-padding-left) - var(--mw-label-padding) - var(--mw-border-width)); cursor: text; - /* color: transparent; */ user-select: none; } fieldset.form-field input, @@ -95,19 +68,12 @@ fieldset.form-field select { line-height: normal; outline: none; background: none; - /* background-color: #0f05 !important; */ } fieldset.form-field select { padding: 0; - /* margin: 0; */ padding-left: calc(var(--mw-padding-left) - var(--mw-label-padding)); - /* background-color: #f005 !important; */ z-index: 2; } -fieldset.form-field select option { - background-color: black; - color: white; -} fieldset.form-field input::placeholder { color: var(--mw-idle-color); font-size: var(--mw-font-size); @@ -135,15 +101,20 @@ fieldset.form-field.animated legend::before { content: ''; position: absolute; display: block; - width: calc(100% - var(--mw-border-radius)); - right: calc(0.5 * var(--mw-border-radius)); - height: 0; - border-top: var(--mw-border-width) solid var(--mw-idle-color); + width: 100%; + height: var(--mw-border-width); + left: 0; + background-color: var(--mw-idle-color); + /* Replace top calc with relative centering */ + top: 50%; + transform: translateY(-50%); align-self: center; transition: width calc(var(--mw-inputlabel-duration) * 0.8); } + fieldset.form-field.animated legend { color: transparent; + position: relative; } fieldset.form-field.animated input::placeholder { opacity: 1; @@ -177,7 +148,8 @@ fieldset.form-field:hover { } fieldset.form-field:hover legend::before { border-width: var(--mw-hover-border-width); - border-color: var(--mw-hover-color); + /*border-color: var(--mw-hover-color);*/ + background-color: var(--mw-hover-color); } fieldset.form-field:hover legend { color: var(--mw-hover-color); diff --git a/components/textfield/forms.ts b/src/textfield/forms.ts similarity index 99% rename from components/textfield/forms.ts rename to src/textfield/forms.ts index 2ea9a5cf65..99ef880905 100644 --- a/components/textfield/forms.ts +++ b/src/textfield/forms.ts @@ -1,4 +1,4 @@ -import { Utils } from '../../src/utils'; +import { Utils } from '../utils'; export class Forms { /** diff --git a/components/textfield/select.ts b/src/textfield/select.ts similarity index 99% rename from components/textfield/select.ts rename to src/textfield/select.ts index 8d03bd31a5..43f8e89907 100644 --- a/components/textfield/select.ts +++ b/src/textfield/select.ts @@ -1,6 +1,6 @@ -import { Utils } from '../../src/utils'; +import { Utils } from '../utils'; import { Dropdown, DropdownOptions } from '../dropdown/dropdown'; -import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; +import { Component, BaseOptions, InitElements, MElement } from '../component'; export interface FormSelectOptions extends BaseOptions { /** diff --git a/components/textfield/textfield.mjs b/src/textfield/textfield.mjs similarity index 56% rename from components/textfield/textfield.mjs rename to src/textfield/textfield.mjs index 2b18faff09..837a2cb064 100644 --- a/components/textfield/textfield.mjs +++ b/src/textfield/textfield.mjs @@ -2,11 +2,14 @@ import { Component } from '../atomic/component.mjs'; class TextField extends Component { #labelText; - #isReadOnly = false; - #isRequired = false; + #isReadOnly; + #isRequired; constructor(options) { super(options); + this.#labelText = ''; + this.#isRequired = false; + this.#isReadOnly = false; this.setTagName('fieldset'); this.addClassname('form-field animated'); } @@ -17,10 +20,11 @@ class TextField extends Component { } toHTML() { - const elemId = 'elem-' + 100000 * Math.random(); - const html = `${this.#labelText} + const elemId = 'textfield-' + 100000 * Math.random(); + const hasLabel = this.#labelText !== ''; + const html = `${hasLabel ? `${this.#labelText}` : ''} - `; + ${hasLabel ? `` : ''}`; this.setChildren(html); return super.toHTML(); } diff --git a/components/textfield/textfield.test.mjs b/src/textfield/textfield.test.mjs similarity index 100% rename from components/textfield/textfield.test.mjs rename to src/textfield/textfield.test.mjs diff --git a/src/timepicker/README.md b/src/timepicker/README.md new file mode 100644 index 0000000000..4fb7729536 --- /dev/null +++ b/src/timepicker/README.md @@ -0,0 +1 @@ +# Timepicker diff --git a/components/timepicker/_timepicker.scss b/src/timepicker/_timepicker.scss similarity index 100% rename from components/timepicker/_timepicker.scss rename to src/timepicker/_timepicker.scss diff --git a/components/timepicker/timepicker.ts b/src/timepicker/timepicker.ts similarity index 99% rename from components/timepicker/timepicker.ts rename to src/timepicker/timepicker.ts index c1acece66f..91bf877aa3 100644 --- a/components/timepicker/timepicker.ts +++ b/src/timepicker/timepicker.ts @@ -1,7 +1,7 @@ -import { Utils } from '../../src/utils'; -import { Component, BaseOptions, InitElements, MElement, I18nOptions } from '../../src/component'; -import { DockedDisplayPlugin } from '../../src/dockedDisplayPlugin'; -import { ModalDisplayPlugin } from '../../src/modalDisplayPlugin'; +import { Utils } from '../utils'; +import { Component, BaseOptions, InitElements, MElement, I18nOptions } from '../component'; +//import { DockedDisplayPlugin } from '../__dockedDisplayPlugin'; +//import { ModalDisplayPlugin } from '../__modalDisplayPlugin'; export type Views = 'hours' | 'minutes'; @@ -178,7 +178,7 @@ export class Timepicker extends Component { g: Element; toggleViewTimer: string | number | NodeJS.Timeout; vibrateTimer: NodeJS.Timeout | number; - private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; + // private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; constructor(el: HTMLInputElement, options: Partial) { super(el, options, Timepicker); diff --git a/src/tooltip/README.md b/src/tooltip/README.md new file mode 100644 index 0000000000..2e4d9bc2ea --- /dev/null +++ b/src/tooltip/README.md @@ -0,0 +1 @@ +# Tooltip diff --git a/components/tooltip/_tooltip.scss b/src/tooltip/___del_tooltip.scss similarity index 100% rename from components/tooltip/_tooltip.scss rename to src/tooltip/___del_tooltip.scss diff --git a/src/tooltip/__tapTarget.ts b/src/tooltip/__tapTarget.ts new file mode 100644 index 0000000000..0594d1cbef --- /dev/null +++ b/src/tooltip/__tapTarget.ts @@ -0,0 +1,319 @@ +//############ REPLACED WITH RICH-TOOLTIPS! + +// import { Utils } from '../../src/utils'; +// import { Component, BaseOptions, InitElements, MElement, Openable } from '../../src/component'; + +// interface TapTargetOptions extends BaseOptions { +// /** +// * Callback function called when Tap Target is opened. +// * @default null +// */ +// onOpen: (origin: HTMLElement) => void; +// /** +// * Callback function called when Tap Target is closed. +// * @default null +// */ +// onClose: (origin: HTMLElement) => void; +// } + +// const _defaults: TapTargetOptions = { +// onOpen: null, +// onClose: null +// }; + +// class TapTarget extends Component implements Openable { +// /** +// * If the tap target is open. +// */ +// isOpen: boolean; + +// static _taptargets: TapTarget[]; +// private wrapper: HTMLElement; +// // private _origin: HTMLElement; +// private originEl: HTMLElement; +// private waveEl: HTMLElement & Element & Node; +// private contentEl: HTMLElement; + +// constructor(el: HTMLElement, options: Partial) { +// super(el, options, TapTarget); +// this.el['M_TapTarget'] = this; + +// this.options = { +// ...TapTarget.defaults, +// ...options +// }; + +// this.isOpen = false; +// // setup +// this.originEl = document.querySelector(`#${el.dataset.target}`); +// this.originEl.tabIndex = 0; + +// this._setup(); +// this._calculatePositioning(); +// this._setupEventHandlers(); + +// TapTarget._taptargets.push(this); +// } + +// static get defaults(): TapTargetOptions { +// return _defaults; +// } + +// /** +// * Initializes instance of TapTarget. +// * @param el HTML element. +// * @param options Component options. +// */ +// static init(el: HTMLElement, options?: Partial): TapTarget; +// /** +// * Initializes instances of TapTarget. +// * @param els HTML elements. +// * @param options Component options. +// */ +// static init(els: InitElements, options?: Partial): TapTarget[]; +// /** +// * Initializes instances of TapTarget. +// * @param els HTML elements. +// * @param options Component options. +// */ +// static init( +// els: HTMLElement | InitElements, +// options: Partial = {} +// ): TapTarget | TapTarget[] { +// return super.init(els, options, TapTarget); +// } + +// static getInstance(el: HTMLElement): TapTarget { +// return el['M_TapTarget']; +// } + +// destroy() { +// this._removeEventHandlers(); +// this.el['M_TapTarget'] = undefined; +// const index = TapTarget._taptargets.indexOf(this); +// if (index >= 0) { +// TapTarget._taptargets.splice(index, 1); +// } +// } + +// _setupEventHandlers() { +// this.originEl.addEventListener('click', this._handleTargetToggle); +// this.originEl.addEventListener('keypress', this._handleKeyboardInteraction, true); +// // this.originEl.addEventListener('click', this._handleOriginClick); +// // Resize +// window.addEventListener('resize', this._handleThrottledResize); +// } + +// _removeEventHandlers() { +// this.originEl.removeEventListener('click', this._handleTargetToggle); +// this.originEl.removeEventListener('keypress', this._handleKeyboardInteraction, true); +// // this.originEl.removeEventListener('click', this._handleOriginClick); +// window.removeEventListener('resize', this._handleThrottledResize); +// } + +// _handleThrottledResize = (): void => Utils.throttle(this._handleResize, 200).bind(this); + +// _handleKeyboardInteraction = (e: KeyboardEvent) => { +// if (Utils.keys.ENTER.includes(e.key)) { +// this._handleTargetToggle(); +// } +// }; + +// _handleTargetToggle = () => { +// if (!this.isOpen) this.open(); +// else this.close(); +// }; + +// /*_handleOriginClick = () => { +// this.close(); +// }*/ + +// _handleResize = () => { +// this._calculatePositioning(); +// }; + +// _handleDocumentClick = (e: MouseEvent | TouchEvent | KeyboardEvent) => { +// if ( +// (e.target as HTMLElement).closest(`#${this.el.dataset.target}`) !== this.originEl && +// !(e.target as HTMLElement).closest('.tap-target-wrapper') +// ) { +// this.close(); +// // e.preventDefault(); +// // e.stopPropagation(); +// } +// }; + +// _setup() { +// // Creating tap target +// this.wrapper = this.el.parentElement; +// this.waveEl = this.wrapper.querySelector('.tap-target-wave'); +// this.el.parentElement.ariaExpanded = 'false'; +// this.originEl.style.zIndex = '1002'; +// // this.originEl = this.wrapper.querySelector('.tap-target-origin'); +// this.contentEl = this.el.querySelector('.tap-target-content'); +// // Creating wrapper +// if (!this.wrapper.classList.contains('.tap-target-wrapper')) { +// this.wrapper = document.createElement('div'); +// this.wrapper.classList.add('tap-target-wrapper'); +// this.el.before(this.wrapper); +// this.wrapper.append(this.el); +// } +// // Creating content +// if (!this.contentEl) { +// this.contentEl = document.createElement('div'); +// this.contentEl.classList.add('tap-target-content'); +// this.el.append(this.contentEl); +// } +// // Creating foreground wave +// if (!this.waveEl) { +// this.waveEl = document.createElement('div'); +// this.waveEl.classList.add('tap-target-wave'); +// // Creating origin +// /*if (!this.originEl) { +// this.originEl = this._origin.cloneNode(true); // .clone(true, true); +// this.originEl.classList.add('tap-target-origin'); +// this.originEl.removeAttribute('id'); +// this.originEl.removeAttribute('style'); +// this.waveEl.append(this.originEl); +// }*/ +// this.wrapper.append(this.waveEl); +// } +// } + +// private _offset(el: HTMLElement) { +// const box = el.getBoundingClientRect(); +// const docElem = document.documentElement; +// return { +// top: box.top + window.pageYOffset - docElem.clientTop, +// left: box.left + window.pageXOffset - docElem.clientLeft +// }; +// } + +// _calculatePositioning() { +// // Element or parent is fixed position? +// let isFixed = getComputedStyle(this.originEl).position === 'fixed'; +// if (!isFixed) { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// let currentElem: any = this.originEl; +// const parents = []; +// while ((currentElem = currentElem.parentNode) && currentElem !== document) +// parents.push(currentElem); + +// for (let i = 0; i < parents.length; i++) { +// isFixed = getComputedStyle(parents[i]).position === 'fixed'; +// if (isFixed) break; +// } +// } +// // Calculating origin +// const originWidth = this.originEl.offsetWidth; +// const originHeight = this.originEl.offsetHeight; +// const originTop = isFixed +// ? this._offset(this.originEl).top - Utils.getDocumentScrollTop() +// : this._offset(this.originEl).top; +// const originLeft = isFixed +// ? this._offset(this.originEl).left - Utils.getDocumentScrollLeft() +// : this._offset(this.originEl).left; + +// // Calculating screen +// const windowWidth = window.innerWidth; +// const windowHeight = window.innerHeight; +// const scrollBarWidth = windowWidth - document.documentElement.clientWidth; +// const centerX = windowWidth / 2; +// const centerY = windowHeight / 2; +// const isLeft = originLeft <= centerX; +// const isRight = originLeft > centerX; +// const isTop = originTop <= centerY; +// const isBottom = originTop > centerY; +// const isCenterX = originLeft >= windowWidth * 0.25 && originLeft <= windowWidth * 0.75; + +// // Calculating tap target +// const tapTargetWidth = this.el.offsetWidth; +// const tapTargetHeight = this.el.offsetHeight; +// const tapTargetTop = originTop + originHeight / 2 - tapTargetHeight / 2; +// const tapTargetLeft = originLeft + originWidth / 2 - tapTargetWidth / 2; +// const tapTargetPosition = isFixed ? 'fixed' : 'absolute'; + +// // Calculating content +// const tapTargetTextWidth = isCenterX ? tapTargetWidth : tapTargetWidth / 2 + originWidth; +// const tapTargetTextHeight = tapTargetHeight / 2; +// const tapTargetTextTop = isTop ? tapTargetHeight / 2 : 0; +// const tapTargetTextBottom = 0; +// const tapTargetTextLeft = isLeft && !isCenterX ? tapTargetWidth / 2 - originWidth : 0; +// const tapTargetTextRight = 0; +// const tapTargetTextPadding = originWidth; +// const tapTargetTextAlign = isBottom ? 'bottom' : 'top'; + +// // Calculating wave +// const tapTargetWaveWidth = originWidth > originHeight ? originWidth * 2 : originWidth * 2; +// const tapTargetWaveHeight = tapTargetWaveWidth; +// const tapTargetWaveTop = tapTargetHeight / 2 - tapTargetWaveHeight / 2; +// const tapTargetWaveLeft = tapTargetWidth / 2 - tapTargetWaveWidth / 2; + +// // Setting tap target +// this.wrapper.style.top = isTop ? tapTargetTop + 'px' : ''; +// this.wrapper.style.right = isRight +// ? windowWidth - tapTargetLeft - tapTargetWidth - scrollBarWidth + 'px' +// : ''; +// this.wrapper.style.bottom = isBottom +// ? windowHeight - tapTargetTop - tapTargetHeight + 'px' +// : ''; +// this.wrapper.style.left = isLeft ? tapTargetLeft + 'px' : ''; +// this.wrapper.style.position = tapTargetPosition; + +// // Setting content +// this.contentEl.style.width = tapTargetTextWidth + 'px'; +// this.contentEl.style.height = tapTargetTextHeight + 'px'; +// this.contentEl.style.top = tapTargetTextTop + 'px'; +// this.contentEl.style.right = tapTargetTextRight + 'px'; +// this.contentEl.style.bottom = tapTargetTextBottom + 'px'; +// this.contentEl.style.left = tapTargetTextLeft + 'px'; +// this.contentEl.style.padding = tapTargetTextPadding + 'px'; +// this.contentEl.style.verticalAlign = tapTargetTextAlign; + +// // Setting wave +// this.waveEl.style.top = tapTargetWaveTop + 'px'; +// this.waveEl.style.left = tapTargetWaveLeft + 'px'; +// this.waveEl.style.width = tapTargetWaveWidth + 'px'; +// this.waveEl.style.height = tapTargetWaveHeight + 'px'; +// } + +// /** +// * Open Tap Target. +// */ +// open = () => { +// if (this.isOpen) return; +// // onOpen callback +// if (typeof this.options.onOpen === 'function') { +// this.options.onOpen.call(this, this.originEl); +// } +// this.isOpen = true; +// this.wrapper.classList.add('open'); +// this.wrapper.ariaExpanded = 'true'; +// document.body.addEventListener('click', this._handleDocumentClick, true); +// document.body.addEventListener('keypress', this._handleDocumentClick, true); +// document.body.addEventListener('touchend', this._handleDocumentClick); +// }; + +// /** +// * Close Tap Target. +// */ +// close = () => { +// if (!this.isOpen) return; +// // onClose callback +// if (typeof this.options.onClose === 'function') { +// this.options.onClose.call(this, this.originEl); +// } +// this.isOpen = false; +// this.wrapper.classList.remove('open'); +// this.wrapper.ariaExpanded = 'false'; +// document.body.removeEventListener('click', this._handleDocumentClick, true); +// document.body.removeEventListener('keypress', this._handleDocumentClick, true); +// document.body.removeEventListener('touchend', this._handleDocumentClick); +// }; + +// static { +// TapTarget._taptargets = []; +// } +// } + +// export { TapTargetOptions, TapTarget }; diff --git a/src/tooltip/tooltip.css b/src/tooltip/tooltip.css new file mode 100644 index 0000000000..9bb29ca7bd --- /dev/null +++ b/src/tooltip/tooltip.css @@ -0,0 +1,56 @@ +/* Reset popover default browser styles */ +.material-tooltip[popover] { + margin: 0; + border: none; + padding: 6px 10px; + background: #323232; + color: #fff; + border-radius: 4px; + font-size: 12px; + + /* Modern CSS Anchor Positioning */ + position: fixed; + position-fallback: --tooltip-fallback; + + /* Transition/Animation Handling */ + opacity: 0; + transition: + opacity 0.2s ease-out, + display 0.2s allow-discrete; +} + +/* Style for open state */ +.material-tooltip[popover]:popover-open { + opacity: 1; +} + +@starting-style { + .material-tooltip[popover]:popover-open { + opacity: 0; + } +} + +/* Position mapping using anchor positioning */ +.material-tooltip[data-position='top'] { + bottom: anchor(top); + justify-self: anchor-center; + margin-bottom: 5px; +} + +.material-tooltip[data-position='bottom'] { + top: anchor(bottom); + justify-self: anchor-center; + margin-top: 5px; +} + +.material-tooltip[data-position='left'] { + right: anchor(left); + align-self: anchor-center; + margin-right: 5px; +} + +.material-tooltip[data-position='right'] { + left: anchor(right); + align-self: anchor-center; + margin-left: 5px; +} diff --git a/src/tooltip/tooltip.test.ts b/src/tooltip/tooltip.test.ts new file mode 100644 index 0000000000..75ddd54dcf --- /dev/null +++ b/src/tooltip/tooltip.test.ts @@ -0,0 +1,129 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Tooltip } from './tooltip'; + +const fixture = ` + + Hover me! + + + + Hover me! + + + +`; + +describe('Tooltip (Popover API)', () => { + beforeEach(() => { + // Setup fake timers for delay assertions + vi.useFakeTimers(); + + // Polyfill popover methods if Happy-DOM environment does not support native Popover API yet + if (!HTMLElement.prototype.showPopover) { + HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) { + this.setAttribute('popover-open', ''); + this.matches = (selector: string) => + selector === ':popover-open' || selector === '[popover]'; + }); + HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) { + this.removeAttribute('popover-open'); + this.matches = (selector: string) => selector === '[popover]'; + }); + } + + // Set up document fixture + document.body.innerHTML = fixture; + + // Initialize tooltips + Tooltip.init(document.querySelectorAll('.tooltipped'), { + enterDelay: 0, + exitDelay: 0 + }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('opens and closes properly', () => { + it('shows tooltip on mouse enter and hides on mouse leave', () => { + const tooltippedBtn = document.querySelector('#test')!; + const instance = Tooltip.getInstance(tooltippedBtn); + const tooltip = instance.tooltipEl; + + // Simulate mouse enter + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + // Verify content and open state + expect(instance.isOpen).toBe(true); + expect(tooltip.textContent).toBe('I am tooltip'); + + // Simulate mouse leave + tooltippedBtn.dispatchEvent(new Event('mouseleave')); + vi.advanceTimersByTime(10); + + expect(instance.isOpen).toBe(false); + }); + + it('removes tooltip DOM object on destroy', () => { + const tooltippedBtn = document.querySelector('#test1')!; + const instance = Tooltip.getInstance(tooltippedBtn); + const tooltipEl = instance.tooltipEl; + + instance.destroy(); + + // Check DOM cleanup & instance ref clearance + expect(Tooltip.getInstance(tooltippedBtn)).toBeUndefined(); + expect(document.body.contains(tooltipEl)).toBe(false); + }); + + it('changes position attribute dynamically', () => { + const tooltippedBtn = document.querySelector('#test')!; + tooltippedBtn.setAttribute('data-position', 'right'); + + const instance = Tooltip.getInstance(tooltippedBtn); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + expect(instance.tooltipEl.dataset.position).toBe('right'); + }); + + it('accepts delay option from javascript initialization', () => { + const tooltippedBtn = document.querySelector('#test')!; + tooltippedBtn.removeAttribute('data-delay'); + + // Re-initialize with a custom enterDelay + const instance = Tooltip.init(tooltippedBtn, { enterDelay: 200 }); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + + // Check before delay completes + vi.advanceTimersByTime(150); + expect(instance.isOpen).toBe(false); + + // Check after delay passes + vi.advanceTimersByTime(100); + expect(instance.isOpen).toBe(true); + }); + + it('assigns correctly with a fixed position parent', () => { + const tooltippedBtn = document.querySelector('#test2')!; + const instance = Tooltip.getInstance(tooltippedBtn); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + expect(instance.isOpen).toBe(true); + expect(instance.tooltipEl.dataset.position).toBe('bottom'); + }); + }); +}); diff --git a/src/tooltip/tooltip.ts b/src/tooltip/tooltip.ts new file mode 100644 index 0000000000..1006d40bdd --- /dev/null +++ b/src/tooltip/tooltip.ts @@ -0,0 +1,176 @@ +import { Component, BaseOptions, InitElements, MElement } from '../component'; + +export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; + +export interface TooltipOptions extends BaseOptions { + /** Delay before tooltip disappears (ms) */ + exitDelay: number; + /** Delay before tooltip appears (ms) */ + enterDelay: number; + /** Custom element ID for existing content */ + tooltipId?: string; + /** Text content for the tooltip */ + text: string; + /** Position relative to the anchor */ + position: TooltipPosition; +} + +const _defaults: TooltipOptions = { + exitDelay: 200, + enterDelay: 0, + text: '', + position: 'bottom' +}; + +export class Tooltip extends Component { + isOpen: boolean = false; + isHovered: boolean = false; + isFocused: boolean = false; + + tooltipEl: HTMLElement; + private _enterTimeout: number | NodeJS.Timeout; + private _exitTimeout: number | NodeJS.Timeout; + + constructor(el: HTMLElement, options: Partial) { + super(el, options, Tooltip); + this.el['M_Tooltip'] = this; + + this.options = { + ...Tooltip.defaults, + ...this._getAttributeOptions(), + ...options + }; + + this._setupPopoverElement(); + this._setupEventHandlers(); + } + + static get defaults(): TooltipOptions { + return _defaults; + } + + static init(el: HTMLElement, options?: Partial): Tooltip; + static init(els: InitElements, options?: Partial): Tooltip[]; + static init( + els: HTMLElement | InitElements, + options: Partial = {} + ): Tooltip | Tooltip[] { + return super.init(els, options, Tooltip); + } + + static getInstance(el: HTMLElement): Tooltip { + return el['M_Tooltip']; + } + + destroy() { + this.tooltipEl.remove(); + this._removeEventHandlers(); + delete this.el['M_Tooltip']; + } + + private _setupPopoverElement() { + if (this.options.tooltipId) { + this.tooltipEl = document.getElementById(this.options.tooltipId); + } else { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'material-tooltip'; + this.tooltipEl.textContent = this.options.text; + document.body.appendChild(this.tooltipEl); + } + + // Set native popover attributes + this.tooltipEl.popover = 'manual'; + + // Assign unique anchor name linking the element and popover + const anchorName = `--tooltip-anchor-${Math.random().toString(36).substring(2, 9)}`; + this.el.style['anchorName'] = anchorName; + this.tooltipEl.style['positionAnchor'] = anchorName; + + // Set default position class/data-attribute for CSS positioning + this.tooltipEl.dataset.position = this.options.position; + } + + private _setupEventHandlers() { + this.el.addEventListener('mouseenter', this._handleMouseEnter); + this.el.addEventListener('mouseleave', this._handleMouseLeave); + this.el.addEventListener('focus', this._handleFocus); + this.el.addEventListener('blur', this._handleBlur); + } + + private _removeEventHandlers() { + this.el.removeEventListener('mouseenter', this._handleMouseEnter); + this.el.removeEventListener('mouseleave', this._handleMouseLeave); + this.el.removeEventListener('focus', this._handleFocus); + this.el.removeEventListener('blur', this._handleBlur); + } + + open = (isManual = true) => { + if (this.isOpen) return; + + clearTimeout(this._exitTimeout); + this.options = { ...this.options, ...this._getAttributeOptions() }; + this._updateContent(); + + this._enterTimeout = setTimeout(() => { + if (!isManual && !this.isHovered && !this.isFocused) return; + + this.tooltipEl.showPopover(); + this.isOpen = true; + }, this.options.enterDelay); + }; + + close = () => { + if (!this.isOpen) return; + + clearTimeout(this._enterTimeout); + this.isHovered = false; + this.isFocused = false; + + this._exitTimeout = setTimeout(() => { + if (this.isHovered || this.isFocused) return; + + this.tooltipEl.hidePopover(); + this.isOpen = false; + }, this.options.exitDelay); + }; + + private _updateContent() { + if (!this.options.tooltipId) { + this.tooltipEl.textContent = this.options.text; + } + this.tooltipEl.dataset.position = this.options.position; + } + + private _handleMouseEnter = () => { + this.isHovered = true; + this.open(false); + }; + + private _handleMouseLeave = () => { + this.isHovered = false; + this.close(); + }; + + private _handleFocus = () => { + this.isFocused = true; + this.open(false); + }; + + private _handleBlur = () => { + this.isFocused = false; + this.close(); + }; + + private _getAttributeOptions(): Partial { + const attributeOptions: Partial = {}; + const text = this.el.getAttribute('data-tooltip'); + const position = this.el.getAttribute('data-position') as TooltipPosition; + const tooltipId = this.el.getAttribute('data-tooltip-id'); + + if (text) attributeOptions.text = text; + if (position) attributeOptions.position = position; + if (tooltipId) attributeOptions.tooltipId = tooltipId; + + return attributeOptions; + } +} diff --git a/src/utils.ts b/src/utils.ts index c3d5ad7ec0..4b0418d97a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,21 @@ -import { Edges } from './edges'; -import { Bounding } from './bounding'; +class Edges { + top: boolean; // If the top edge was exceeded + right: boolean; // If the right edge was exceeded + bottom: boolean; // If the bottom edge was exceeded + left: boolean; // If the left edge was exceeded +} + +class Bounding { + left: number; // left offset coordinate + top: number; + width: number; + height: number; +} /** * Class with utilitary functions for global usage. */ -export class Utils { +class Utils { /** Specifies wether tab is pressed or not. */ static tabPressed: boolean = false; /** Specifies wether there is a key pressed. */ @@ -379,7 +390,7 @@ export class Utils { container.style.top = newCoordinates.y + 'px'; container.style.left = newCoordinates.x + 'px'; - return {x: xMovement, y: yMovement}; + return { x: xMovement, y: yMovement }; } static _repositionWithinScreen( @@ -426,3 +437,5 @@ export class Utils { }; } } + +export { Utils }; diff --git a/tsconfig.json b/tsconfig.json index 702a550266..a9eac4ce45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,8 @@ -{ +{ "compilerOptions": { - //"outDir": "./dist", - //"emitDecoratorMetadata": true, - //"experimentalDecorators": true, "module": "ESNext", "moduleResolution": "Bundler", - "target": "ES2022", - //"skipLibCheck": true, - //"esModuleInterop": true, - //"resolveJsonModule": true, - //"sourceMap": true, - //"composite": true, - //"declaration": true, - //"declarationMap": true, + "target": "ES2022" }, - "include": [ - "src", - "src/**/*", - "rollup.config.ts" - ] -} \ No newline at end of file + "include": ["src", "src/**/*", "rollup.config.ts"] +}