js-alert
Version:
A simple JavaScript popup alert manager.
540 lines (375 loc) • 14.6 kB
JavaScript
//
// Main class for the JSAlert package
import Queue from './queue.js'
import EventSource from './event-source.js'
import PkgInfo from '../package.json'
import sanitize from 'light-sanitize-html'
export default class JSAlert extends EventSource {
/** Library version */
static get version() {
return PkgInfo.version
}
/** @static Creates and shows a new alert with the specified text */
static alert(text, title, icon, closeText = "Close") {
// Check if not in a browser
if (typeof window === "undefined")
return Promise.resolve(console.log("Alert: " + text));
// Create alert
var alert = new JSAlert(text, title);
alert.addButton(closeText, null);
// Set icon
if (icon !== false)
alert.setIcon(icon || JSAlert.Icons.Information);
// Show it
return alert.show();
}
/** @static Creates and shows a new confirm alert with the specified text */
static confirm(text, title, icon, acceptText = "OK", rejectText = "Cancel") {
// Check if not in a browser
if (typeof window === "undefined")
return Promise.resolve(console.log("Alert: " + text));
// Create alert
var alert = new JSAlert(text, title);
alert.addButton(acceptText, true);
alert.addButton(rejectText, false);
// Set icon
if (icon !== false)
alert.setIcon(icon || JSAlert.Icons.Question);
// Show it
return alert.show();
}
/** @static Creates and shows a new prompt, an alert with a single text field. */
static prompt(text, defaultText, placeholderText, title, icon, acceptText = "OK", rejectText = "Cancel") {
// Check if not in a browser
if (typeof window === "undefined")
return Promise.resolve(console.log("Alert: " + text));
// Create alert
var alert = new JSAlert(text, title);
alert.addButton(acceptText, true, "default");
alert.addButton(rejectText, false, "cancel");
// Set icon
if (icon !== false)
alert.setIcon(icon || JSAlert.Icons.Question);
// Add text field
alert.addTextField(defaultText, null, placeholderText);
// Show it
return alert.show().then((result) => {
// Check if cancelled
if (alert.cancelled)
return null;
else
return alert.getTextFieldValue(0);
});
}
/** @static Creates and shows a loader, which is just an alert with no buttons. */
static loader(text, cancelable) {
// Check if not in a browser
if (typeof window === "undefined")
return Promise.resolve(console.log("Loading: " + text));
// Create alert
var alert = new JSAlert(text);
alert.cancelable = cancelable;
// Show it
return alert.show();
}
/** Constructor */
constructor(text = "", title = "") {
super();
// Setup vars
this.elems = {};
this.title = title;
this.text = text;
this.buttons = [];
this.textFields = [];
this.result = false;
this.iconURL = null;
this.cancelable = true;
this.cancelled = false;
this.dismissed = false;
}
/** Sets an icon for the alert. `icon` is either a URL or one of `JSAlert.Icons`. */
setIcon(icon) {
this.iconURL = icon;
}
/** Adds a button. Returns a Promise that is called if the button is clicked. */
addButton(text, value, type) {
// Return promise
return new Promise((onSuccess, onFail) => {
// Add button
this.buttons.push({
text: text,
value: typeof value == "undefined" ? text : value,
type: type || (this.buttons.length == 0 ? "default" : "normal"),
callback: onSuccess
});
});
}
/** Adds a text field. Returns a Promise that will be called when the dialog is dismissed, but not cancelled. */
addTextField(value, type, placeholderText) {
// Add text field
this.textFields.push({
value: value || "",
type: type || "text",
placeholder: placeholderText || ""
});
}
/** Gets a text field's value */
getTextFieldValue(index) {
// Get text field info
var info = this.textFields[index];
// Return the value
return (info.elem ? info.elem.value : info.value);
}
/** Shows the alert. */
show() {
// Add to the queue
JSAlert.popupQueue.add(this).then(() => {
// Show us
this._show();
// Notify that we have been shown
this.emit("opened");
});
// Return the alert
return this;
}
/** A then function, to allow chaining with Promises */
then(func) {
return this.when("closed").then(func);
}
/** Dismisses the alert. */
dismiss(result) {
// Do nothing if dismissed already
if (this.dismissed) return;
this.dismissed = true;
// Remove us from the queue
JSAlert.popupQueue.remove(this);
// Store result
this.result = result;
if (typeof result == "undefined")
this.cancelled = true;
// Remove elements
this.removeElements();
// Remove global keyboard listener
window.removeEventListener("keydown", this);
// Trigger cancel-specific event
if (this.cancelled)
this.emit("cancelled", this.result);
else
this.emit("complete", this.result);
// Trigger closed event
this.emit("closed", this.result);
return this;
}
/** Dismisses the alert some time in the future */
dismissIn(time) {
setTimeout(this.dismiss.bind(this), time);
return this;
}
/** @private Called to actually show the alert. */
_show() {
// Create elements
this.createBackground();
this.createPopup();
// Add global keyboard listener
window.addEventListener("keydown", this);
}
/** @private Called to create the overlay element. Theme subclasses can override this. */
createBackground() {
// Create element
this.elems.background = document.createElement("div");
this.elems.background.style.cssText = "position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 10000; background-color: rgba(0, 0, 0, 0.1); opacity: 0; transition: opacity 0.15s; ";
// Add to document
document.body.appendChild(this.elems.background);
// Do animation
setTimeout(() => {
this.elems.background.offsetWidth;
this.elems.background.style.opacity = 1;
}, 0);
}
/** @private Called to create the popup element. Theme subclasses can override this. */
createPopup() {
// Create container element
this.elems.container = document.createElement("div");
this.elems.container.focusable = true;
this.elems.container.style.cssText = "position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 10001; display: flex; justify-content: center; align-items: center; opacity: 0; transform: translateY(-40px); transition: opacity 0.15s, transform 0.15s; ";
document.body.appendChild(this.elems.container);
// Do animation
setTimeout(() => {
this.elems.container.offsetWidth;
this.elems.container.style.opacity = 1;
this.elems.container.style.transform = "translateY(0px)";
}, 0);
// Add dismiss handler
this.addTouchHandler(this.elems.container, () => {
// Check if cancelable
if (!this.cancelable)
return;
// Dismiss
this.cancelled = true;
this.dismiss();
});
// Create window
this.elems.window = document.createElement("div");
this.elems.window.style.cssText = "position: relative; background-color: rgba(255, 255, 255, 0.95); box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.25); border-radius: 5px; padding: 10px; min-width: 50px; min-height: 10px; max-width: 50%; max-height: 90%; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); ";
this.elems.container.appendChild(this.elems.window);
// Create icon if there is one
if (this.iconURL) {
this.elems.icon = document.createElement("img");
this.elems.icon.style.cssText = "display: block; margin: auto; max-height: 40px; text-align: center; font-family: Helvetica, Arial; font-size: 17px; font-weight: bold; color: #000; cursor: default; padding: 10px 0px; ";
this.elems.icon.src = this.iconURL;
this.elems.window.appendChild(this.elems.icon);
}
// Create title if there is one
if (this.title) {
this.elems.title = document.createElement("div");
this.elems.title.style.cssText = "display: block; text-align: center; font-family: Helvetica, Arial; font-size: 17px; font-weight: bold; color: #000; cursor: default; padding: 2px 20px; ";
this.elems.title.innerHTML = sanitize(this.title)
this.elems.window.appendChild(this.elems.title);
}
// Create text if there is one
if (this.text) {
this.elems.text = document.createElement("div");
this.elems.text.style.cssText = "display: block; text-align: center; font-family: Helvetica, Arial; font-size: 15px; font-weight: normal; color: #000; cursor: default; padding: 2px 20px; ";
this.elems.text.innerHTML = sanitize(this.text)
this.elems.window.appendChild(this.elems.text);
}
// Create text fields if there are any
if (this.textFields.length > 0) {
this.elems.textFields = document.createElement("div");
this.elems.textFields.style.cssText = "display: block; ";
this.elems.window.appendChild(this.elems.textFields);
// Add each text field
this.textFields.forEach((b, idx) => {
b.elem = document.createElement("input");
b.elem.style.cssText = "display: block; width: 90%; min-width: 250px; padding: 5px 0px; margin: 10px auto; background-color: #FFF; border: 1px solid #EEE; border-radius: 5px; text-align: center; font-family: Helvetica, Arial; font-size: 15px; color: #222; ";
b.elem.value = b.value;
b.elem.placeholder = b.placeholder;
b.elem.type = b.type;
this.elems.textFields.appendChild(b.elem);
// Add keyboard listener
b.elem.addEventListener("keypress", (e) => {
// Ignore if not enter
if (e.keyCode != 13)
return;
// Check if this is the last input field
if (idx+1 >= this.textFields.length) {
// Done
this.dismiss("enter-pressed");
} else {
// Just select the next field
this.textFields[idx+1].elem.focus();
}
});
});
// Focus on first field
this.textFields[0].elem.focus();
}
// Create buttons if there are any
if (this.buttons.length > 0) {
this.elems.buttons = document.createElement("div");
this.elems.buttons.style.cssText = "display: block; display: flex; justify-content: space-around; align-items: center; text-align: right; border-top: 1px solid #EEE; margin-top: 10px; ";
this.elems.window.appendChild(this.elems.buttons);
// Add each button
this.buttons.forEach((b) => {
var btn = document.createElement("div");
btn.style.cssText = "display: inline-block; font-family: Helvetica, Arial; font-size: 15px; font-weight: 200; color: #08F; padding: 10px 20px; padding-bottom: 0px; cursor: pointer; ";
btn.innerText = b.text;
this.elems.buttons.appendChild(btn);
// Add button handler
this.addTouchHandler(btn, () => {
b.callback && b.callback(b.value);
if (b.type == "cancel") this.cancelled = true;
this.dismiss(b.value);
});
});
}
}
/** @private Called to remove all elements from the screen */
removeElements() {
// Don't do anything if not loaded
if (!this.elems || !this.elems.container)
return;
// Animate background away
this.elems.background.style.opacity = 0;
this.elems.container.style.opacity = 0;
this.elems.container.style.transform = "translateY(40px)";
// Remove elements after animation
setTimeout(() => {
this.removeElement(this.elems.background);
this.removeElement(this.elems.container);
}, 250);
}
/** @private Helper function to remove an element */
removeElement(elem) {
elem && elem.parentNode && elem.parentNode.removeChild(elem);
}
/** @private Helper function to add a click or touch event handler that doesn't bubble */
addTouchHandler(elem, callback) {
// Create handler
var handler = (e) => {
// Stop default browser action, unless this is an input field
if (e.target.nodeName.toLowerCase() != "input")
e.preventDefault();
// Check if our element was pressed, not a child element
if (e.target != elem)
return;
// Trigger callback
callback();
}
// Add listeners
this.elems.container.addEventListener("mousedown", handler, true);
this.elems.container.addEventListener("touchstart", handler, true);
}
/** @private Called by the browser when a keyboard event is fired on the whole window */
handleEvent(e) {
// Check if enter was pressed
if (e.keyCode == 13) {
// Find the first default button and use that value instead
for (var i = 0 ; i < this.buttons.length ; i++) {
if (this.buttons[i].type == "default") {
// Use this button's value
this.dismiss(this.buttons[i].value);
e.preventDefault();
// Trigger the button's callback
this.buttons[i].callback && this.buttons[i].callback(this.result);
return;
}
}
// No default button found, cancel
this.cancelled = true;
this.dismiss();
return;
}
// Check if escape was pressed
if (e.keyCode == 27) {
// Check if cancelable
if (!this.cancelable)
return;
// Find the first default button and use that value instead
this.cancelled = true;
this.result = null;
for (var i = 0 ; i < this.buttons.length ; i++) {
if (this.buttons[i].type == "cancel") {
// Use this button's value
this.dismiss(this.buttons[i].value);
e.preventDefault();
// Trigger the button's callback
this.buttons[i].callback && this.buttons[i].callback(this.result);
return;
}
}
// No cancel button found, just cancel
this.cancelled = true;
this.dismiss();
return;
}
}
}
// Include theme's icons
import icons from "./icons.js";
JSAlert.Icons = icons;
// The default popup queue
JSAlert.popupQueue = new Queue();
// In case anyone wants to use the classes of this project on their own...
JSAlert.Queue = Queue;
JSAlert.EventSource = EventSource;