Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a template factory helper to handle all template cases #34519

Merged
merged 8 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
{
"path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "18.25 kB"
"maxSize": "18.5 kB"
},
{
"path": "./dist/js/bootstrap.js",
Expand Down
10 changes: 6 additions & 4 deletions js/src/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
129 changes: 47 additions & 82 deletions js/src/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`

Expand Down Expand Up @@ -132,6 +132,7 @@ class Tooltip extends BaseComponent {
this._hoverState = ''
this._activeTrigger = {}
this._popper = null
this._templateFactory = null

// Protected
this._config = this._getConfig(config)
Expand Down Expand Up @@ -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) :
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -485,7 +461,7 @@ class Tooltip extends BaseComponent {
{
name: 'arrow',
options: {
element: `.${this.constructor.NAME}-arrow`
element: SELECTOR_TOOLTIP_ARROW
}
},
{
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
}

Expand Down
Loading