diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f4927aacd68f..4b98565e2ce0 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -10,13 +10,16 @@ import { getElementFromSelector, getSelectorFromElement, getTransitionDurationFromElement, - isVisible + isDisabled, + isVisible, + typeCheckConfig } from './util/index' import { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar' import Data from './dom/data' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' +import Manipulator from './dom/manipulator' /** * ------------------------------------------------------------------------ @@ -29,10 +32,20 @@ const DATA_KEY = 'bs.offcanvas' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const ESCAPE_KEY = 'Escape' -const DATA_BODY_ACTIONS = 'data-bs-body' + +const Default = { + backdrop: true, + keyboard: true, + scroll: false +} + +const DefaultType = { + backdrop: 'boolean', + keyboard: 'boolean', + scroll: 'boolean' +} const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' -const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_TOGGLING = 'offcanvas-toggling' const ACTIVE_SELECTOR = `.offcanvas.show, .${CLASS_NAME_TOGGLING}` @@ -55,14 +68,24 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]' */ class Offcanvas extends BaseComponent { - constructor(element) { + constructor(element, config) { super(element) + this._config = this._getConfig(config) this._isShown = element.classList.contains(CLASS_NAME_SHOW) - this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) || '' this._addEventListeners() } + // Getters + + static get Default() { + return Default + } + + static get DATA_KEY() { + return DATA_KEY + } + // Public toggle(relatedTarget) { @@ -83,11 +106,11 @@ class Offcanvas extends BaseComponent { this._isShown = true this._element.style.visibility = 'visible' - if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + if (this._config.backdrop) { document.body.classList.add(CLASS_NAME_BACKDROP_BODY) } - if (!this._bodyOptionsHas('scroll')) { + if (!this._config.scroll) { scrollBarHide() } @@ -129,11 +152,11 @@ class Offcanvas extends BaseComponent { this._element.removeAttribute('role') this._element.style.visibility = 'hidden' - if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + if (this._config.backdrop) { document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) } - if (!this._bodyOptionsHas('scroll')) { + if (!this._config.scroll) { scrollBarReset() } @@ -144,6 +167,18 @@ class Offcanvas extends BaseComponent { setTimeout(completeCallback, getTransitionDurationFromElement(this._element)) } + // Private + + _getConfig(config) { + config = { + ...Default, + ...Manipulator.getDataAttributes(this._element), + ...(typeof config === 'object' ? config : {}) + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + _enforceFocusOnElement(element) { EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop EventHandler.on(document, EVENT_FOCUSIN, event => { @@ -156,15 +191,11 @@ class Offcanvas extends BaseComponent { element.focus() } - _bodyOptionsHas(option) { - return this._bodyOptions.split(',').includes(option) - } - _addEventListeners() { EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide()) EventHandler.on(document, 'keydown', event => { - if (event.key === ESCAPE_KEY) { + if (this._config.keyboard && event.key === ESCAPE_KEY) { this.hide() } }) @@ -181,15 +212,17 @@ class Offcanvas extends BaseComponent { static jQueryInterface(config) { return this.each(function () { - const data = Data.get(this, DATA_KEY) || new Offcanvas(this) + const data = Data.get(this, DATA_KEY) || new Offcanvas(this, typeof config === 'object' ? config : {}) - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } + if (typeof config !== 'string') { + return + } - data[config](this) + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { + throw new TypeError(`No method named "${config}"`) } + + data[config](this) }) } } @@ -207,7 +240,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( event.preventDefault() } - if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) { + if (isDisabled(this)) { return } @@ -225,6 +258,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( } const data = Data.get(target, DATA_KEY) || new Offcanvas(target) + data.toggle(this) }) diff --git a/js/src/util/index.js b/js/src/util/index.js index ae3cd2ac00a9..e268b07287a0 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -153,6 +153,22 @@ const isVisible = element => { return false } +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true + } + + if (element.classList.contains('disabled')) { + return true + } + + if (typeof element.disabled !== 'undefined') { + return element.disabled + } + + return element.getAttribute('disabled') !== 'false' +} + const findShadowRoot = element => { if (!document.documentElement.attachShadow) { return null @@ -226,6 +242,7 @@ export { emulateTransitionEnd, typeCheckConfig, isVisible, + isDisabled, findShadowRoot, noop, reflow, diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 07a7cf682b33..4fb6c17ecb9c 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -2,7 +2,7 @@ import Offcanvas from '../../src/offcanvas' import EventHandler from '../../src/dom/event-handler' /** Test helpers */ -import { clearFixture, getFixture, jQueryMock, createEvent } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('Offcanvas', () => { let fixtureEl @@ -22,6 +22,18 @@ describe('Offcanvas', () => { }) }) + describe('Default', () => { + it('should return plugin default config', () => { + expect(Offcanvas.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Offcanvas.DATA_KEY).toEqual('bs.offcanvas') + }) + }) + describe('constructor', () => { it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => { fixtureEl.innerHTML = [ @@ -70,6 +82,68 @@ describe('Offcanvas', () => { expect(offCanvas.hide).not.toHaveBeenCalled() }) + + it('should not hide if esc is pressed but with keyboard = false', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + spyOn(offCanvas, 'hide') + + document.dispatchEvent(keyDownEsc) + + expect(offCanvas.hide).not.toHaveBeenCalled() + }) + }) + + describe('config', () => { + it('should have default values', () => { + fixtureEl.innerHTML = [ + 'Try scrolling the rest of the page to see this option in action.
.....