diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 8f6a1226e1..5a5a8f5551 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -26,11 +26,11 @@ }, { "path": "./dist/css/boosted.css", - "maxSize": "34.5 kB" + "maxSize": "35.75 kB" }, { "path": "./dist/css/boosted.min.css", - "maxSize": "31.75 kB" + "maxSize": "32.75 kB" }, { "path": "./dist/js/boosted.bundle.js", @@ -42,7 +42,7 @@ }, { "path": "./dist/js/boosted.esm.js", - "maxSize": "32.5 kB" + "maxSize": "32.75 kB" }, { "path": "./dist/js/boosted.esm.min.js", @@ -54,7 +54,7 @@ }, { "path": "./dist/js/boosted.min.js", - "maxSize": "17.5 kB" + "maxSize": "17.75 kB" } ], "ci": { diff --git a/.cspell.json b/.cspell.json index 8aba1e5595..416684375d 100644 --- a/.cspell.json +++ b/.cspell.json @@ -20,6 +20,7 @@ "btnradio", "callout", "callouts", + "camelCase", "clearfix", "clic", "Codesniffer", @@ -69,6 +70,7 @@ "markdownify", "mediaqueries", "minifiers", + "misfunction", "monospace", "mouseleave", "navbars", diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5c6a4e727c..e25e2bca50 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,7 +2,7 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards diff --git a/js/src/carousel.js b/js/src/carousel.js index 86463f71e6..f6903a899e 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -83,19 +83,19 @@ const KEY_TO_DIRECTION = { const Default = { interval: 5000, keyboard: true, - slide: false, pause: 'hover', - wrap: true, - touch: true + ride: false, + touch: true, + wrap: true } const DefaultType = { interval: '(number|boolean)', keyboard: 'boolean', - slide: '(boolean|string)', + ride: '(boolean|string)', pause: '(string|boolean)', - wrap: 'boolean', - touch: 'boolean' + touch: 'boolean', + wrap: 'boolean' } /** @@ -108,7 +108,6 @@ class Carousel extends BaseComponent { this._interval = null this._activeElement = null - this._stayPaused = false this._isSliding = false this.touchTimeout = null this._swipeHelper = null @@ -118,6 +117,10 @@ class Carousel extends BaseComponent { this._playPauseButton = SelectorEngine.findOne(`${SELECTOR_CONTROL_PAUSE}[${SELECTOR_CAROUSEL_TO_PAUSE}="#${this._element.id}"]`) // Boosted mod this._addEventListeners() + + if (this._config.ride === CLASS_NAME_CAROUSEL) { + this.cycle() + } } // Getters @@ -151,7 +154,7 @@ class Carousel extends BaseComponent { this._slide(ORDER_PREV) } - pause(event) { + pause() { // Boosted mod: reset the animation on progress indicator if (this._indicatorsElement) { this._element.classList.add(CLASS_NAME_PAUSED) @@ -168,19 +171,14 @@ class Carousel extends BaseComponent { } // End mod - if (!event) { - this._stayPaused = true - } - if (this._isSliding) { triggerTransitionEnd(this._element) - this.cycle(true) } this._clearInterval() } - cycle(event) { + cycle() { // Boosted mod: restart the animation on progress indicator if (this._indicatorsElement) { this._element.classList.remove(CLASS_NAME_PAUSED) @@ -197,16 +195,23 @@ class Carousel extends BaseComponent { } // End mod - if (!event) { - this._stayPaused = false - } - this._clearInterval() - if (this._config.interval && !this._stayPaused) { - this._updateInterval() + this._updateInterval() + + this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval) + } - this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval) + _maybeEnableCycle() { + if (!this._config.ride) { + return } + + if (this._isSliding) { + EventHandler.one(this._element, EVENT_SLID, () => this.cycle()) + return + } + + this.cycle() } to(index) { @@ -228,8 +233,6 @@ class Carousel extends BaseComponent { const activeIndex = this._getItemIndex(this._getActive()) if (activeIndex === index) { - this.pause() - this.cycle() return } @@ -258,8 +261,8 @@ class Carousel extends BaseComponent { } if (this._config.pause === 'hover') { - EventHandler.on(this._element, EVENT_MOUSEENTER, event => this.pause(event)) - EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event)) + EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause()) + EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle()) } if (this._config.touch && Swipe.isSupported()) { @@ -290,7 +293,7 @@ class Carousel extends BaseComponent { clearTimeout(this.touchTimeout) } - this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) + this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval) } const swipeConfig = { @@ -436,12 +439,10 @@ class Carousel extends BaseComponent { return } - this._isSliding = true - const isCycling = Boolean(this._interval) - if (isCycling) { - this.pause() - } + this.pause() + + this._isSliding = true this._setActiveIndicatorElement(nextElementIndex) this._activeElement = nextElement @@ -554,12 +555,6 @@ class Carousel extends BaseComponent { } data[config]() - return - } - - if (data._config.interval && data._config.ride) { - data.pause() - data.cycle() } }) } @@ -583,15 +578,18 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (e if (slideIndex) { carousel.to(slideIndex) + carousel._maybeEnableCycle() return } if (Manipulator.getDataAttribute(this, 'slide') === 'next') { carousel.next() + carousel._maybeEnableCycle() return } carousel.prev() + carousel._maybeEnableCycle() }) EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_CONTROL_PAUSE, Carousel.PauseCarousel) // Boosted mod diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index a31ed333c9..413aa6e284 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -20,7 +20,7 @@ const customEvents = { mouseenter: 'mouseover', mouseleave: 'mouseout' } -const customEventsRegex = /^(mouseenter|mouseleave)/i + const nativeEvents = new Set([ 'click', 'dblclick', @@ -150,7 +150,7 @@ function addHandler(element, originalTypeEvent, handler, delegationFunction, one // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position // this prevents the handler from being dispatched the same way as mouseover or mouseout does - if (customEventsRegex.test(originalTypeEvent)) { + if (originalTypeEvent in customEvents) { const wrapFunction = fn => { return function (event) { if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) { diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js index 5e6ad92ae7..2d96d65fc8 100644 --- a/js/src/dom/manipulator.js +++ b/js/src/dom/manipulator.js @@ -22,7 +22,15 @@ function normalizeData(value) { return null } - return value + if (typeof value !== 'string') { + return value + } + + try { + return JSON.parse(decodeURIComponent(value)) + } catch { + return value + } } function normalizeDataKey(key) { @@ -44,7 +52,7 @@ const Manipulator = { } const attributes = {} - const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs')) + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')) for (const key of bsKeys) { let pureKey = key.replace(/^bs/, '') diff --git a/js/src/tab.js b/js/src/tab.js index 135e929dda..3fa5e4c9e9 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -168,8 +168,11 @@ class Tab extends BaseComponent { event.stopPropagation()// stopPropagation/preventDefault both added to support up/down keys without scrolling the page event.preventDefault() const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key) - const nextActiveElement = getNextActiveElement(this._getChildren(), event.target, isNext, true) - Tab.getOrCreateInstance(nextActiveElement).show() + const nextActiveElement = getNextActiveElement(this._getChildren().filter(element => !isDisabled(element)), event.target, isNext, true) + + if (nextActiveElement) { + Tab.getOrCreateInstance(nextActiveElement).show() + } } _getChildren() { // collection of inner elements diff --git a/js/src/toast.js b/js/src/toast.js index b85e20b605..8ee8c8c968 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -100,7 +100,7 @@ class Toast extends BaseComponent { } hide() { - if (!this._element.classList.contains(CLASS_NAME_SHOW)) { + if (!this.isShown()) { return } @@ -123,13 +123,17 @@ class Toast extends BaseComponent { dispose() { this._clearTimeout() - if (this._element.classList.contains(CLASS_NAME_SHOW)) { + if (this.isShown()) { this._element.classList.remove(CLASS_NAME_SHOW) } super.dispose() } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW) + } + // Private _maybeScheduleHide() { diff --git a/js/src/util/config.js b/js/src/util/config.js index 19d02955dd..f6c194276b 100644 --- a/js/src/util/config.js +++ b/js/src/util/config.js @@ -38,8 +38,11 @@ class Config { } _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse + return { ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), ...(typeof config === 'object' ? config : {}) } diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 653510302a..c76f6f7612 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -63,6 +63,20 @@ describe('Carousel', () => { expect(carouselByElement._element).toEqual(carouselEl) }) + it('should start cycling if `ride`===`carousel`', () => { + fixtureEl.innerHTML = '
' + + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).not.toBeNull() + }) + + it('should not start cycling if `ride`!==`carousel`', () => { + fixtureEl.innerHTML = '' + + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).toBeNull() + }) + it('should go to next item if right arrow key is pressed', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ @@ -95,6 +109,40 @@ describe('Carousel', () => { }) }) + it('should ignore keyboard events if data-bs-keyboard=false', () => { + fixtureEl.innerHTML = [ + ' ' + ].join('') + + spyOn(EventHandler, 'trigger').and.callThrough() + const carouselEl = fixtureEl.querySelector('#myCarousel') + // eslint-disable-next-line no-new + new Carousel('#myCarousel') + expect(EventHandler.trigger).not.toHaveBeenCalledWith(carouselEl, 'keydown.bs.carousel', jasmine.any(Function)) + }) + + it('should ignore mouse events if data-bs-pause=false', () => { + fixtureEl.innerHTML = [ + ' ' + ].join('') + + spyOn(EventHandler, 'trigger').and.callThrough() + const carouselEl = fixtureEl.querySelector('#myCarousel') + // eslint-disable-next-line no-new + new Carousel('#myCarousel') + expect(EventHandler.trigger).not.toHaveBeenCalledWith(carouselEl, 'hover.bs.carousel', jasmine.any(Function)) + }) + it('should go to previous item if left arrow key is pressed', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ @@ -812,19 +860,21 @@ describe('Carousel', () => { }) }) - it('should call cycle on mouse out with pause equal to hover', () => { + it('should call `maybeCycle` on mouse out with pause equal to hover', () => { return new Promise(resolve => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) + spyOn(carousel, '_maybeEnableCycle').and.callThrough() spyOn(carousel, 'cycle') const mouseOutEvent = createEvent('mouseout') carouselEl.dispatchEvent(mouseOutEvent) setTimeout(() => { + expect(carousel._maybeEnableCycle).toHaveBeenCalled() expect(carousel.cycle).toHaveBeenCalled() resolve() }, 10) @@ -969,6 +1019,28 @@ describe('Carousel', () => { expect(carousel._activeElement).toEqual(secondItemEl) }) + it('should continue cycling if it was already', () => { + fixtureEl.innerHTML = [ + ' ' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) + spyOn(carousel, 'cycle') + + carousel.next() + expect(carousel.cycle).not.toHaveBeenCalled() + + carousel.cycle() + carousel.next() + expect(carousel.cycle).toHaveBeenCalled() + }) + it('should update indicators if present', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ @@ -1023,12 +1095,14 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl) const nextSpy = spyOn(carousel, 'next') const prevSpy = spyOn(carousel, 'prev') + spyOn(carousel, '_maybeEnableCycle') nextBtnEl.click() prevBtnEl.click() expect(nextSpy).toHaveBeenCalled() expect(prevSpy).toHaveBeenCalled() + expect(carousel._maybeEnableCycle).toHaveBeenCalled() }) }) @@ -1414,82 +1488,32 @@ describe('Carousel', () => { }) // End mod - it('should call cycle if the carousel have carousel-item-next or carousel-item-prev class, cause is sliding', () => { - fixtureEl.innerHTML = [ - ' ' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(carousel, '_clearInterval') - - carousel._slide('next') - carousel.pause() - - expect(carousel.cycle).toHaveBeenCalledWith(true) - expect(carousel._clearInterval).toHaveBeenCalled() - expect(carousel._stayPaused).toBeTrue() - }) - - it('should not call cycle if nothing is in transition', () => { - fixtureEl.innerHTML = [ - ' ' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(carousel, '_clearInterval') - - carousel.pause() - - expect(carousel.cycle).not.toHaveBeenCalled() - expect(carousel._clearInterval).toHaveBeenCalled() - expect(carousel._stayPaused).toBeTrue() - }) - - it('should not set is paused at true if an event is passed', () => { - fixtureEl.innerHTML = [ - ' ' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - const event = createEvent('mouseenter') + it('should trigger transitionend if the carousel have carousel-item-next or carousel-item-prev class, cause is sliding', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + ' ' + ].join('') - spyOn(carousel, '_clearInterval') + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) - carousel.pause(event) + carouselEl.addEventListener('transitionend', () => { + expect(carousel._clearInterval).toHaveBeenCalled() + resolve() + }) - expect(carousel._clearInterval).toHaveBeenCalled() - expect(carousel._stayPaused).toBeFalse() + spyOn(carousel, '_clearInterval') + carousel._slide('next') + carousel.pause() + }) }) }) @@ -1540,30 +1564,6 @@ describe('Carousel', () => { expect(window.setInterval).toHaveBeenCalled() }) - it('should not set interval if the carousel is paused', () => { - fixtureEl.innerHTML = [ - ' ' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(window, 'setInterval').and.callThrough() - - carousel._stayPaused = true - carousel.cycle(true) - - expect(window.setInterval).not.toHaveBeenCalled() - }) - it('should clear interval if there is one', () => { fixtureEl.innerHTML = [ '