bootstrap5-toggle
Version:
Bootstrap Toggle is a bootstrap 5 plugin that converts checkboxes into toggles.
532 lines (490 loc) • 18 kB
JavaScript
/* Copyright Notice
* bootstrap5-toggle v5.1.2
* https://palcarazm.github.io/bootstrap5-toggle/
* @author 2011-2014 Min Hur (https://github.com/minhur)
* @author 2018-2019 Brent Ely (https://github.com/gitbrent)
* @author 2022 Pablo Alcaraz Martínez (https://github.com/palcarazm)
* @funding GitHub Sponsors
* @see https://github.com/sponsors/palcarazm
* @license MIT
* @see https://github.com/palcarazm/bootstrap5-toggle/blob/master/LICENSE
*/
;
function sanitize(text) {
if (!text) return text; // handle null or undefined
var map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/",
};
return text.replace(/[&<>"'/]/g, function (m) {
return map[m];
});
}
(function () {
/**
* `Toggle` is instantiated for each toggle-button
*/
class Toggle {
constructor(element, options) {
const DEPRECATION = {
value:
"BOOTSTRAP TOGGLE DEPRECATION CHECK -- a0Jhux0QySypjjs4tLtEo8xT2kx0AbYaq9K6mgNjWSs0HF0L8T8J0M0o3Kr7zkm7 --",
ATTRIBUTE: "attribute",
OPTION: "option",
log: function (type, oldlabel, newlabel) {
console.warn(
`Bootstrap Toggle deprecation warning: Using ${oldlabel} ${type} is deprected. Use ${newlabel} instead.`
);
},
};
const DEFAULTS = {
onlabel: "On",
onstyle: "primary",
onvalue: null,
ontitle: null,
offlabel: "Off",
offstyle: "secondary",
offvalue: null,
offtitle: null,
size: "",
style: "",
width: null,
height: null,
tabindex: 0,
tristate: false,
name: null,
};
options = options || {};
// A: Capture ref to HMTL element
this.element = element;
// B: Set options
this.options = {
onlabel:
this.element.getAttribute("data-onlabel") ||
options.onlabel ||
DEPRECATION.value ||
DEFAULTS.onlabel,
onstyle:
sanitize(this.element.getAttribute("data-onstyle")) ||
options.onstyle ||
DEFAULTS.onstyle,
onvalue:
sanitize(this.element.getAttribute("value")) ||
sanitize(this.element.getAttribute("data-onvalue")) ||
options.onvalue ||
DEFAULTS.onvalue,
ontitle:
sanitize(this.element.getAttribute("data-ontitle")) ||
options.ontitle ||
sanitize(this.element.getAttribute("title")) ||
DEFAULTS.ontitle,
offlabel:
this.element.getAttribute("data-offlabel") ||
options.offlabel ||
DEPRECATION.value ||
DEFAULTS.offlabel,
offstyle:
sanitize(this.element.getAttribute("data-offstyle")) ||
options.offstyle ||
DEFAULTS.offstyle,
offvalue:
sanitize(this.element.getAttribute("data-offvalue")) ||
options.offvalue ||
DEFAULTS.offvalue,
offtitle:
sanitize(this.element.getAttribute("data-offtitle")) ||
options.offtitle ||
sanitize(this.element.getAttribute("title")) ||
DEFAULTS.offtitle,
size:
sanitize(this.element.getAttribute("data-size")) ||
options.size ||
DEFAULTS.size,
style:
sanitize(this.element.getAttribute("data-style")) ||
options.style ||
DEFAULTS.style,
width:
sanitize(this.element.getAttribute("data-width")) ||
options.width ||
DEFAULTS.width,
height:
sanitize(this.element.getAttribute("data-height")) ||
options.height ||
DEFAULTS.height,
tabindex:
sanitize(this.element.getAttribute("tabindex")) ||
options.tabindex ||
DEFAULTS.tabindex,
tristate:
this.element.hasAttribute("tristate") ||
options.tristate ||
DEFAULTS.tristate,
name:
sanitize(this.element.getAttribute("name")) ||
options.name ||
DEFAULTS.name,
};
// C: Check deprecations
if (this.options.onlabel === DEPRECATION.value) {
if (sanitize(this.element.getAttribute("data-on"))) {
DEPRECATION.log(DEPRECATION.ATTRIBUTE, "data-on", "data-onlabel");
this.options.onlabel = this.element.getAttribute("data-on");
} else if (options.on) {
DEPRECATION.log(DEPRECATION.OPTION, "on", "onlabel");
this.options.onlabel = options.on;
} else {
this.options.onlabel = DEFAULTS.onlabel;
}
}
if (this.options.offlabel === DEPRECATION.value) {
if (sanitize(this.element.getAttribute("data-off"))) {
DEPRECATION.log(DEPRECATION.ATTRIBUTE, "data-off", "data-offlabel");
this.options.offlabel = this.element.getAttribute("data-off");
} else if (options.off) {
DEPRECATION.log(DEPRECATION.OPTION, "off", "offlabel");
this.options.offlabel = options.off;
} else {
this.options.offlabel = DEFAULTS.offlabel;
}
}
// LAST: Render Toggle
this.render();
}
render() {
function calcH(el) {
const styles = window.getComputedStyle(el);
const height = el.offsetHeight;
const borderTopWidth = parseFloat(styles.borderTopWidth);
const borderBottomWidth = parseFloat(styles.borderBottomWidth);
const paddingTop = parseFloat(styles.paddingTop);
const paddingBottom = parseFloat(styles.paddingBottom);
return (
height -
borderBottomWidth -
borderTopWidth -
paddingTop -
paddingBottom
);
}
// 0: Parse size
let size;
switch (this.options.size) {
case "large":
case "lg":
size = "btn-lg";
break;
case "small":
case "sm":
size = "btn-sm";
break;
case "mini":
case "xs":
size = "btn-xs";
break;
default:
size = "";
break;
}
// 1: On
let ecmasToggleOn = document.createElement("span");
ecmasToggleOn.setAttribute(
"class",
"btn btn-" + this.options.onstyle + " " + size
);
ecmasToggleOn.innerHTML = this.options.onlabel;
if (this.options.ontitle) {
ecmasToggleOn.setAttribute("title", this.options.ontitle);
}
// 2: Off
let ecmasToggleOff = document.createElement("span");
ecmasToggleOff.setAttribute(
"class",
"btn btn-" + this.options.offstyle + " " + size
);
ecmasToggleOff.innerHTML = this.options.offlabel;
if (this.options.offtitle) {
ecmasToggleOff.setAttribute("title", this.options.offtitle);
}
// 3: Handle
let ecmasToggleHandle = document.createElement("span");
ecmasToggleHandle.setAttribute("class", "toggle-handle btn " + size);
// 4: Toggle Group
let ecmasToggleGroup = document.createElement("div");
ecmasToggleGroup.setAttribute("class", "toggle-group");
ecmasToggleGroup.appendChild(ecmasToggleOn);
ecmasToggleGroup.appendChild(ecmasToggleOff);
ecmasToggleGroup.appendChild(ecmasToggleHandle);
// 5: Toggle
let ecmasToggle = document.createElement("div");
ecmasToggle.setAttribute("class", "toggle btn");
ecmasToggle.classList.add(
this.element.checked
? "btn-" + this.options.onstyle
: "btn-" + this.options.offstyle
);
ecmasToggle.setAttribute("tabindex", this.options.tabindex);
if (!this.element.checked) ecmasToggle.classList.add("off");
if (this.options.size) ecmasToggle.classList.add(size);
if (this.options.style) {
this.options.style.split(" ").forEach((style) => {
ecmasToggle.classList.add(style);
});
}
if (this.element.disabled || this.element.readOnly) {
ecmasToggle.classList.add("disabled");
ecmasToggle.setAttribute("disabled", "disabled");
}
// 6: Set form values
if (this.options.onvalue)
this.element.setAttribute("value", this.options.onvalue);
let invElement = null;
if (this.options.offvalue) {
invElement = this.element.cloneNode();
invElement.setAttribute("value", this.options.offvalue);
invElement.setAttribute("data-toggle", "invert-toggle");
invElement.removeAttribute("id");
invElement.checked = !this.element.checked;
}
// 7: Replace HTML checkbox with Toggle-Button
this.element.parentElement.insertBefore(ecmasToggle, this.element);
ecmasToggle.appendChild(this.element);
if (invElement) ecmasToggle.appendChild(invElement);
ecmasToggle.appendChild(ecmasToggleGroup);
// 8: Set button W/H, lineHeight
{
// A: Set style W/H
// NOTE: `offsetWidth` returns *rounded* integer values, so use `getBoundingClientRect` instead.
if (this.options.width) {
ecmasToggle.style.width = `${this.options.width}px`;
} else {
ecmasToggle.style["min-width"] = "100px"; // First approach for better calculation
ecmasToggle.style["min-width"] = `${
Math.max(
ecmasToggleOn.getBoundingClientRect().width,
ecmasToggleOff.getBoundingClientRect().width
) +
ecmasToggleHandle.getBoundingClientRect().width / 2
}px`;
}
if (this.options.height) {
ecmasToggle.style.height = `${this.options.height}px`;
} else {
ecmasToggle.style["min-height"] = "36px"; // First approach for better calculation
ecmasToggle.style["min-height"] = `${Math.max(
ecmasToggleOn.getBoundingClientRect().height,
ecmasToggleOff.getBoundingClientRect().height
)}px`;
}
// B: Apply on/off class
ecmasToggleOn.classList.add("toggle-on");
ecmasToggleOff.classList.add("toggle-off");
// C: Finally, set lineHeight if needed
if (this.options.height) {
ecmasToggleOn.style.lineHeight = calcH(ecmasToggleOn) + "px";
ecmasToggleOff.style.lineHeight = calcH(ecmasToggleOff) + "px";
}
}
// 9: Add listeners
ecmasToggle.addEventListener(
"pointerdown",
(e) => {
this.#toggleActionPerformed(e);
},
{ passive: true }
);
ecmasToggle.addEventListener(
"keypress",
(e) => {
if (e.key == " ") {
this.#toggleActionPerformed(e);
}
},
{ passive: true }
);
if (this.element.id) {
document
.querySelectorAll('label[for="' + this.element.id + '"]')
.forEach((label) => {
label.addEventListener(
"pointerdown",
(e) => {
e.preventDefault();
this.toggle();
ecmasToggle.focus();
},
{ passive: true }
);
});
}
// 10: Set elements to bootstrap object
this.ecmasToggle = ecmasToggle;
this.invElement = invElement;
// 11: Keep reference to this instance for subsequent calls via `getElementById().bootstrapToggle()`
this.element.bsToggle = this;
}
/**
* Trigger actions
* @param {Event} e event
*/
#toggleActionPerformed(e) {
if (this.options.tristate) {
if (this.ecmasToggle.classList.contains("indeterminate")) {
this.determinate(true);
this.toggle();
} else {
this.indeterminate();
}
} else {
this.toggle();
}
}
toggle(silent = false) {
if (this.element.checked) this.off(silent);
else this.on(silent);
}
on(silent = false) {
if (this.element.disabled || this.element.readOnly) return false;
this.ecmasToggle.classList.remove("btn-" + this.options.offstyle);
this.ecmasToggle.classList.add("btn-" + this.options.onstyle);
this.ecmasToggle.classList.remove("off");
this.element.checked = true;
if (this.invElement) this.invElement.checked = false;
if (!silent) this.trigger();
}
off(silent = false) {
if (this.element.disabled || this.element.readOnly) return false;
this.ecmasToggle.classList.remove("btn-" + this.options.onstyle);
this.ecmasToggle.classList.add("btn-" + this.options.offstyle);
this.ecmasToggle.classList.add("off");
this.element.checked = false;
if (this.invElement) this.invElement.checked = true;
if (!silent) this.trigger();
}
indeterminate(silent = false) {
if (
!this.options.tristate ||
this.element.disabled ||
this.element.readOnly
)
return false;
this.ecmasToggle.classList.add("indeterminate");
this.element.indeterminate = true;
this.element.removeAttribute("name");
if (this.invElement) this.invElement.indeterminate = true;
if (this.invElement) this.invElement.removeAttribute("name");
if (!silent) this.trigger();
}
determinate(silent = false) {
if (
!this.options.tristate ||
this.element.disabled ||
this.element.readOnly
)
return false;
this.ecmasToggle.classList.remove("indeterminate");
this.element.indeterminate = false;
if (this.options.name)
this.element.setAttribute("name", this.options.name);
if (this.invElement) this.invElement.indeterminate = false;
if (this.invElement && this.options.name)
this.invElement.setAttribute("name", this.options.name);
if (!silent) this.trigger();
}
enable() {
this.ecmasToggle.classList.remove("disabled");
this.ecmasToggle.removeAttribute("disabled");
this.element.removeAttribute("disabled");
this.element.removeAttribute("readonly");
if (this.invElement) {
this.invElement.removeAttribute("disabled");
this.invElement.removeAttribute("readonly");
}
}
disable() {
this.ecmasToggle.classList.add("disabled");
this.ecmasToggle.setAttribute("disabled", "");
this.element.setAttribute("disabled", "");
this.element.removeAttribute("readonly");
if (this.invElement) {
this.invElement.setAttribute("disabled", "");
this.invElement.removeAttribute("readonly");
}
}
readonly() {
this.ecmasToggle.classList.add("disabled");
this.ecmasToggle.setAttribute("disabled", "");
this.element.removeAttribute("disabled");
this.element.setAttribute("readonly", "");
if (this.invElement) {
this.invElement.removeAttribute("disabled");
this.invElement.setAttribute("readonly", "");
}
}
update(silent) {
if (this.element.disabled) this.disable();
else if (this.element.readOnly) this.readonly();
else this.enable();
if (this.element.checked) this.on(silent);
else this.off(silent);
}
trigger(silent) {
if (!silent)
this.element.dispatchEvent(new Event("change", { bubbles: true }));
}
destroy() {
// A: Remove button-group from UI, replace checkbox element
this.ecmasToggle.parentNode.insertBefore(this.element, this.ecmasToggle);
this.ecmasToggle.parentNode.removeChild(this.ecmasToggle);
// B: Delete internal refs
delete this.element.bsToggle;
delete this.ecmasToggle;
}
rerender() {
this.destroy();
this.element.bootstrapToggle();
}
}
/**
* Add `bootstrapToggle` prototype function to HTML Elements
* Enables execution when used with HTML - ex: `document.getElementById('toggle').bootstrapToggle('on')`
*/
Element.prototype.bootstrapToggle = function (options, silent) {
let _bsToggle = this.bsToggle || new Toggle(this, options);
// Execute method calls
if (options && typeof options === "string") {
if (options.toLowerCase() == "toggle") _bsToggle.toggle(silent);
else if (options.toLowerCase() == "on") _bsToggle.on(silent);
else if (options.toLowerCase() == "off") _bsToggle.off(silent);
else if (options.toLowerCase() == "indeterminate")
_bsToggle.indeterminate(silent);
else if (options.toLowerCase() == "determinate")
_bsToggle.determinate(silent);
else if (options.toLowerCase() == "enable") _bsToggle.enable();
else if (options.toLowerCase() == "disable") _bsToggle.disable();
else if (options.toLowerCase() == "readonly") _bsToggle.readonly();
else if (options.toLowerCase() == "destroy") _bsToggle.destroy();
else if (options.toLowerCase() == "rerender") _bsToggle.rerender();
}
};
/**
* Replace all `input[type=checkbox][data-toggle="toggle"]` inputs with "Bootstrap-Toggle"
* Executes once page elements have rendered enabling script to be placed in `<head>`
*/
if (typeof window !== "undefined")
window.onload = function () {
document
.querySelectorAll('input[type=checkbox][data-toggle="toggle"]')
.forEach(function (ele) {
ele.bootstrapToggle();
});
};
// Export library if possible
if (typeof module !== "undefined" && module.exports) {
module.exports = Toggle;
}
})();