mirror of
https://github.com/twbs/bootstrap.git
synced 2022-11-09 12:25:43 -05:00
Add a template factory helper to handle all template cases (#34519)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
This commit is contained in:
parent
fa33e83f25
commit
94a596fbcb
10 changed files with 606 additions and 131 deletions
|
@ -46,7 +46,7 @@
|
|||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.esm.min.js",
|
||||
"maxSize": "18.25 kB"
|
||||
"maxSize": "18.5 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/js/bootstrap.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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
const tipId = getUID(this.constructor.NAME).toString()
|
||||
|
||||
tip.setAttribute('id', tipId)
|
||||
|
||||
if (this._config.animation) {
|
||||
tip.classList.add(CLASS_NAME_FADE)
|
||||
}
|
||||
|
||||
this.tip = tip
|
||||
return this.tip
|
||||
}
|
||||
|
||||
setContent(tip) {
|
||||
this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER)
|
||||
setContent(content) {
|
||||
let isShown = false
|
||||
if (this.tip) {
|
||||
isShown = this.tip.classList.contains(CLASS_NAME_SHOW)
|
||||
this.tip.remove()
|
||||
}
|
||||
|
||||
this._disposePopper()
|
||||
|
||||
this.tip = this._getTemplateFactory(content).toHtml()
|
||||
|
||||
if (isShown) {
|
||||
this.show()
|
||||
}
|
||||
}
|
||||
|
||||
_sanitizeAndSetContent(template, content, selector) {
|
||||
const templateElement = SelectorEngine.findOne(selector, template)
|
||||
|
||||
if (!content && templateElement) {
|
||||
templateElement.remove()
|
||||
return
|
||||
}
|
||||
|
||||
// we use append for html objects to maintain js events
|
||||
this.setElementContent(templateElement, content)
|
||||
}
|
||||
|
||||
setElementContent(element, content) {
|
||||
if (element === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isElement(content)) {
|
||||
content = getElement(content)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_getContentForTemplate() {
|
||||
return {
|
||||
[SELECTOR_TOOLTIP_INNER]: this.getTitle()
|
||||
}
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
const title = this._element.getAttribute('data-bs-original-title') || this._config.title
|
||||
|
||||
return this._resolvePossibleFunction(title)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
161
js/src/util/template-factory.js
Normal file
161
js/src/util/template-factory.js
Normal file
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
305
js/tests/unit/util/template-factory.spec.js
Normal file
305
js/tests/unit/util/template-factory.spec.js
Normal file
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue