Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/twbs/bootstrap.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeoSot <geo.sotis@gmail.com>2021-11-25 20:14:02 +0300
committerGitHub <noreply@github.com>2021-11-25 20:14:02 +0300
commit94a596fbcb1011ba990da2078ba7e20b39dba2d9 (patch)
tree26af41580d5cae017e32e29cfef96178e897afa6
parentfa33e83f25faf8c378b99126fbd69977e667ad9a (diff)
Add a template factory helper to handle all template cases (#34519)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
-rw-r--r--.bundlewatch.config.json2
-rw-r--r--js/src/popover.js10
-rw-r--r--js/src/tooltip.js129
-rw-r--r--js/src/util/template-factory.js161
-rw-r--r--js/tests/unit/popover.spec.js7
-rw-r--r--js/tests/unit/tooltip.spec.js67
-rw-r--r--js/tests/unit/util/template-factory.spec.js305
-rw-r--r--site/assets/js/application.js18
-rw-r--r--site/content/docs/5.1/components/popovers.md15
-rw-r--r--site/content/docs/5.1/components/tooltips.md11
10 files changed, 600 insertions, 125 deletions
diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 316976ee9c..8749403007 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -46,7 +46,7 @@
},
{
"path": "./dist/js/bootstrap.esm.min.js",
- "maxSize": "18.25 kB"
+ "maxSize": "18.5 kB"
},
{
"path": "./dist/js/bootstrap.js",
diff --git a/js/src/popover.js b/js/src/popover.js
index 144ec1cad5..0b255a585e 100644
--- a/js/src/popover.js
+++ b/js/src/popover.js
@@ -78,12 +78,14 @@ class Popover extends Tooltip {
return this.getTitle() || this._getContent()
}
- setContent(tip) {
- this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TITLE)
- this._sanitizeAndSetContent(tip, this._getContent(), SELECTOR_CONTENT)
+ // Private
+ _getContentForTemplate() {
+ return {
+ [SELECTOR_TITLE]: this.getTitle(),
+ [SELECTOR_CONTENT]: this._getContent()
+ }
}
- // Private
_getContent() {
return this._resolvePossibleFunction(this._config.content)
}
diff --git a/js/src/tooltip.js b/js/src/tooltip.js
index f069dc7515..c845961011 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -11,17 +11,16 @@ import {
findShadowRoot,
getElement,
getUID,
- isElement,
isRTL,
noop,
typeCheckConfig
} from './util/index'
-import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer'
+import { DefaultAllowlist } from './util/sanitizer'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
-import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
+import TemplateFactory from './util/template-factory'
/**
* Constants
@@ -40,6 +39,7 @@ const CLASS_NAME_SHOW = 'show'
const HOVER_STATE_SHOW = 'show'
const HOVER_STATE_OUT = 'out'
+const SELECTOR_TOOLTIP_ARROW = '.tooltip-arrow'
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
@@ -132,6 +132,7 @@ class Tooltip extends BaseComponent {
this._hoverState = ''
this._activeTrigger = {}
this._popper = null
+ this._templateFactory = null
// Protected
this._config = this._getConfig(config)
@@ -227,23 +228,9 @@ class Tooltip extends BaseComponent {
return
}
- // A trick to recreate a tooltip in case a new title is given by using the NOT documented `data-bs-original-title`
- // This will be removed later in favor of a `setContent` method
- if (this.constructor.NAME === 'tooltip' && this.tip && this.getTitle() !== this.tip.querySelector(SELECTOR_TOOLTIP_INNER).innerHTML) {
- this._disposePopper()
- this.tip.remove()
- this.tip = null
- }
-
const tip = this.getTipElement()
- const tipId = getUID(this.constructor.NAME)
-
- tip.setAttribute('id', tipId)
- this._element.setAttribute('aria-describedby', tipId)
- if (this._config.animation) {
- tip.classList.add(CLASS_NAME_FADE)
- }
+ this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
const placement = typeof this._config.placement === 'function' ?
this._config.placement.call(this, tip, this._element) :
@@ -268,11 +255,6 @@ class Tooltip extends BaseComponent {
tip.classList.add(CLASS_NAME_SHOW)
- const customClass = this._resolvePossibleFunction(this._config.customClass)
- if (customClass) {
- tip.classList.add(...customClass.split(' '))
- }
-
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
@@ -360,69 +342,63 @@ class Tooltip extends BaseComponent {
return this.tip
}
- const element = document.createElement('div')
- element.innerHTML = this._config.template
+ const templateFactory = this._getTemplateFactory(this._getContentForTemplate())
- const tip = element.children[0]
- this.setContent(tip)
+ const tip = templateFactory.toHtml()
tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
- this.tip = tip
- return this.tip
- }
-
- setContent(tip) {
- this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER)
- }
+ const tipId = getUID(this.constructor.NAME).toString()
- _sanitizeAndSetContent(template, content, selector) {
- const templateElement = SelectorEngine.findOne(selector, template)
+ tip.setAttribute('id', tipId)
- if (!content && templateElement) {
- templateElement.remove()
- return
+ if (this._config.animation) {
+ tip.classList.add(CLASS_NAME_FADE)
}
- // we use append for html objects to maintain js events
- this.setElementContent(templateElement, content)
+ this.tip = tip
+ return this.tip
}
- setElementContent(element, content) {
- if (element === null) {
- return
+ setContent(content) {
+ let isShown = false
+ if (this.tip) {
+ isShown = this.tip.classList.contains(CLASS_NAME_SHOW)
+ this.tip.remove()
}
- if (isElement(content)) {
- content = getElement(content)
+ this._disposePopper()
- // content is a DOM node or a jQuery
- if (this._config.html) {
- if (content.parentNode !== element) {
- element.innerHTML = ''
- element.append(content)
- }
- } else {
- element.textContent = content.textContent
- }
+ this.tip = this._getTemplateFactory(content).toHtml()
- return
+ if (isShown) {
+ this.show()
}
+ }
- if (this._config.html) {
- if (this._config.sanitize) {
- content = sanitizeHtml(content, this._config.allowList, this._config.sanitizeFn)
- }
-
- element.innerHTML = content // lgtm [js/xss-through-dom]
+ _getTemplateFactory(content) {
+ if (this._templateFactory) {
+ this._templateFactory.changeContent(content)
} else {
- element.textContent = content
+ this._templateFactory = new TemplateFactory({
+ ...this._config,
+ // the `content` var has to be after `this._config`
+ // to override config.content in case of popover
+ content,
+ extraClass: this._resolvePossibleFunction(this._config.customClass)
+ })
}
+
+ return this._templateFactory
}
- getTitle() {
- const title = this._element.getAttribute('data-bs-original-title') || this._config.title
+ _getContentForTemplate() {
+ return {
+ [SELECTOR_TOOLTIP_INNER]: this.getTitle()
+ }
+ }
- return this._resolvePossibleFunction(title)
+ getTitle() {
+ return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('title')
}
updateAttachment(attachment) {
@@ -456,8 +432,8 @@ class Tooltip extends BaseComponent {
return offset
}
- _resolvePossibleFunction(content) {
- return typeof content === 'function' ? content.call(this._element) : content
+ _resolvePossibleFunction(arg) {
+ return typeof arg === 'function' ? arg.call(this._element) : arg
}
_getPopperConfig(attachment) {
@@ -485,7 +461,7 @@ class Tooltip extends BaseComponent {
{
name: 'arrow',
options: {
- element: `.${this.constructor.NAME}-arrow`
+ element: SELECTOR_TOOLTIP_ARROW
}
},
{
@@ -556,15 +532,9 @@ class Tooltip extends BaseComponent {
_fixTitle() {
const title = this._element.getAttribute('title')
- const originalTitleType = typeof this._element.getAttribute('data-bs-original-title')
- if (title || originalTitleType !== 'string') {
- this._element.setAttribute('data-bs-original-title', title || '')
- if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) {
- this._element.setAttribute('aria-label', title)
- }
-
- this._element.setAttribute('title', '')
+ if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) {
+ this._element.setAttribute('aria-label', title)
}
}
@@ -670,11 +640,6 @@ class Tooltip extends BaseComponent {
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
-
- if (config.sanitize) {
- config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn)
- }
-
return config
}
diff --git a/js/src/util/template-factory.js b/js/src/util/template-factory.js
new file mode 100644
index 0000000000..a9cee1086c
--- /dev/null
+++ b/js/src/util/template-factory.js
@@ -0,0 +1,161 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.3): util/template-factory.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import { DefaultAllowlist, sanitizeHtml } from './sanitizer'
+import { getElement, isElement, typeCheckConfig } from '../util/index'
+import SelectorEngine from '../dom/selector-engine'
+
+/**
+ * Constants
+ */
+
+const NAME = 'TemplateFactory'
+
+const Default = {
+ extraClass: '',
+ template: '<div></div>',
+ content: {}, // { selector : text , selector2 : text2 , }
+ html: false,
+ sanitize: true,
+ sanitizeFn: null,
+ allowList: DefaultAllowlist
+}
+
+const DefaultType = {
+ extraClass: '(string|function)',
+ template: 'string',
+ content: 'object',
+ html: 'boolean',
+ sanitize: 'boolean',
+ sanitizeFn: '(null|function)',
+ allowList: 'object'
+}
+
+const DefaultContentType = {
+ selector: '(string|element)',
+ entry: '(string|element|function|null)'
+}
+
+/**
+ * Class definition
+ */
+
+class TemplateFactory {
+ constructor(config) {
+ this._config = this._getConfig(config)
+ }
+
+ // Getters
+ static get NAME() {
+ return NAME
+ }
+
+ static get Default() {
+ return Default
+ }
+
+ // Public
+ getContent() {
+ return Object.values(this._config.content)
+ .map(config => this._resolvePossibleFunction(config))
+ .filter(Boolean)
+ }
+
+ hasContent() {
+ return this.getContent().length > 0
+ }
+
+ changeContent(content) {
+ this._checkContent(content)
+ this._config.content = { ...this._config.content, ...content }
+ return this
+ }
+
+ toHtml() {
+ const templateWrapper = document.createElement('div')
+ templateWrapper.innerHTML = this._maybeSanitize(this._config.template)
+
+ for (const [selector, text] of Object.entries(this._config.content)) {
+ this._setContent(templateWrapper, text, selector)
+ }
+
+ const template = templateWrapper.children[0]
+ const extraClass = this._resolvePossibleFunction(this._config.extraClass)
+
+ if (extraClass) {
+ template.classList.add(...extraClass.split(' '))
+ }
+
+ return template
+ }
+
+ // Private
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...(typeof config === 'object' ? config : {})
+ }
+
+ typeCheckConfig(NAME, config, DefaultType)
+ this._checkContent(config.content)
+
+ return config
+ }
+
+ _checkContent(arg) {
+ for (const [selector, content] of Object.entries(arg)) {
+ typeCheckConfig(NAME, { selector, entry: content }, DefaultContentType)
+ }
+ }
+
+ _setContent(template, content, selector) {
+ const templateElement = SelectorEngine.findOne(selector, template)
+
+ if (!templateElement) {
+ return
+ }
+
+ content = this._resolvePossibleFunction(content)
+
+ if (!content) {
+ templateElement.remove()
+ return
+ }
+
+ if (isElement(content)) {
+ this._putElementInTemplate(getElement(content), templateElement)
+ return
+ }
+
+ if (this._config.html) {
+ templateElement.innerHTML = this._maybeSanitize(content)
+ return
+ }
+
+ templateElement.textContent = content
+ }
+
+ _maybeSanitize(arg) {
+ return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg
+ }
+
+ _resolvePossibleFunction(arg) {
+ return typeof arg === 'function' ? arg(this) : arg
+ }
+
+ _putElementInTemplate(element, templateElement) {
+ if (this._config.html) {
+ templateElement.innerHTML = ''
+ templateElement.append(element)
+ return
+ }
+
+ templateElement.textContent = element.textContent
+ }
+}
+
+export default TemplateFactory
diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js
index 4452a132d4..b3bba3180e 100644
--- a/js/tests/unit/popover.spec.js
+++ b/js/tests/unit/popover.spec.js
@@ -162,8 +162,8 @@ describe('Popover', () => {
const popover = new Popover(popoverEl, {
content: 'Popover content'
})
-
- const spy = spyOn(popover, 'setContent').and.callThrough()
+ expect(popover._templateFactory).toBeNull()
+ let spy = null
let times = 1
popoverEl.addEventListener('hidden.bs.popover', () => {
@@ -171,11 +171,12 @@ describe('Popover', () => {
})
popoverEl.addEventListener('shown.bs.popover', () => {
+ spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough()
const popoverDisplayed = document.querySelector('.popover')
expect(popoverDisplayed).not.toBeNull()
expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content')
- expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toHaveBeenCalledTimes(0)
if (times > 1) {
done()
}
diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js
index 0cca4acff8..3c28cd837f 100644
--- a/js/tests/unit/tooltip.spec.js
+++ b/js/tests/unit/tooltip.spec.js
@@ -1041,7 +1041,7 @@ describe('Tooltip', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
const tooltipEl = fixtureEl.querySelector('a')
- const tooltip = new Tooltip(tooltipEl)
+ const tooltip = new Tooltip(tooltipEl, { animation: false })
const tip = tooltip.getTipElement()
@@ -1051,6 +1051,35 @@ describe('Tooltip', () => {
expect(tip.classList.contains('fade')).toEqual(false)
expect(tip.querySelector('.tooltip-inner').textContent).toEqual('Another tooltip')
})
+
+ it('should re-show tip if it was already shown', () => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ tooltip.show()
+ const tip = () => tooltip.getTipElement()
+
+ expect(tip().classList.contains('show')).toEqual(true)
+ tooltip.setContent({ '.tooltip-inner': 'foo' })
+
+ expect(tip().classList.contains('show')).toEqual(true)
+ expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo')
+ })
+
+ it('should keep tip hidden, if it was already hidden before', () => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ const tip = () => tooltip.getTipElement()
+
+ expect(tip().classList.contains('show')).toEqual(false)
+ tooltip.setContent({ '.tooltip-inner': 'foo' })
+
+ expect(tip().classList.contains('show')).toEqual(false)
+ expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo')
+ })
})
describe('updateAttachment', () => {
@@ -1087,34 +1116,17 @@ describe('Tooltip', () => {
})
})
- describe('setElementContent', () => {
+ describe('setContent', () => {
it('should do nothing if the element is null', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
- tooltip.setElementContent(null, null)
+ tooltip.setContent({ '.tooltip': null })
expect().nothing()
})
- it('should add the content as a child of the element', () => {
- fixtureEl.innerHTML = [
- '<a href="#" rel="tooltip" title="Another tooltip">',
- '<div id="childContent"></div>'
- ].join('')
-
- const tooltipEl = fixtureEl.querySelector('a')
- const childContent = fixtureEl.querySelector('div')
- const tooltip = new Tooltip(tooltipEl, {
- html: true
- })
-
- tooltip.setElementContent(tooltip.getTipElement(), childContent)
-
- expect(childContent.parentNode).toEqual(tooltip.getTipElement())
- })
-
it('should do nothing if the content is a child of the element', () => {
fixtureEl.innerHTML = [
'<a href="#" rel="tooltip" title="Another tooltip">',
@@ -1128,7 +1140,7 @@ describe('Tooltip', () => {
})
tooltip.getTipElement().append(childContent)
- tooltip.setElementContent(tooltip.getTipElement(), childContent)
+ tooltip.setContent({ '.tooltip': childContent })
expect().nothing()
})
@@ -1145,7 +1157,7 @@ describe('Tooltip', () => {
html: true
})
- tooltip.setElementContent(tooltip.getTipElement(), { 0: childContent, jquery: 'jQuery' })
+ tooltip.setContent({ '.tooltip': { 0: childContent, jquery: 'jQuery' } })
expect(childContent.parentNode).toEqual(tooltip.getTipElement())
})
@@ -1160,7 +1172,7 @@ describe('Tooltip', () => {
const childContent = fixtureEl.querySelector('div')
const tooltip = new Tooltip(tooltipEl)
- tooltip.setElementContent(tooltip.getTipElement(), childContent)
+ tooltip.setContent({ '.tooltip': childContent })
expect(childContent.textContent).toEqual(tooltip.getTipElement().textContent)
})
@@ -1174,7 +1186,7 @@ describe('Tooltip', () => {
html: true
})
- tooltip.setElementContent(tooltip.getTipElement(), '<div id="childContent">Tooltip</div>')
+ tooltip.setContent({ '.tooltip': '<div id="childContent">Tooltip</div>' })
expect(tooltip.getTipElement().querySelector('div').id).toEqual('childContent')
})
@@ -1187,12 +1199,13 @@ describe('Tooltip', () => {
html: true
})
- tooltip.setElementContent(tooltip.getTipElement(), [
+ const content = [
'<div id="childContent">',
' <button type="button">test btn</button>',
'</div>'
- ].join(''))
+ ].join('')
+ tooltip.setContent({ '.tooltip': content })
expect(tooltip.getTipElement().querySelector('div').id).toEqual('childContent')
expect(tooltip.getTipElement().querySelector('button')).toEqual(null)
})
@@ -1203,7 +1216,7 @@ describe('Tooltip', () => {
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
- tooltip.setElementContent(tooltip.getTipElement(), 'test')
+ tooltip.setContent({ '.tooltip': 'test' })
expect(tooltip.getTipElement().textContent).toEqual('test')
})
diff --git a/js/tests/unit/util/template-factory.spec.js b/js/tests/unit/util/template-factory.spec.js
new file mode 100644
index 0000000000..842c480c2b
--- /dev/null
+++ b/js/tests/unit/util/template-factory.spec.js
@@ -0,0 +1,305 @@
+import { clearFixture, getFixture } from '../../helpers/fixture'
+import TemplateFactory from '../../../src/util/template-factory'
+
+describe('TemplateFactory', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('NAME', () => {
+ it('should return plugin NAME', () => {
+ expect(TemplateFactory.NAME).toEqual('TemplateFactory')
+ })
+ })
+
+ describe('Default', () => {
+ it('should return plugin default config', () => {
+ expect(TemplateFactory.Default).toEqual(jasmine.any(Object))
+ })
+ })
+
+ describe('toHtml', () => {
+ describe('Sanitization', () => {
+ it('should use "sanitizeHtml" to sanitize template', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ template: '<div><a href="javascript:alert(7)">Click me</a></div>'
+ })
+ const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
+
+ expect(factory.toHtml().innerHTML).not.toContain('href="javascript:alert(7)')
+ expect(spy).toHaveBeenCalled()
+ })
+
+ it('should not sanitize template', () => {
+ const factory = new TemplateFactory({
+ sanitize: false,
+ template: '<div><a href="javascript:alert(7)">Click me</a></div>'
+ })
+ const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
+
+ expect(factory.toHtml().innerHTML).toContain('href="javascript:alert(7)')
+ expect(spy).toHaveBeenCalled()
+ })
+
+ it('should use "sanitizeHtml" to sanitize content', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ html: true,
+ template: '<div id="foo"></div>',
+ content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
+ })
+ expect(factory.toHtml().innerHTML).not.toContain('href="javascript:alert(7)')
+ })
+
+ it('should not sanitize content', () => {
+ const factory = new TemplateFactory({
+ sanitize: false,
+ html: true,
+ template: '<div id="foo"></div>',
+ content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
+ })
+ expect(factory.toHtml().innerHTML).toContain('href="javascript:alert(7)')
+ })
+
+ it('should sanitize content only if "config.html" is enabled', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ html: false,
+ template: '<div id="foo"></div>',
+ content: { '#foo': '<a href="javascript:alert(7)">Click me</a>' }
+ })
+ const spy = spyOn(factory, '_maybeSanitize').and.callThrough()
+
+ expect(spy).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Extra Class', () => {
+ it('should add extra class', () => {
+ const factory = new TemplateFactory({
+ extraClass: 'testClass'
+ })
+ expect(factory.toHtml().classList.contains('testClass')).toBeTrue()
+ })
+
+ it('should add extra classes', () => {
+ const factory = new TemplateFactory({
+ extraClass: 'testClass testClass2'
+ })
+ expect(factory.toHtml().classList.contains('testClass')).toBeTrue()
+ expect(factory.toHtml().classList.contains('testClass2')).toBeTrue()
+ })
+
+ it('should resolve class if function is given', () => {
+ const factory = new TemplateFactory({
+ extraClass: arg => {
+ expect(arg).toEqual(factory)
+ return 'testClass'
+ }
+ })
+
+ expect(factory.toHtml().classList.contains('testClass')).toBeTrue()
+ })
+ })
+ })
+
+ describe('Content', () => {
+ it('add simple text content', () => {
+ const template = [
+ '<div>' +
+ '<div class="foo"></div>' +
+ '<div class="foo2"></div>' +
+ '</div>'
+ ].join(' ')
+
+ const factory = new TemplateFactory({
+ template,
+ content: {
+ '.foo': 'bar',
+ '.foo2': 'bar2'
+ }
+ })
+
+ const html = factory.toHtml()
+ expect(html.querySelector('.foo').textContent).toBe('bar')
+ expect(html.querySelector('.foo2').textContent).toBe('bar2')
+ })
+
+ it('should not fill template if selector not exists', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ html: true,
+ template: '<div id="foo"></div>',
+ content: { '#bar': 'test' }
+ })
+
+ expect(factory.toHtml().outerHTML).toBe('<div id="foo"></div>')
+ })
+
+ it('should remove template selector, if content is null', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ html: true,
+ template: '<div><div id="foo"></div></div>',
+ content: { '#foo': null }
+ })
+
+ expect(factory.toHtml().outerHTML).toBe('<div></div>')
+ })
+
+ it('should resolve content if is function', () => {
+ const factory = new TemplateFactory({
+ sanitize: true,
+ html: true,
+ template: '<div><div id="foo"></div></div>',
+ content: { '#foo': () => null }
+ })
+
+ expect(factory.toHtml().outerHTML).toBe('<div></div>')
+ })
+
+ it('if content is element and "config.html=false", should put content\'s textContent', () => {
+ fixtureEl.innerHTML = '<div>foo<span>bar</span></div>'
+ const contentElement = fixtureEl.querySelector('div')
+
+ const factory = new TemplateFactory({
+ html: false,
+ template: '<div><div id="foo"></div></div>',
+ content: { '#foo': contentElement }
+ })
+
+ const fooEl = factory.toHtml().querySelector('#foo')
+ expect(fooEl.innerHTML).not.toBe(contentElement.innerHTML)
+ expect(fooEl.textContent).toBe(contentElement.textContent)
+ expect(fooEl.textContent).toBe('foobar')
+ })
+
+ it('if content is element and "config.html=true", should put content\'s outerHtml as child', () => {
+ fixtureEl.innerHTML = '<div>foo<span>bar</span></div>'
+ const contentElement = fixtureEl.querySelector('div')
+
+ const factory = new TemplateFactory({
+ html: true,
+ template: '<div><div id="foo"></div></div>',
+ content: { '#foo': contentElement }
+ })
+
+ const fooEl = factory.toHtml().querySelector('#foo')
+ expect(fooEl.innerHTML).toBe(contentElement.outerHTML)
+ expect(fooEl.textContent).toBe(contentElement.textContent)
+ })
+ })
+
+ describe('getContent', () => {
+ it('should get content as array', () => {
+ const factory = new TemplateFactory({
+ content: {
+ '.foo': 'bar',
+ '.foo2': 'bar2'
+ }
+ })
+ expect(factory.getContent()).toEqual(['bar', 'bar2'])
+ })
+
+ it('should filter empties', () => {
+ const factory = new TemplateFactory({
+ content: {
+ '.foo': 'bar',
+ '.foo2': '',
+ '.foo3': null,
+ '.foo4': () => 2,
+ '.foo5': () => null
+ }
+ })
+ expect(factory.getContent()).toEqual(['bar', 2])
+ })
+ })
+
+ describe('hasContent', () => {
+ it('should return true, if it has', () => {
+ const factory = new TemplateFactory({
+ content: {
+ '.foo': 'bar',
+ '.foo2': 'bar2',
+ '.foo3': ''
+ }
+ })
+ expect(factory.hasContent()).toBeTrue()
+ })
+
+ it('should return false, if filtered content is empty', () => {
+ const factory = new TemplateFactory({
+ content: {
+ '.foo2': '',
+ '.foo3': null,
+ '.foo4': () => null
+ }
+ })
+ expect(factory.hasContent()).toBeFalse()
+ })
+ })
+ describe('changeContent', () => {
+ it('should change Content', () => {
+ const template = [
+ '<div>' +
+ '<div class="foo"></div>' +
+ '<div class="foo2"></div>' +
+ '</div>'
+ ].join(' ')
+
+ const factory = new TemplateFactory({
+ template,
+ content: {
+ '.foo': 'bar',
+ '.foo2': 'bar2'
+ }
+ })
+
+ const html = selector => factory.toHtml().querySelector(selector).textContent
+ expect(html('.foo')).toEqual('bar')
+ expect(html('.foo2')).toEqual('bar2')
+ factory.changeContent({
+ '.foo': 'test',
+ '.foo2': 'test2'
+ })
+
+ expect(html('.foo')).toEqual('test')
+ expect(html('.foo2')).toEqual('test2')
+ })
+
+ it('should change only the given, content', () => {
+ const template = [
+ '<div>' +
+ '<div class="foo"></div>' +
+ '<div class="foo2"></div>' +
+ '</div>'
+ ].join(' ')
+
+ const factory = new TemplateFactory({
+ template,
+ content: {
+ '.foo': 'bar',
+ '.foo2': 'bar2'
+ }
+ })
+
+ const html = selector => factory.toHtml().querySelector(selector).textContent
+ expect(html('.foo')).toEqual('bar')
+ expect(html('.foo2')).toEqual('bar2')
+ factory.changeContent({
+ '.foo': 'test',
+ '.wrong': 'wrong'
+ })
+
+ expect(html('.foo')).toEqual('test')
+ expect(html('.foo2')).toEqual('bar2')
+ })
+ })
+})
diff --git a/site/assets/js/application.js b/site/assets/js/application.js
index acf859764e..2c57906c9f 100644
--- a/site/assets/js/application.js
+++ b/site/assets/js/application.js
@@ -144,11 +144,12 @@
clipboard.on('success', function (event) {
var tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
+ var originalTitle = event.trigger.getAttribute('title')
- event.trigger.setAttribute('data-bs-original-title', 'Copied!')
- tooltipBtn.show()
-
- event.trigger.setAttribute('data-bs-original-title', 'Copy to clipboard')
+ tooltipBtn.setContent({ '.tooltip-inner': 'Copied!' })
+ event.trigger.addEventListener('hidden.bs.tooltip', function () {
+ tooltipBtn.setContent({ '.tooltip-inner': originalTitle })
+ }, { once: true })
event.clearSelection()
})
@@ -156,11 +157,12 @@
var modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
var fallbackMsg = 'Press ' + modifierKey + 'C to copy'
var tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
+ var originalTitle = event.trigger.getAttribute('title')
- event.trigger.setAttribute('data-bs-original-title', fallbackMsg)
- tooltipBtn.show()
-
- event.trigger.setAttribute('data-bs-original-title', 'Copy to clipboard')
+ tooltipBtn.setContent({ '.tooltip-inner': fallbackMsg })
+ event.trigger.addEventListener('hidden.bs.tooltip', function () {
+ tooltipBtn.setContent({ '.tooltip-inner': originalTitle })
+ }, { once: true })
})
anchors.options = {
diff --git a/site/content/docs/5.1/components/popovers.md b/site/content/docs/5.1/components/popovers.md
index dc1c985d3f..0acc76a0a2 100644
--- a/site/content/docs/5.1/components/popovers.md
+++ b/site/content/docs/5.1/components/popovers.md
@@ -368,6 +368,21 @@ Removes the ability for an element's popover to be shown. The popover will only
myPopover.disable()
```
+#### setContent
+
+Gives a way to change the popover's content after its initialization.
+
+```js
+myPopover.setContent({
+ '.popover-header': 'another title',
+ '.popover-body': 'another content'
+})
+```
+
+{{< callout info >}}
+The `setContent` method accepts an `object` argument, where each property-key is a valid `string` selector within the popover template, and each related property-value can be `string` | `element` | `function` | `null`
+{{< /callout >}}
+
#### toggleEnabled
Toggles the ability for an element's popover to be shown or hidden.
diff --git a/site/content/docs/5.1/components/tooltips.md b/site/content/docs/5.1/components/tooltips.md
index caa2a2d0c0..16501a3c96 100644
--- a/site/content/docs/5.1/components/tooltips.md
+++ b/site/content/docs/5.1/components/tooltips.md
@@ -392,6 +392,17 @@ Removes the ability for an element's tooltip to be shown. The tooltip will only
tooltip.disable()
```
+#### setContent
+
+Gives a way to change the tooltip's content after its initialization.
+
+```js
+tooltip.setContent({ '.tooltip-inner': 'another title' })
+```
+{{< callout info >}}
+The `setContent` method accepts an `object` argument, where each property-key is a valid `string` selector within the popover template, and each related property-value can be `string` | `element` | `function` | `null`
+{{< /callout >}}
+
#### toggleEnabled
Toggles the ability for an element's tooltip to be shown or hidden.