UNPKG

bootstrap-confirmation2

Version:

Bootstrap plugin for on-place confirm boxes using Popover

544 lines (464 loc) 13.6 kB
import $ from 'jquery'; import Popover from 'bootstrap/js/src/popover'; /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ const NAME = 'confirmation'; const VERSION = '$VERSION'; const DATA_KEY = `bs.${NAME}`; const EVENT_KEY = `.${DATA_KEY}`; const JQUERY_NO_CONFLICT = $.fn[NAME]; const BTN_CLASS_BASE = 'h-100 d-flex align-items-center'; const BTN_CLASS_DEFAULT = 'btn btn-sm'; const DefaultType = { ...Popover.DefaultType, singleton : 'boolean', popout : 'boolean', copyAttributes : '(string|array)', onConfirm : 'function', onCancel : 'function', btnOkClass : 'string', btnOkLabel : 'string', btnOkIconClass : 'string', btnOkIconContent : 'string', btnCancelClass : 'string', btnCancelLabel : 'string', btnCancelIconClass : 'string', btnCancelIconContent: 'string', buttons : 'array', }; const Default = { ...Popover.Default, _attributes : {}, _selector : null, placement : 'top', title : 'Are you sure?', trigger : 'click', confirmationEvent : undefined, content : '', singleton : false, popout : false, copyAttributes : 'href target', onConfirm : $.noop, onCancel : $.noop, btnOkClass : `${BTN_CLASS_DEFAULT} btn-primary`, btnOkLabel : 'Yes', btnOkIconClass : '', btnOkIconContent : '', btnCancelClass : `${BTN_CLASS_DEFAULT} btn-secondary`, btnCancelLabel : 'No', btnCancelIconClass : '', btnCancelIconContent: '', buttons : [], // @formatter:off template : ` <div class="popover confirmation"> <div class="arrow"></div> <h3 class="popover-header"></h3> <div class="popover-body"> <p class="confirmation-content"></p> <div class="confirmation-buttons text-center"> <div class="btn-group"></div> </div> </div> </div>`, // @formatter:on }; if (Default.whiteList) { Default.whiteList['*'].push('data-apply', 'data-dismiss'); } const ClassName = { FADE: 'fade', SHOW: 'show', }; const Selector = { TITLE : '.popover-header', CONTENT: '.confirmation-content', BUTTONS: '.confirmation-buttons .btn-group', }; const Keymap = { 13: 'Enter', 27: 'Escape', 39: 'ArrowRight', 40: 'ArrowDown', }; const Event = { HIDE : `hide${EVENT_KEY}`, HIDDEN : `hidden${EVENT_KEY}`, SHOW : `show${EVENT_KEY}`, SHOWN : `shown${EVENT_KEY}`, INSERTED : `inserted${EVENT_KEY}`, CLICK : `click${EVENT_KEY}`, FOCUSIN : `focusin${EVENT_KEY}`, FOCUSOUT : `focusout${EVENT_KEY}`, MOUSEENTER: `mouseenter${EVENT_KEY}`, MOUSELEAVE: `mouseleave${EVENT_KEY}`, CONFIRMED : `confirmed${EVENT_KEY}`, CANCELED : `canceled${EVENT_KEY}`, KEYUP : `keyup${EVENT_KEY}`, }; /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ // keep track of the last openned confirmation for keyboard navigation let activeConfirmation; class Confirmation extends Popover { // Getters static get VERSION() { return VERSION; } static get Default() { return Default; } static get NAME() { return NAME; } static get DATA_KEY() { return DATA_KEY; } static get Event() { return Event; } static get EVENT_KEY() { return EVENT_KEY; } static get DefaultType() { return DefaultType; } // Constructor constructor(element, config) { super(element, config); if ((this.config.popout || this.config.singleton) && !this.config.rootSelector) { throw new Error('The rootSelector option is required to use popout and singleton features since jQuery 3.'); } // keep trace of selectors this._isDelegate = false; if (config.selector) { // container of buttons config._selector = `${config.rootSelector} ${config.selector}`; this.config._selector = config._selector; } else if (config._selector) { // children of container this.config._selector = config._selector; this._isDelegate = true; } else { // standalone this.config._selector = config.rootSelector; } if (this.config.confirmationEvent === undefined) { this.config.confirmationEvent = this.config.trigger; } if (!this.config.selector) { this._copyAttributes(); } this._setConfirmationListeners(); } // Overrides isWithContent() { return true; } setContent() { const $tip = $(this.getTipElement()); let content = this._getContent(); if (typeof content === 'function') { content = content.call(this.element); } this.setElementContent($tip.find(Selector.TITLE), this.getTitle()); $tip.find(Selector.CONTENT).toggle(!!content); if (content) { this.setElementContent($tip.find(Selector.CONTENT), content); } if (this.config.buttons.length > 0) { this._setButtons($tip, this.config.buttons); } else { this._setStandardButtons($tip); } $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`); this._setupKeyupEvent(); } dispose() { $('body').off(`${Event.CLICK}.${this.uid}`); this.eventBody = false; this._cleanKeyupEvent(); super.dispose(); } hide(callback) { this._cleanKeyupEvent(); super.hide(callback); } // Private /** * Build configuration object * Bootstrap standard is to give priority to JS config over data attributes, * but for Confirmation we prefer data attributes * @param config * @return {*} * @private */ _getConfig(config) { config = super._getConfig(config); const dataAttributes = $(this.element).data(); Object.keys(dataAttributes) .forEach((dataAttr) => { if (dataAttr.indexOf('btn') !== 0) { delete dataAttributes[dataAttr]; } }); return { ...config, ...dataAttributes, }; } /** * Copy the value of `copyAttributes` on the config object * @private */ _copyAttributes() { this.config._attributes = {}; if (this.config.copyAttributes) { if (typeof this.config.copyAttributes === 'string') { this.config.copyAttributes = this.config.copyAttributes.split(' '); } } else { this.config.copyAttributes = []; } this.config.copyAttributes.forEach((attr) => { this.config._attributes[attr] = $(this.element).attr(attr); }); } /** * Custom event listeners for popouts and singletons * @private */ _setConfirmationListeners() { const self = this; if (!this.config.selector) { // cancel original event $(this.element).on(this.config.trigger, (e, ack) => { if (!ack) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } }); // manage singleton $(this.element).on(Event.SHOWN, function () { if (self.config.singleton) { // close all other popover already initialized $(self.config._selector).not($(this)).filter(function () { return $(this).data(DATA_KEY) !== undefined; }).confirmation('hide'); } }); } else { // cancel original event $(this.element).on(this.config.trigger, this.config.selector, (e, ack) => { if (!ack) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } }); } if (!this._isDelegate) { // manage popout this.eventBody = false; this.uid = this.element.id || Confirmation.getUID(`${NAME}_group`); $(this.element).on(Event.SHOWN, () => { if (self.config.popout && !self.eventBody) { self.eventBody = $('body').on(`${Event.CLICK}.${self.uid}`, (e) => { if ($(self.config._selector).is(e.target) || $(self.config._selector).has(e.target).length > 0) { return; } // close all popover already initialized $(self.config._selector).filter(function () { return $(this).data(DATA_KEY) !== undefined; }).confirmation('hide'); $('body').off(`${Event.CLICK}.${self.uid}`); self.eventBody = false; }); } }); } } /** * Init the standard ok/cancel buttons * @param $tip * @private */ _setStandardButtons($tip) { const buttons = [ { class : this.config.btnOkClass, label : this.config.btnOkLabel, iconClass : this.config.btnOkIconClass, iconContent: this.config.btnOkIconContent, attr : this.config._attributes, }, { class : this.config.btnCancelClass, label : this.config.btnCancelLabel, iconClass : this.config.btnCancelIconClass, iconContent: this.config.btnCancelIconContent, cancel : true, }, ]; this._setButtons($tip, buttons); } /** * Init the buttons * @param $tip * @param buttons * @private */ _setButtons($tip, buttons) { const self = this; const $group = $tip.find(Selector.BUTTONS).empty(); buttons.forEach((button) => { const btn = $('<a href="#"></a>') .addClass(BTN_CLASS_BASE) .addClass(button.class || `${BTN_CLASS_DEFAULT} btn-secondary`) .html(button.label || '') .attr(button.attr || (button.cancel ? {} : self.config._attributes)); if (button.iconClass || button.iconContent) { btn.prepend($('<i></i>') .addClass(button.iconClass || '') .text(button.iconContent || '')); } btn.one('click', function (e) { if ($(this).attr('href') === '#') { e.preventDefault(); } if (button.onClick) { button.onClick.call($(self.element)); } if (button.cancel) { self.config.onCancel.call(self.element, button.value); $(self.element).trigger(Event.CANCELED, [button.value]); } else { self.config.onConfirm.call(self.element, button.value); $(self.element).trigger(Event.CONFIRMED, [button.value]); $(self.element).trigger(self.config.confirmationEvent, [true]); } self.hide(); }); $group.append(btn); }); } /** * Install the keyboatd event handler * @private */ _setupKeyupEvent() { activeConfirmation = this; $(window) .off(Event.KEYUP) .on(Event.KEYUP, this._onKeyup.bind(this)); } /** * Remove the keyboard event handler * @private */ _cleanKeyupEvent() { if (activeConfirmation === this) { activeConfirmation = undefined; $(window).off(Event.KEYUP); } } /** * Event handler for keyboard navigation * @param event * @private */ _onKeyup(event) { if (!this.tip) { this._cleanKeyupEvent(); return; } const $tip = $(this.getTipElement()); const key = event.key || Keymap[event.keyCode || event.which]; const $group = $tip.find(Selector.BUTTONS); const $active = $group.find('.active'); let $next; switch (key) { case 'Escape': this.hide(); break; case 'ArrowRight': if ($active.length && $active.next().length) { $next = $active.next(); } else { $next = $group.children().first(); } $active.removeClass('active'); $next.addClass('active').focus(); break; case 'ArrowLeft': if ($active.length && $active.prev().length) { $next = $active.prev(); } else { $next = $group.children().last(); } $active.removeClass('active'); $next.addClass('active').focus(); break; default: break; } } // Static /** * Generates an uui, copied from Bootrap's utils * @param {string} prefix * @returns {string} */ static getUID(prefix) { let uid = prefix; do { // eslint-disable-next-line no-bitwise uid += ~~(Math.random() * 1000000); // "~~" acts like a faster Math.floor() here } while (document.getElementById(uid)); return uid; } static _jQueryInterface(config) { return this.each(function () { let data = $(this).data(DATA_KEY); const _config = typeof config === 'object' ? config : {}; _config.rootSelector = $(this).selector || _config.rootSelector; // this.selector removed in jQuery > 3 if (!data && /destroy|hide/.test(config)) { return; } if (!data) { data = new Confirmation(this, _config); $(this).data(DATA_KEY, data); } if (typeof config === 'string') { if (typeof data[config] === 'undefined') { throw new TypeError(`No method named "${config}"`); } data[config](); } }); } } /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ $.fn[NAME] = Confirmation._jQueryInterface; $.fn[NAME].Constructor = Confirmation; $.fn[NAME].noConflict = function () { $.fn[NAME] = JQUERY_NO_CONFLICT; return Confirmation._jQueryInterface; }; export default Confirmation;