diff --git a/js/index.esm.js b/js/index.esm.js index 51a9ff8ea2624fc1e186c90712d66e1299652965..5d9a1655c1569d6bc1dca38f193a7e783b000957 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -6,10 +6,12 @@ */ export { default as Alert } from './src/alert' +export { default as Badge } from './src/badge' export { default as Button } from './src/button' export { default as Carousel } from './src/carousel' export { default as Collapse } from './src/collapse' export { default as Dropdown } from './src/dropdown' +export { default as InputBadges } from './src/input-badges' export { default as Modal } from './src/modal' export { default as Offcanvas } from './src/offcanvas' export { default as Popover } from './src/popover' diff --git a/js/index.umd.js b/js/index.umd.js index d6e587fb1d5443b233329148dbb6f3d9fc1cd7b9..fbb7381c92f4bd058c03d69c842a52b87bfddea0 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -6,10 +6,12 @@ */ import Alert from './src/alert' +import Badge from './src/badge' import Button from './src/button' import Carousel from './src/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' +import InputBadges from './src/input-badges' import Modal from './src/modal' import Offcanvas from './src/offcanvas' import Popover from './src/popover' @@ -20,10 +22,12 @@ import Tooltip from './src/tooltip' export default { Alert, + Badge, Button, Carousel, Collapse, Dropdown, + InputBadges, Modal, Offcanvas, Popover, diff --git a/js/src/badge.js b/js/src/badge.js new file mode 100644 index 0000000000000000000000000000000000000000..38fa88b24b9c6e1c5067bbda0361ecce171258cc --- /dev/null +++ b/js/src/badge.js @@ -0,0 +1,98 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.1.3): badge.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { defineJQueryPlugin } from './util/index' +import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' +import { enableDismissTrigger } from './util/component-functions' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'badge' +const DATA_KEY = 'bs.badge' +const EVENT_KEY = `.${DATA_KEY}` +const EVENT_CLOSE = `close${EVENT_KEY}` +const EVENT_CLOSED = `closed${EVENT_KEY}` + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Badge extends BaseComponent { + constructor(element) { + super(element) + + this.badge = null + } + + // Getters + + static get NAME() { + return NAME + } + + // Private + + _destroyElement() { + this._element.remove() + EventHandler.trigger(this._element, EVENT_CLOSED) + this.dispose() + } + + // Public + + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE) + + if (closeEvent.defaultPrevented) { + return + } + + this._queueCallback(() => this._destroyElement(), this._element, false) + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + const data = Badge.getOrCreateInstance(this, config) + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config]() + } + }) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +enableDismissTrigger(Badge, 'close') + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .badge to jQuery only if jQuery is present + */ + +defineJQueryPlugin(Badge) + +export default Badge diff --git a/js/src/input-badges.js b/js/src/input-badges.js new file mode 100644 index 0000000000000000000000000000000000000000..0be317b977164daa86706f00e00b4c67b19fdf43 --- /dev/null +++ b/js/src/input-badges.js @@ -0,0 +1,204 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.1.3): input-badges.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + defineJQueryPlugin, + typeCheckConfig +} from './util/index' +import Manipulator from './dom/manipulator' +import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' +import Badge from './badge' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'input-badges' +const DefaultType = { + rounded: 'boolean', + colour: 'string' +} + +const Default = { + rounded: false, + colour: 'primary' +} + +const COLOUR_VALUES = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'light', + 'dark' +] + +const DATA_KEY = 'bs.input-badges' +const EVENT_KEY = `.${DATA_KEY}` + +const EVENT_BADGE_ADD = `add${EVENT_KEY}` +const EVENT_BADGE_ADDED = `added${EVENT_KEY}` +const EVENT_BADGE_REMOVED = `removed${EVENT_KEY}` + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class InputBadges extends BaseComponent { + constructor(element, config) { + super(element) + + // Protected + this._config = this._getConfig(config) + + const wrapperDiv = document.createElement('div') + for (let i = 0; i < this._element.classList.length; i++) { + const className = this._element.classList[i] + wrapperDiv.classList.add(className) + } + + this._element.parentNode.insertBefore(wrapperDiv, this._element) + wrapperDiv.append(this._element) + this._element.classList.add('d-none') + + const visibleInput = document.createElement('INPUT') + visibleInput.setAttribute('type', 'text') + visibleInput.classList.add('border-0', 'w-25', 'form-control-plaintext', 'py-0', 'd-inline') + wrapperDiv.append(visibleInput) + + wrapperDiv.style.cursor = 'text' + + wrapperDiv.addEventListener('click', () => { + visibleInput.focus() + }) + + visibleInput.addEventListener('keyup', event => { + if (event.key !== 'Backspace' || visibleInput.value !== '') { + return + } + + const existingBadges = Array.from(wrapperDiv.childNodes).filter(item => { + return item.nodeName === 'SPAN' + }) + + if (existingBadges.length > 0) { + Badge.getOrCreateInstance(existingBadges[existingBadges.length - 1]).close() + EventHandler.trigger(this._element, EVENT_BADGE_REMOVED) + } + }) + + visibleInput.addEventListener('keyup', event => { + if (event.key !== 'Enter') { + return + } + + if (visibleInput.value === '') { + return + } + + const existingValue = this._element.value.split(',').find(value => value === encodeURIComponent(visibleInput.value)) + if (existingValue) { + return + } + + const addBadgeEvent = EventHandler.trigger(this._element, EVENT_BADGE_ADD) + + if (addBadgeEvent.defaultPrevented) { + return + } + + const newBadge = document.createElement('span') + newBadge.classList.add('badge', `bg-${this._config.colour}`, 'badge-dismissable') + if (this._config.rounded) { + newBadge.classList.add('rounded-pill') + } + + this._element.value += `${encodeURIComponent(visibleInput.value)},` + newBadge.textContent = visibleInput.value + const closeButton = document.createElement('button') + closeButton.setAttribute('type', 'button') + closeButton.classList.add('btn-close') + closeButton.setAttribute('data-bs-dismiss', 'badge') + closeButton.setAttribute('aria-label', 'Close') + newBadge.append(closeButton) + Badge.getOrCreateInstance(newBadge) + newBadge.addEventListener('closed.bs.badge', () => { + this._element.value = this._element.value.replace(`${encodeURIComponent(newBadge.textContent)},`, '') + EventHandler.trigger(this._element, EVENT_BADGE_REMOVED) + }) + visibleInput.value = '' + visibleInput.before(newBadge) + EventHandler.trigger(this._element, EVENT_BADGE_ADDED) + }) + } + + // Getters + + static get NAME() { + return NAME + } + + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + // Private + + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element) + + config = { + ...this.constructor.Default, + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + } + + typeCheckConfig(NAME, config, this.constructor.DefaultType) + + config.colour = COLOUR_VALUES.find(col => col === config.colour) || Default.colour + + return config + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + const data = InputBadges.getOrCreateInstance(this, config) + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config]() + } + }) + } +} + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .input-badges to jQuery only if jQuery is present + */ + +defineJQueryPlugin(InputBadges) + +export default InputBadges diff --git a/js/tests/unit/badge.spec.js b/js/tests/unit/badge.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8e65d2fddaf14703e374f159df06d2e47ac1d7fc --- /dev/null +++ b/js/tests/unit/badge.spec.js @@ -0,0 +1,232 @@ +import Badge from '../../src/badge' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' + +describe('Badge', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + it('should take care of element either passed as a CSS selector or DOM element', () => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = fixtureEl.querySelector('.badge') + const badgeBySelector = new Badge('.badge') + const badgeByElement = new Badge(badgeEl) + + expect(badgeBySelector._element).toEqual(badgeEl) + expect(badgeByElement._element).toEqual(badgeEl) + }) + + it('should return version', () => { + expect(Badge.VERSION).toEqual(jasmine.any(String)) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Badge.DATA_KEY).toEqual('bs.badge') + }) + }) + + describe('data-api', () => { + it('should close a badge without instantiating it manually', () => { + fixtureEl.innerHTML = [ + '<div class="badge">', + ' <button type="button" data-bs-dismiss="badge">x</button>', + '</div>' + ].join('') + + const button = document.querySelector('button') + + button.click() + expect(document.querySelectorAll('.badge')).toHaveSize(0) + }) + + it('should close a badge without instantiating it manually with the parent selector', () => { + fixtureEl.innerHTML = [ + '<div class="badge">', + ' <button type="button" data-bs-target=".badge" data-bs-dismiss="badge">x</button>', + '</div>' + ].join('') + + const button = document.querySelector('button') + + button.click() + expect(document.querySelectorAll('.badge')).toHaveSize(0) + }) + }) + + describe('close', () => { + it('should close a badge', done => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = document.querySelector('.badge') + const badge = new Badge(badgeEl) + + badgeEl.addEventListener('closed.bs.badge', () => { + expect(document.querySelectorAll('.badge')).toHaveSize(0) + done() + }) + + badge.close() + }) + + it('should not remove badge if close event is prevented', done => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const getBadge = () => document.querySelector('.badge') + const badgeEl = getBadge() + const badge = new Badge(badgeEl) + + badgeEl.addEventListener('close.bs.badge', event => { + event.preventDefault() + setTimeout(() => { + expect(getBadge()).not.toBeNull() + done() + }, 10) + }) + + badgeEl.addEventListener('closed.bs.badge', () => { + throw new Error('should not fire closed event') + }) + + badge.close() + }) + }) + + describe('dispose', () => { + it('should dispose a badge', () => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = document.querySelector('.badge') + const badge = new Badge(badgeEl) + + expect(Badge.getInstance(badgeEl)).not.toBeNull() + + badge.dispose() + + expect(Badge.getInstance(badgeEl)).toBeNull() + }) + }) + + describe('jQueryInterface', () => { + it('should handle config passed and toggle existing badge', () => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = fixtureEl.querySelector('.badge') + const badge = new Badge(badgeEl) + + spyOn(badge, 'close') + + jQueryMock.fn.badge = Badge.jQueryInterface + jQueryMock.elements = [badgeEl] + + jQueryMock.fn.badge.call(jQueryMock, 'close') + + expect(badge.close).toHaveBeenCalled() + }) + + it('should create new badge instance and call close', () => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = fixtureEl.querySelector('.badge') + + jQueryMock.fn.badge = Badge.jQueryInterface + jQueryMock.elements = [badgeEl] + + expect(Badge.getInstance(badgeEl)).toBeNull() + jQueryMock.fn.badge.call(jQueryMock, 'close') + + expect(fixtureEl.querySelector('.badge')).toBeNull() + }) + + it('should just create a badge instance without calling close', () => { + fixtureEl.innerHTML = '<div class="badge"></div>' + + const badgeEl = fixtureEl.querySelector('.badge') + + jQueryMock.fn.badge = Badge.jQueryInterface + jQueryMock.elements = [badgeEl] + + jQueryMock.fn.badge.call(jQueryMock) + + expect(Badge.getInstance(badgeEl)).not.toBeNull() + expect(fixtureEl.querySelector('.badge')).not.toBeNull() + }) + + it('should throw an error on undefined method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.badge = Badge.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.badge.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw an error on protected method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const action = '_getConfig' + + jQueryMock.fn.badge = Badge.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.badge.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + }) + + describe('getInstance', () => { + it('should return badge instance', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const badge = new Badge(div) + + expect(Badge.getInstance(div)).toEqual(badge) + expect(Badge.getInstance(div)).toBeInstanceOf(Badge) + }) + + it('should return null when there is no badge instance', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + + expect(Badge.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return badge instance', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const badge = new Badge(div) + + expect(Badge.getOrCreateInstance(div)).toEqual(badge) + expect(Badge.getInstance(div)).toEqual(Badge.getOrCreateInstance(div, {})) + expect(Badge.getOrCreateInstance(div)).toBeInstanceOf(Badge) + }) + + it('should return new instance when there is no badge instance', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + + expect(Badge.getInstance(div)).toBeNull() + expect(Badge.getOrCreateInstance(div)).toBeInstanceOf(Badge) + }) + }) +}) diff --git a/js/tests/visual/badge.html b/js/tests/visual/badge.html new file mode 100644 index 0000000000000000000000000000000000000000..a3793d46052300fcd85e8c964d7db5a7ede528fc --- /dev/null +++ b/js/tests/visual/badge.html @@ -0,0 +1,152 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link href="../../../dist/css/bootstrap.min.css" rel="stylesheet"> + <title>Badge</title> + </head> + <body> + <div class="container"> + <h1>Badge <small>Bootstrap Visual Test</small></h1> + + <span class="badge bg-primary badge-dismissable" role="badge"> + Primary + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-secondary badge-dismissable" role="badge"> + Secondary + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-success badge-dismissable" role="badge"> + Success + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-danger badge-dismissable" role="badge"> + Danger + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-warning text-dark badge-dismissable" role="badge"> + Warning + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-info text-dark badge-dismissable" role="badge"> + Info + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-light text-dark badge-dismissable" role="badge"> + Light + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge bg-dark badge-dismissable" role="badge"> + Dark + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + + <span class="badge rounded-pill badge-dismissable bg-primary" role="badge"> + Primary + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-secondary" role="badge"> + Secondary + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-success" role="badge"> + Success + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-danger" role="badge"> + Danger + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-warning text-dark" role="badge"> + Warning + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-info text-dark" role="badge"> + Info + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-light text-dark" role="badge"> + Light + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + <span class="badge rounded-pill badge-dismissable bg-dark" role="badge"> + Dark + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + </span> + + <span class="badge bg-primary badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Primary + </span> + <span class="badge bg-secondary badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Secondary + </span> + <span class="badge bg-success badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Success + </span> + <span class="badge bg-danger badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Danger + </span> + <span class="badge bg-warning text-dark badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Warning + </span> + <span class="badge bg-info text-dark badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Info + </span> + <span class="badge bg-light text-dark badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Light + </span> + <span class="badge bg-dark badge-dismissable" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Dark + </span> + + <span class="badge rounded-pill badge-dismissable bg-primary" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Primary + </span> + <span class="badge rounded-pill badge-dismissable bg-secondary" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Secondary + </span> + <span class="badge rounded-pill badge-dismissable bg-success" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Success + </span> + <span class="badge rounded-pill badge-dismissable bg-danger" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Danger + </span> + <span class="badge rounded-pill badge-dismissable bg-warning text-dark" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Warning + </span> + <span class="badge rounded-pill badge-dismissable bg-info text-dark" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Info + </span> + <span class="badge rounded-pill badge-dismissable bg-light text-dark" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Light + </span> + <span class="badge rounded-pill badge-dismissable bg-dark" role="badge"> + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> + Dark + </span> + </div> + + <script src="../../dist/dom/event-handler.js"></script> + <script src="../../dist/dom/selector-engine.js"></script> + <script src="../../dist/dom/data.js"></script> + <script src="../../dist/base-component.js"></script> + <script src="../../dist/badge.js"></script> + </body> +</html> diff --git a/scss/_badge.scss b/scss/_badge.scss index 08df1b84a7e625d4f0995d111ff179da692df20a..0f338275b072e50810fd3c778fafdf12451f0b85 100644 --- a/scss/_badge.scss +++ b/scss/_badge.scss @@ -4,6 +4,7 @@ // `background-color`. .badge { + position: relative; display: inline-block; padding: $badge-padding-y $badge-padding-x; @include font-size($badge-font-size); @@ -22,6 +23,19 @@ } } +.badge-dismissable { + padding-right: $badge-dismissible-padding-r; + + // Adjust close link position + .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: $stretched-link-z-index + 1; + padding: $badge-padding-y $badge-padding-x * .77; + } +} + // Quick fix for badges in buttons .btn .badge { position: relative; diff --git a/scss/_variables.scss b/scss/_variables.scss index 9db64ae7228b1a6ee4a6018ba0aa985d735aafbe..6a490d10f0c43bee0576b4abe6264095c97f1f1d 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1356,6 +1356,7 @@ $badge-color: $white !default; $badge-padding-y: .35em !default; $badge-padding-x: .65em !default; $badge-border-radius: $border-radius !default; +$badge-dismissible-padding-r: $badge-padding-x * 3 !default; // 3x covers width of x plus default padding on either side // scss-docs-end badge-variables diff --git a/scss/forms/_form-control.scss b/scss/forms/_form-control.scss index 51b3baa838603ee1b1af1f7cce6cbb36be5bbd4a..eaa9ed16aadc059b339d19b572e2f63658c5329f 100644 --- a/scss/forms/_form-control.scss +++ b/scss/forms/_form-control.scss @@ -186,3 +186,9 @@ textarea { @include border-radius($input-border-radius); } } + +.form-control .badge { + margin-top: 2px; + margin-right: 2px; + vertical-align: top; +} diff --git a/site/assets/js/application.js b/site/assets/js/application.js index 2c57906c9fbd34906a14c8d83f6ab162794f8050..731e76f63e373471fd09e82e1bb825d2ff24d2ce 100644 --- a/site/assets/js/application.js +++ b/site/assets/js/application.js @@ -23,6 +23,16 @@ }) }) + document.querySelectorAll('input[type="text"][data-bs-badges="input-badges"]') + .forEach(function (inputBadge) { + new bootstrap.InputBadges(inputBadge) + }) + + document.querySelectorAll('.badge') + .forEach(function (badge) { + new bootstrap.Badge(badge) + }) + document.querySelectorAll('[data-bs-toggle="popover"]') .forEach(function (popover) { new bootstrap.Popover(popover) diff --git a/site/content/docs/5.1/components/badge.md b/site/content/docs/5.1/components/badge.md index de80d3b27a59cbde392250644308742829587d6c..4298827fc0f6c31bfb8cce5f46d61853275d8982 100644 --- a/site/content/docs/5.1/components/badge.md +++ b/site/content/docs/5.1/components/badge.md @@ -60,6 +60,27 @@ You can also replace the `.badge` class with a few more utilities without a coun </button> {{< /example >}} +### Dismissing + +Using the badge JavaScript plugin, it's possible to dismiss any badge inline. Here's how: + +- Be sure you've loaded the badge plugin, or the compiled Bootstrap JavaScript. +- Add a [close button]({{< docsref "/components/close-button" >}}) and the `.badge-dismissible` class, which adds extra padding to the right of the badge and positions the close button. +- On the close button, add the `data-bs-dismiss="badge"` attribute, which triggers the JavaScript functionality. Be sure to use the `<button>` element with it for proper behavior across all devices. + +You can see this in action with a live demo: + +{{< example >}} +<span class="badge bg-primary badge-dismissable" role="badge"> + Primary + <button type="button" class="btn-close" data-bs-dismiss="badge" aria-label="Close"></button> +</span> +{{< /example >}} + +{{< callout warning >}} +When an badge is dismissed, the element is completely removed from the page structure. If a keyboard user dismisses the badge using the close button, their focus will suddenly be lost and, depending on the browser, reset to the start of the page/document. For this reason, we recommend including additional JavaScript that listens for the `closed.bs.badge` event and programmatically sets `focus()` to the most appropriate location in the page. If you're planning to move focus to a non-interactive element that normally does not receive focus, make sure to add `tabindex="-1"` to the element. +{{< /callout >}} + ## Background colors Use our background utility classes to quickly change the appearance of a badge. Please note that when using Bootstrap's default `.bg-light`, you'll likely need a text color utility like `.text-dark` for proper styling. This is because background utilities do not set anything but `background-color`. @@ -91,3 +112,116 @@ Use the `.rounded-pill` utility class to make badges more rounded with a larger ### Variables {{< scss-docs name="badge-variables" file="scss/_variables.scss" >}} + +## JavaScript behavior + +### Initialize + +Initialize elements as badges + +```js +var badgeList = document.querySelectorAll('.badge') +var badges = Array.prototype.slice.call(badgeList).map(function (element) { + return new bootstrap.Badge(element) +}) +``` + +{{< callout info >}} +For the sole purpose of dismissing a badge, it isn't necessary to initialize the component manually via the JS API. By making use of `data-bs-dismiss="badge"`, the component will be initialized automatically and properly dismissed. + +See the [triggers](#triggers) section for more details. +{{< /callout >}} + +### Triggers + +{{% js-dismiss "badge" %}} + +**Note that closing an badge will remove it from the DOM.** + +### Methods + +<table class="table"> + <thead> + <tr> + <th>Method</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr> + <td> + <code>close</code> + </td> + <td> + Closes an badge by removing it from the DOM. If the <code>.fade</code> and <code>.show</code> classes are present on the element, the badge will fade out before it is removed. + </td> + </tr> + <tr> + <td> + <code>dispose</code> + </td> + <td> + Destroys an element's badge. (Removes stored data on the DOM element) + </td> + </tr> + <tr> + <td> + <code>getInstance</code> + </td> + <td> + Static method which allows you to get the badge instance associated to a DOM element, you can use it like this: <code>bootstrap.Badge.getInstance(badge)</code> + </td> + </tr> + <tr> + <td> + <code>getOrCreateInstance</code> + </td> + <td> + Static method which returns an badge instance associated to a DOM element or create a new one in case it wasn't initialized. + You can use it like this: <code>bootstrap.Badge.getOrCreateInstance(element)</code> + </td> + </tr> + </tbody> +</table> + +```js +var badgeNode = document.querySelector('.badge') +var badge = bootstrap.Badge.getInstance(badgeNode) +badge.close() +``` + +### Events + +Bootstrap's badge plugin exposes a few events for hooking into badge functionality. + +<table class="table"> + <thead> + <tr> + <th>Event</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr> + <td><code>close.bs.badge</code></td> + <td> + Fires immediately when the <code>close</code> instance method is called. + </td> + </tr> + <tr> + <td><code>closed.bs.badge</code></td> + <td> + Fired when the badge has been closed and CSS transitions have completed. + </td> + </tr> + </tbody> +</table> + +```js +var myBadge = document.getElementById('myBadge') +myBadge.addEventListener('closed.bs.badge', function () { + // do something, for instance, explicitly move focus to the most appropriate element, + // so it doesn't get lost/reset to the start of the page + // document.getElementById('...').focus() +}) +``` diff --git a/site/content/docs/5.1/components/input-badges.md b/site/content/docs/5.1/components/input-badges.md new file mode 100644 index 0000000000000000000000000000000000000000..e58130abb273be74eed2e1bbd82949d81b70f34b --- /dev/null +++ b/site/content/docs/5.1/components/input-badges.md @@ -0,0 +1,117 @@ +--- +layout: docs +title: Input Badges +description: Input badges can be used to represent small blocks of information. +group: components +toc: true +--- + +## How it works + +The input badges uses dismissable [badge]({{< docsref "/components/badge" >}}) internally to provide the functionality. + +## Example + +Add tags below by pressing "Enter" and click on the cross button to remove them. + +{{< example >}} +<input class="form-control" type="text" data-bs-badges="input-badges" data-bs-colour="secondary" data-bs-rounded="false"> +{{< /example >}} + +## JavaScript behavior + +### Initialize + +Initialize elements as input badges + +```js +var inputBadgesList = document.querySelectorAll('input[type="text"][data-bs-badges="input-badges"]') +var inputBadges = Array.prototype.slice.call(inputBadgesList).map(function (element) { + return new bootstrap.InputBadges(element) +}) +``` + +### Methods + +<table class="table"> + <thead> + <tr> + <th>Method</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr> + <td> + <code>dispose</code> + </td> + <td> + Destroys an element's badge. (Removes stored data on the DOM element) + </td> + </tr> + <tr> + <td> + <code>getInstance</code> + </td> + <td> + Static method which allows you to get the badge instance associated to a DOM element, you can use it like this: <code>bootstrap.Badge.getInstance(badge)</code> + </td> + </tr> + <tr> + <td> + <code>getOrCreateInstance</code> + </td> + <td> + Static method which returns an badge instance associated to a DOM element or create a new one in case it wasn't initialized. + You can use it like this: <code>bootstrap.Badge.getOrCreateInstance(element)</code> + </td> + </tr> + </tbody> +</table> + +```js +var inputBadgesNode = document.querySelector('input[type="text"][data-bs-badges="input-badges"]') +var inputBadge = bootstrap.InputBadges.getInstance(inputBadgesNode) +inputBadge.dispose() +``` + +### Events + +Bootstrap's input badges plugin exposes a few events for hooking into input badges functionality. + +<table class="table"> + <thead> + <tr> + <th>Event</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr> + <td><code>add.bs.input-badges</code></td> + <td> + Fires immediately 'Enter' is pressed and the input value is new and not empty. + </td> + </tr> + <tr> + <td><code>added.bs.input-badges</code></td> + <td> + Fires immediately after a new badge is added to the list. + </td> + </tr> + <tr> + <td><code>removed.bs.input-badges</code></td> + <td> + Fired after a badge is removed from the list. + </td> + </tr> + </tbody> +</table> + +```js +var inputBadges = document.getElementById('inputBadges') +inputBadges.addEventListener('add.bs.input-badges', function () { + // do something, for instance, check the value to be added is valid + // so it doesn't get to the field +}) +``` diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index df95692afa3894b4404a427b98da4d4b1d04219e..9b6ef8dc05550810e07d4529604bc89e49b35bbe 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -69,6 +69,7 @@ - title: Close button - title: Collapse - title: Dropdowns + - title: Input Badges - title: List group - title: Modal - title: Navs & tabs