@rolemodel/turbo-confirm
Version:
Drop-in upgrade for Rails' data-turbo-confirm feature to support custom HTML dialogs.
124 lines (105 loc) • 3.71 kB
JavaScript
import ConfirmationController from './ConfirmationController.js'
export class TurboConfirm {
#controller
#config = {
dialogSelector: '#confirm',
activeClass: 'modal--active',
acceptSelector: '#confirm-accept',
denySelector: '.confirm-cancel',
animationDuration: 300,
showConfirmCallback: element => element.showModal && element.showModal(),
hideConfirmCallback: element => element.close && element.close(),
messageSlotSelector: '#confirm-title',
contentSlots: {
body: {
contentAttribute: 'confirm-details',
slotSelector: '#confirm-body'
},
acceptText: {
contentAttribute: 'confirm-button',
slotSelector: '#confirm-accept'
}
}
}
constructor(options = {}) {
for(const [key, value] of Object.entries(options)) {
this.#config[key] = value
}
this.#controller = new ConfirmationController(this)
}
/**
* Present a confirmation challenge to the user.
* @public
* @param {string} [message] - The main challenge message; Value of `data-turbo-confirm` attribute.
* @param {HTMLFormElement} [_formElement] - (ignored) `form` element that contains the submitter.
* @param {HTMLElement} [submitter] - button of input of type submit that triggered the form submission.
* @returns {Promise<boolean>} - A promise that resolves to true if the user accepts the challenge or false if they deny it.
*/
confirm(message, _formElement, submitter) {
const clickTarget = this.#clickTarget(submitter)
const contentMap = this.#contentMap(message, clickTarget)
return this.confirmWithContent(contentMap)
}
/**
* Present a confirmation challenge to the user.
* @public
* @param {Object} contentMap - A map of CSS selectors to HTML content to be inserted into the dialog.
* @returns {Promise<boolean>} - A promise that resolves to true if the user accepts the challenge or false if they deny it.
*/
confirmWithContent(contentMap) {
return this.#controller.showConfirm(contentMap)
}
/**
* a function for #controller to call after setup
* @private
*/
showConfirm(element) {
element.classList.add(this.#config.activeClass)
if (typeof this.#config.showConfirmCallback === 'function') {
this.#config.showConfirmCallback(element)
}
}
/**
* a function for #controller to call before teardown
* @private
*/
hideConfirm(element) {
element.classList.remove(this.#config.activeClass)
if (typeof this.#config.hideConfirmCallback === 'function') {
this.#config.hideConfirmCallback(element)
}
}
get dialogSelector() {
return this.#config.dialogSelector
}
get acceptSelector() {
return this.#config.acceptSelector
}
get denySelector() {
return this.#config.denySelector
}
get animationDuration() {
return this.#config.animationDuration
}
#contentMap(message, sourceElement) {
const contentMap = {}
if (message) contentMap[this.#config.messageSlotSelector] = message
if (sourceElement) {
for(const slotName of Object.keys(this.#config.contentSlots)) {
contentMap[this.#slotSelector(slotName)] = this.#slotContent(slotName, sourceElement)
}
}
return contentMap
}
#slotSelector(slotName) {
return this.#config.contentSlots[slotName].slotSelector
}
#slotContent(slotName, element) {
return element.getAttribute(`data-${this.#config.contentSlots[slotName].contentAttribute}`)
}
#clickTarget(target) {
// in the case of a turbo form link, Turbo fails to forward the submitter.
const element = target ?? document.activeElement
return element.closest('[data-turbo-confirm]')
}
}