diff --git a/package-lock.json b/package-lock.json index 69fd47d..17914c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@travelopia/web-components", - "version": "0.8.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@travelopia/web-components", - "version": "0.8.0", + "version": "0.9.1", "license": "MIT", "devDependencies": { "@travelopia/eslint-plugin-wordpress-coding-standards": "^1.0.0", @@ -38,13 +38,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -382,9 +383,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", "peer": true, @@ -393,9 +394,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "license": "MIT", "engines": { @@ -430,45 +431,29 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1977,9 +1962,9 @@ "peer": true }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dev": true, "license": "MIT", "peer": true, @@ -1991,16 +1976,16 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -2027,16 +2012,15 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -3172,19 +3156,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3620,9 +3591,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001650", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001650.tgz", - "integrity": "sha512-fgEc7hP/LB7iicdXHUI9VsBsMZmUmlVJeQP2qqQW+3lkqVhbmjEU8zp+h5stWeilX+G7uXuIUIIlWlDw9jdt8g==", + "version": "1.0.30001702", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", "dev": true, "funding": [ { @@ -3640,21 +3611,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3717,23 +3673,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -4360,16 +4299,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -5750,16 +5679,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -8443,19 +8362,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -8576,17 +8482,6 @@ "license": "MIT", "peer": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index d8d2afa..ef65c73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@travelopia/web-components", - "version": "0.8.0", + "version": "0.9.1", "description": "Accessible web components for the modern web", "files": [ "dist" @@ -8,7 +8,8 @@ "scripts": { "build": "webpack --config ./webpack.config.js --mode production --env=production", "dev": "webpack --config ./webpack.config.js --mode production --env=development --watch", - "lint": "eslint src" + "lint": "eslint src", + "lint:fix": "eslint src --fix" }, "repository": { "type": "git", diff --git a/src/slider/index.html b/src/slider/index.html index c3a68e2..915895c 100644 --- a/src/slider/index.html +++ b/src/slider/index.html @@ -33,28 +33,25 @@
- + -

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
- - - - + 1 / 4
diff --git a/src/slider/index.ts b/src/slider/index.ts index 87f62f7..5eb6cc7 100644 --- a/src/slider/index.ts +++ b/src/slider/index.ts @@ -18,11 +18,11 @@ import { TPSliderCountElement } from './tp-slider-count'; /** * Register Components. */ +customElements.define( 'tp-slider-nav', TPSliderNavElement ); customElements.define( 'tp-slider', TPSliderElement ); customElements.define( 'tp-slider-count', TPSliderCountElement ); customElements.define( 'tp-slider-track', TPSliderTrackElement ); customElements.define( 'tp-slider-slides', TPSliderSlidesElement ); customElements.define( 'tp-slider-slide', TPSliderSlideElement ); customElements.define( 'tp-slider-arrow', TPSliderArrowElement ); -customElements.define( 'tp-slider-nav', TPSliderNavElement ); customElements.define( 'tp-slider-nav-item', TPSliderNavItemElement ); diff --git a/src/slider/style.scss b/src/slider/style.scss index f78544f..0ee4a62 100644 --- a/src/slider/style.scss +++ b/src/slider/style.scss @@ -13,6 +13,10 @@ tp-slider-slides { position: relative; display: flex; align-items: flex-start; + overflow-y: visible; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; tp-slider:not([resizing="yes"]) & { transition-duration: 0.6s; diff --git a/src/slider/tp-slider-nav.ts b/src/slider/tp-slider-nav.ts index 5aae6c0..407de51 100644 --- a/src/slider/tp-slider-nav.ts +++ b/src/slider/tp-slider-nav.ts @@ -1,5 +1,59 @@ +/** + * Internal dependencies. + */ +import { TPSliderElement } from './tp-slider'; + /** * TP Slider Nav. */ export class TPSliderNavElement extends HTMLElement { + /** + * Properties. + */ + protected template: HTMLTemplateElement | null = null; + protected slider: TPSliderElement | null = null; + + /** + * Constructor. + */ + constructor() { + // Initialize parent. + super(); + + // Get elements. + this.template = this.querySelector( 'template' ); + this.slider = this.closest( 'tp-slider' ); + } + + /** + * Update nav items based on template. + */ + public updateNavItems(): void { + // Bail if no template. + if ( ! this.template || ! this.slider ) { + // Exit. + return; + } + + // Total slides. + const step = Number( this.slider?.getAttribute( 'step' ) ? Number( this.slider?.getAttribute( 'per-view' ) ?? '1' ) : 1 ); + const totalSlides = Number( this.slider?.getAttribute( 'total' ) ?? 0 ); + + // Calculate the number of navigation items. + const totalNavItems = Math.ceil( totalSlides / step ); + + // Clear the navigation. + this.innerHTML = ''; + + // Append the navigation items. + for ( let i = 1; i <= totalNavItems; i++ ) { + // Clone the template. + const navItem: Node = this.template.content.cloneNode( true ); + const div: HTMLDivElement = document.createElement( 'div' ); + div.appendChild( navItem ); + + // Append the navigation item. + this.innerHTML += div.innerHTML.replace( '$index', i.toString() ); + } + } } diff --git a/src/slider/tp-slider.ts b/src/slider/tp-slider.ts index b1b476a..34ff1f9 100644 --- a/src/slider/tp-slider.ts +++ b/src/slider/tp-slider.ts @@ -4,6 +4,7 @@ import { TPSliderSlidesElement } from './tp-slider-slides'; import { TPSliderSlideElement } from './tp-slider-slide'; import { TPSliderCountElement } from './tp-slider-count'; +import { TPSliderNavElement } from './tp-slider-nav'; import { TPSliderNavItemElement } from './tp-slider-nav-item'; import { TPSliderArrowElement } from './tp-slider-arrow'; @@ -14,9 +15,11 @@ export class TPSliderElement extends HTMLElement { /** * Properties. */ - protected touchStartX: number = 0; - protected touchStartY: number = 0; - protected swipeThreshold: number = 200; + protected isProgramaticScroll: boolean = false; + // protected _observer: IntersectionObserver; + protected slidesTrack: TPSliderSlidesElement | null; + protected slidesTrackRect: DOMRect | undefined; + protected slides: NodeListOf | null; protected responsiveSettings: { [ key: string ]: any }; protected allowedResponsiveKeys: string[] = [ 'flexible-height', @@ -35,15 +38,17 @@ export class TPSliderElement extends HTMLElement { constructor() { // Initialize parent. super(); + this.slides = this.querySelectorAll( 'tp-slider-slide' ); + this.slidesTrack = this.querySelector( 'tp-slider-slides' ); + this.slidesTrackRect = this.slidesTrack?.getBoundingClientRect(); + + this.slidesTrack?.addEventListener( 'scroll', this.handleCurrentSlideOnScroll.bind( this ) ); // Set current slide. if ( ! this.getAttribute( 'current-slide' ) ) { this.setAttribute( 'current-slide', '1' ); } - // Threshold Setting. - this.swipeThreshold = Number( this?.getAttribute( 'swipe-threshold' ) ?? '200' ); - // Initialize slider. this.slide(); this.autoSlide(); @@ -64,10 +69,6 @@ export class TPSliderElement extends HTMLElement { window.addEventListener( 'resize', this.handleResize.bind( this ) ); document.fonts.ready.then( () => this.handleResize() ); } - - // Touch listeners. - this.addEventListener( 'touchstart', this.handleTouchStart.bind( this ), { passive: true } ); - this.addEventListener( 'touchend', this.handleTouchEnd.bind( this ) ); } /** @@ -84,6 +85,33 @@ export class TPSliderElement extends HTMLElement { this.update(); } + /** + * Handle current slide attribute on scroll. + * + * This is to handle the case when the user scrolls the slides track + * and we need to update the current slide index based on the scroll position. + */ + handleCurrentSlideOnScroll() { + if ( ! this.slidesTrack || ! this.slides ) { + return; + } + + const isAtRightEnd = Math.abs(this.slidesTrack.scrollLeft + this.slidesTrack.clientWidth - this.slidesTrack.scrollWidth) < 1; + + if ( isAtRightEnd ) { + // If the current slide index is equal to the total number of slides, set it to the last slide. + this.setCurrentSlide( this.slides.length ); + } else { + this.slides?.forEach( ( slide: TPSliderSlideElement, index: number ) => { + const slideRect = slide.getBoundingClientRect(); + + if ( this.slidesTrackRect && slideRect?.left - this.slidesTrackRect.left === 0 ) { + this.setCurrentSlide( index + 1 ); + } + } ); + } + } + /** * Get observed attributes. * @@ -104,6 +132,12 @@ export class TPSliderElement extends HTMLElement { attributeChangedCallback( name: string = '', oldValue: string = '', newValue: string = '' ): void { // Keep an eye on current slide. if ( 'current-slide' === name && oldValue !== newValue ) { + // console.log('in attribute changed callback'); + + // if( ! this.isProgramaticScroll ) { + // this.updateHeight(); + // } + this.slide(); this.dispatchEvent( new CustomEvent( 'slide-complete', { bubbles: true } ) ); } @@ -207,6 +241,9 @@ export class TPSliderElement extends HTMLElement { * Navigate to the next slide. */ next(): void { + // Flag that it is a programatic scroll. + this.flagProgramaticScroll(); + // Initialize total slides variable. const totalSlides: number = this.getTotalSlides(); @@ -239,6 +276,9 @@ export class TPSliderElement extends HTMLElement { * Navigate to the previous slide. */ previous(): void { + // Flag that it is a programatic scroll. + this.flagProgramaticScroll(); + // Check if we are at the first slide. if ( this.currentSlideIndex <= 1 ) { // Check if we are in infinite mode. @@ -301,6 +341,8 @@ export class TPSliderElement extends HTMLElement { * @protected */ protected slide(): void { + // console.log("i am in slide"); + // Check if slider is disabled. if ( 'yes' === this.getAttribute( 'disabled' ) ) { // Yes, it is. So stop. @@ -318,7 +360,9 @@ export class TPSliderElement extends HTMLElement { } // First, update the height. - this.updateHeight(); + + // Yield to main thread to fix a bug in Safari 16. + setTimeout( () => this.updateHeight(), 0 ); // Now lets slide! const behaviour: string = this.getAttribute( 'behaviour' ) || ''; @@ -326,7 +370,10 @@ export class TPSliderElement extends HTMLElement { // Check if behaviour is set to fade and slide on the current slide index is present in the slides array. if ( 'fade' !== behaviour && slides[ this.currentSlideIndex - 1 ] ) { // Yes, it is. So slide to the current slide. - slidesContainer.style.left = `-${ slides[ this.currentSlideIndex - 1 ].offsetLeft }px`; + slidesContainer.scroll( { + left: slides[ this.currentSlideIndex - 1 ].offsetLeft - slides[ 0 ].offsetLeft, + behavior: 'smooth', + } ); } } @@ -367,7 +414,7 @@ export class TPSliderElement extends HTMLElement { */ update(): void { // Get sub-components. - const sliderNavItems: NodeListOf | null = this.querySelectorAll( 'tp-slider-nav-item' ); + const sliderNav: TPSliderNavElement | null = this.querySelector( 'tp-slider-nav' ); const sliderCounts: NodeListOf | null = this.querySelectorAll( 'tp-slider-count' ); const leftArrow: TPSliderArrowElement | null = this.getArrow( 'tp-slider-arrow[direction="previous"]' ); const rightArrow: TPSliderArrowElement | null = this.getArrow( 'tp-slider-arrow[direction="next"]' ); @@ -387,6 +434,12 @@ export class TPSliderElement extends HTMLElement { } ); } + // First, set the template for the slider nav. + sliderNav?.updateNavItems(); + + // Once the template has been set, query the slider nav items. + const sliderNavItems: NodeListOf | null = this.querySelectorAll( 'tp-slider-nav-item' ); + // Set current slider nav item. if ( sliderNavItems ) { sliderNavItems.forEach( ( navItem: TPSliderNavItemElement, index: number ): void => { @@ -485,6 +538,7 @@ export class TPSliderElement extends HTMLElement { // Set the height of the container to be the max height of the slides in the current view. slidesContainer.style.height = `${ maxHeight }px`; } else { + // console.log("i am in update height"); // Set the height of the container to be the height of the current slide. const height: number = slides[ this.currentSlideIndex - 1 ].scrollHeight; slidesContainer.style.height = `${ height }px`; @@ -572,64 +626,6 @@ export class TPSliderElement extends HTMLElement { } ); } - /** - * Detect touch start event, and store the starting location. - * - * @param {Event} e Touch event. - * - * @protected - */ - protected handleTouchStart( e: TouchEvent ): void { - // initialize touch start coordinates - if ( 'yes' === this.getAttribute( 'swipe' ) ) { - this.touchStartX = e.touches[ 0 ].clientX; - this.touchStartY = e.touches[ 0 ].clientY; - } - } - - /** - * Detect touch end event, and check if it was a left or right swipe. - * - * @param {Event} e Touch event. - * - * @protected - */ - protected handleTouchEnd( e: TouchEvent ): void { - // Early return if swipe is not enabled. - if ( 'yes' !== this.getAttribute( 'swipe' ) ) { - // Early return. - return; - } - - // Calculate the horizontal and vertical distance moved. - const touchEndX: number = e.changedTouches[ 0 ].clientX; - const touchEndY: number = e.changedTouches[ 0 ].clientY; - const swipeDistanceX: number = touchEndX - this.touchStartX; - const swipeDistanceY: number = touchEndY - this.touchStartY; - - // Determine if the swipe is predominantly horizontal or vertical. - const isHorizontalSwipe: boolean = Math.abs( swipeDistanceX ) > Math.abs( swipeDistanceY ); - - // If it's not horizontal swipe, return - if ( ! isHorizontalSwipe ) { - // Early return. - return; - } - - // Check if it's a right or left swipe. - if ( swipeDistanceX > 0 ) { - // Right-Swipe: Check if horizontal swipe distance is less than the threshold. - if ( swipeDistanceX < this.swipeThreshold ) { - this.previous(); - } - } else if ( swipeDistanceX < 0 ) { - // Left-Swipe: Check if horizontal swipe distance is less than the threshold. - if ( swipeDistanceX > -this.swipeThreshold ) { - this.next(); - } - } - } - /** * Auto slide. */ @@ -660,4 +656,18 @@ export class TPSliderElement extends HTMLElement { this.dispatchEvent( new CustomEvent( 'auto-slide-complete' ) ); }, interval ); } + + /** + * To set a flag if it is a programatic scroll. + */ + flagProgramaticScroll() { + // Set the flag to true. + this.isProgramaticScroll = true; + + // Set the flag to false after 500ms. + setTimeout( () => { + // Set the flag to false. + this.isProgramaticScroll = false; + }, 500 ); + } } diff --git a/src/tooltip/README.md b/src/tooltip/README.md new file mode 100644 index 0000000..13630b1 --- /dev/null +++ b/src/tooltip/README.md @@ -0,0 +1,64 @@ +# Tooltip + + + + + + +
+

Built by the super talented team at Travelopia.

+
+ +
+ +## Sample Usage + +This is a highly customizable tooltip component. + +Example: + +```js +// Import the component as needed: +import '@travelopia/web-components/dist/tooltip'; +import '@travelopia/web-components/dist/tooltip/style.css'; + +// No JavaScript is required to initialise! +``` + +```html + <-- Define and style the tooltip, and give it an ID + + <-- This is where the content of the tooltip would go + + <-- If you want an arrow + + +

+ Here is some informative content about + + <-- Make any element a tooltip trigger by wrapping this component + + interesting subject <-- The first direct descendant (that is not a template) is the trigger + + + that you may be interested in! +

+``` + +## Attributes + +| Attribute | Required | Values | Notes | +|---------------------|----------|----------|-----------------------------------------------------------------------------------------------------------------| +| offset | No | | The offset in pixels from the trigger that the tooltip should display | + +## Events + +| Event | Notes | +|-------|-------------------------------| +| show | After the tooltip is visible | +| hide | After the tooltip is hidden | diff --git a/src/tooltip/index.html b/src/tooltip/index.html new file mode 100644 index 0000000..b56b5f6 --- /dev/null +++ b/src/tooltip/index.html @@ -0,0 +1,179 @@ + + + + + + + + Web Component: Tooltip + + + + + + +
+ +
+ + + + + + +

Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero excepturi, esse aperiam omnis voluptatem + et perspiciatis quas nostrum. Modi velit repellat labore illo, laudantium eaque, ea esse tempora earum + natus ullam repudiandae voluptatibus ut perferendis nobis. Nesciunt, possimus accusamus sed dolor + exercitationem repellendus sunt perspiciatis. Nulla saepe assumenda facere. Aliquid et eum accusantium + velit nisi rem, eaque, repudiandae voluptas culpa dolor hic at aperiam dignissimos voluptatum quia, + architecto incidunt! Unde ipsa maiores maxime repellendus, voluptatem assumenda doloribus fugit + necessitatibus ipsam soluta laboriosam voluptas optio a tenetur eveniet nostrum debitis hic, beatae + eos asperiores repellat consectetur animi est dolor? Quis, recusandae neque deserunt asperiores culpa + + + Paragraph link 1 + + exercitationem autem consequatur, sequi saepe blanditiis, laudantium vel? Voluptates pariatur ducimus + corporis magni. Placeat, fugiat, veniam incidunt dolorum numquam itaque non inventore, aliquam laboriosam + dolor mollitia ipsa! Nemo voluptas, animi corrupti facere perspiciatis commodi voluptatibus sint saepe + suscipit? Molestiae temporibus recusandae delectus molestias, ea, tempora unde, sapiente quae reiciendis + suscipit est? Nemo enim eius suscipit possimus eum debitis adipisci officiis, dolor, iste autem delectus + quam illum ut asperiores expedita impedit ad atque repellat aut quisquam doloremque! Nihil magnam harum + + + Paragraph link 2 + + dolores in neque perspiciatis! Velit, repellendus sapiente cumque ullam optio molestias ipsum doloribus + blanditiis. Eum at quibusdam voluptatem fugiat facere omnis ipsam est obcaecati provident laborum dolores + exercitationem aspernatur saepe cumque animi eligendi corrupti, aliquid tenetur. Similique quam accusantium + numquam a, aliquam libero debitis quibusdam consequatur modi doloremque, exercitationem eum temporibus quo + quia velit repellendus, aspernatur eos. Optio exercitationem dolore ipsum alias est quas velit eaque autem + ducim us. Iusto fugit reiciendis nisi fugiat deserunt, quibusdam doloribus. Eligendi a officia quas totam + adipisci minus voluptatibus dignissimos sed nostrum. Consequatur eos est illo tempore odit, adipisci itaque + + + Paragraph link 3 + + magnam, obcaecati ullam possimus ab sint beatae, animi sed mollitia sit perspiciatis eligendi! Tenetur minus + vero soluta amet excepturi sit quia animi. +

+
+ +
+ +
+ + + + + + +
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+ Card Image + + +
+

Card Title

+ i +
+
+

This is a simple description for the card. It provides some basic information about the content displayed above.

+
+
+
+ +
+ + diff --git a/src/tooltip/index.ts b/src/tooltip/index.ts new file mode 100644 index 0000000..89552c7 --- /dev/null +++ b/src/tooltip/index.ts @@ -0,0 +1,18 @@ +/** + * Styles. + */ +import './style.scss'; + +/** + * Components. + */ +import { TPTooltip } from './tp-tooltip'; +import { TPTooltipTrigger } from './tp-tooltip-trigger'; +import { TPTooltipArrow } from './tp-tooltip-arrow'; + +/** + * Register Components. + */ +customElements.define( 'tp-tooltip', TPTooltip ); +customElements.define( 'tp-tooltip-trigger', TPTooltipTrigger ); +customElements.define( 'tp-tooltip-arrow', TPTooltipArrow ); diff --git a/src/tooltip/style.scss b/src/tooltip/style.scss new file mode 100644 index 0000000..f6e4b09 --- /dev/null +++ b/src/tooltip/style.scss @@ -0,0 +1,41 @@ +tp-tooltip { + border: 1px solid #000; + + &[popover] { + overflow: visible; + margin: 0; + padding: 0; + } + + tp-tooltip-content { + position: relative; + display: block; + z-index: 10; + } + + tp-tooltip-arrow { + position: absolute; + display: block; + z-index: 5; + border: 1px solid #000; + width: 15px; + height: 15px; + background-color: #fff; + + &[position="top"] { + top: -1px; + transform: translateY(-50%) translateX(-50%) rotate(45deg); + border-right: none; + border-bottom: none; + } + + &[position="bottom"] { + bottom: -1px; + transform: translateY(50%) translateX(-50%) rotate(45deg); + border-left: none; + border-top: none; + } + } +} + + diff --git a/src/tooltip/tp-tooltip-arrow.ts b/src/tooltip/tp-tooltip-arrow.ts new file mode 100644 index 0000000..d9dcce4 --- /dev/null +++ b/src/tooltip/tp-tooltip-arrow.ts @@ -0,0 +1,5 @@ +/** + * TP Tooltip Arrow. + */ +export class TPTooltipArrow extends HTMLElement { +} diff --git a/src/tooltip/tp-tooltip-trigger.ts b/src/tooltip/tp-tooltip-trigger.ts new file mode 100644 index 0000000..8a9ae58 --- /dev/null +++ b/src/tooltip/tp-tooltip-trigger.ts @@ -0,0 +1,133 @@ +/** + * Internal dependencies. + */ +import { TPTooltip } from './tp-tooltip'; + +/** + * TP Tooltip Trigger. + */ +export class TPTooltipTrigger extends HTMLElement { + /** + * Constructor. + */ + constructor() { + // Call parent's constructor. + super(); + + // Check if touch device. + if ( navigator.maxTouchPoints > 0 ) { + // Yes it is, toggle tooltip on click. + this.addEventListener( 'click', this.toggleTooltip.bind( this ) ); + } else { + // Else add event listeners for mouse. + this.addEventListener( 'mouseenter', this.showTooltip.bind( this ) ); + this.addEventListener( 'mouseleave', this.hideTooltip.bind( this ) ); + } + + // On window scroll, hide tooltip. + window.addEventListener( 'scroll', this.handleWindowScroll.bind( this ), true ); + } + + /** + * Toggle the tooltip. + */ + toggleTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find the tooltip. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + + // Check if the tooltip is already shown. + if ( 'yes' === tooltip?.getAttribute( 'show' ) ) { + // It is, hide it. + tooltip?.hide(); + } else { + // It isn't, show it. + tooltip?.setTrigger( this ); + tooltip?.show(); + } + } + + /** + * Show the tooltip. + */ + showTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find and show the tooltip with its content. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + tooltip?.setTrigger( this ); + tooltip?.show(); + } + + /** + * Hide the tooltip. + */ + hideTooltip(): void { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + + // Check if we have a tooltip. + if ( '' === tooltipId ) { + // We don't, bail. + return; + } + + // Find and hide the tooltip. + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + tooltip?.hide(); + } + + /** + * Get the content of the tooltip. + * + * @return {Node|null} The content of the tooltip. + */ + getContent(): Node | null { + // Find template for content. + const template: HTMLTemplateElement | null = this.querySelector( 'template' ); + + // Check if we found a template. + if ( template ) { + // We did, return its content. + return template.content.cloneNode( true ); + } + + // No template found, return null. + return null; + } + + /** + * Handles the scroll outside of the tooltip. + * + * @param { Event } evt The event object. + */ + handleWindowScroll( evt: Event ) { + // Get the tooltip. + const tooltipId: string = this.getAttribute( 'tooltip' ) ?? ''; + const tooltip: TPTooltip | null = document.querySelector( `#${ tooltipId }` ); + + // If the content was the original target. + if ( evt.target === tooltip ) { + // Do nothing. + return; + } + + // Hide the popover + this.hideTooltip(); + } +} diff --git a/src/tooltip/tp-tooltip.ts b/src/tooltip/tp-tooltip.ts new file mode 100644 index 0000000..9b9b2d7 --- /dev/null +++ b/src/tooltip/tp-tooltip.ts @@ -0,0 +1,175 @@ +/** + * Internal dependencies. + */ +import { TPTooltipTrigger } from './tp-tooltip-trigger'; +import { TPTooltipArrow } from './tp-tooltip-arrow'; + +/** + * TP Tooltip. + */ +export class TPTooltip extends HTMLElement { + /** + * Properties. + */ + protected trigger: TPTooltipTrigger | null = null; + + /** + * Constructor. + */ + constructor() { + // Call parent's constructor. + super(); + + // Make the tooltip a popover. + this.makePopover(); + } + + /** + * Get offset. + */ + get offset(): number { + // Get the offset. + return parseInt( this.getAttribute( 'offset' ) ?? '0' ); + } + + /** + * Set offset. + */ + set offset( offset: number ) { + // Set or remove offset. + if ( ! offset ) { + this.removeAttribute( 'offset' ); + } else { + this.setAttribute( 'offset', offset.toString() ); + } + } + + /** + * Make this tooltip a popover, if it isn't already. + */ + makePopover(): void { + // Check if this isn't already a popover. + if ( ! this.getAttribute( 'popover' ) ) { + this.setAttribute( 'popover', '' ); + } + } + + /** + * Set the trigger. + * + * @param {HTMLElement} trigger The trigger node. + */ + setTrigger( trigger: TPTooltipTrigger ): void { + // Set the trigger. + this.trigger = trigger; + } + + /** + * Set the content for our tooltip. + */ + setContent(): void { + // Get content. + const content: Node | null = this.trigger?.getContent() ?? null; + + // Check if we have content. + if ( content ) { + // Yes, replace slot's children with new content. + this.querySelector( 'slot' )?.replaceChildren( content ); + } + } + + /** + * Set the position of the tooltip. + */ + setPosition(): void { + // Do we have a trigger? + if ( ! this.trigger ) { + // We don't, bail! + return; + } + + // Get width and height of this tooltip. + const { height: tooltipHeight, width: tooltipWidth } = this.getBoundingClientRect(); + + // Get position and coordinates of the trigger. + const { x: triggerLeftPosition, y: triggerTopPosition, width: triggerWidth, height: triggerHeight } = this.trigger.getBoundingClientRect(); + + // Trigger center from left edge of screen. + const triggerCenterPosition = triggerLeftPosition + ( triggerWidth / 2 ); + + // Get arrow dimensions. + let arrowHeight: number = 0; + const arrow: TPTooltipArrow | null = this.querySelector( 'tp-tooltip-arrow' ); + + // Check if we have an arrow. + if ( arrow ) { + ( { height: arrowHeight } = arrow.getBoundingClientRect() ); + } + + // Determine the vertical position of this tooltip. + if ( triggerTopPosition > tooltipHeight + this.offset + arrowHeight ) { + // There is enough space on top of trigger element, so place popover above the trigger element. + this.style.marginTop = `${ triggerTopPosition - tooltipHeight - this.offset - ( arrowHeight / 2 ) }px`; + + // Set arrow placement on bottom of popover + arrow?.setAttribute( 'position', 'bottom' ); + } else { + // There is not enough space on top of trigger element, so place popover below the trigger element. + this.style.marginTop = `${ triggerTopPosition + triggerHeight + this.offset + ( arrowHeight / 2 ) }px`; + + // Set arrow placement on top of popover + arrow?.setAttribute( 'position', 'top' ); + } + + // Determine the horizontal position of this tooltip. + if ( triggerCenterPosition < ( tooltipWidth / 2 ) ) { + // There is not enough space on left of trigger element, so place popover at the left edge of the screen. + this.style.marginLeft = '0px'; + } else if ( window.innerWidth - triggerCenterPosition < ( tooltipWidth / 2 ) ) { + // There is not enough space on right of trigger element, so place popover at the right edge of the screen. + this.style.marginLeft = `${ window.innerWidth - tooltipWidth }px`; + } else { + // There is enough space on both sides of trigger element, so place popover at the center of the trigger element. + this.style.marginLeft = `${ triggerLeftPosition + ( triggerWidth / 2 ) - ( tooltipWidth / 2 ) }px`; + } + + // Get left position of the tooltip. + const { left: tooltipLeftPosition } = this.getBoundingClientRect(); + + // Percentage the arrow should be moved from left edge of the tooltip. + const arrowPosition = ( ( triggerCenterPosition - tooltipLeftPosition ) / tooltipWidth ) * 100; + + // Set the arrow position. + if ( arrow ) { + arrow.style.left = `${ arrowPosition }%`; + } + } + + /** + * Show the tooltip. + */ + show(): void { + // Position tooltip and show it. + this.setContent(); + + // Show the tooltip. + this.showPopover(); + this.setPosition(); + this.setAttribute( 'show', 'yes' ); + + // Trigger event. + this.dispatchEvent( new CustomEvent( 'show' ) ); + } + + /** + * Hide the tooltip. + */ + hide(): void { + // Hide the tooltip. + this.hidePopover(); + this.removeAttribute( 'show' ); + + // Trigger event. + this.dispatchEvent( new CustomEvent( 'hide' ) ); + } +} diff --git a/webpack.config.js b/webpack.config.js index a347deb..19cfb26 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,6 +78,7 @@ module.exports = ( env ) => { entry: { modal: './src/modal/index.ts', slider: './src/slider/index.ts', + newSlider: './src/carousel/index.ts', tabs: './src/tabs/index.ts', form: './src/form/index.ts', accordion: './src/accordion/index.ts', @@ -85,6 +86,7 @@ module.exports = ( env ) => { lightbox: './src/lightbox/index.ts', 'toggle-attribute': './src/toggle-attribute/index.ts', 'number-spinner': './src/number-spinner/index.ts', + tooltip: './src/tooltip/index.ts', }, module: { rules: [