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

Refactoring with Typescript #143

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
17 changes: 0 additions & 17 deletions .gitattributes

This file was deleted.

9 changes: 0 additions & 9 deletions .prettierrc

This file was deleted.

12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

38 changes: 6 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -74,11 +74,9 @@ And the script at the bottom of the page
## Documentation

```javascript
Toastify({
Toast({
text: "This is a toast",
duration: 3000,
destination: "https://github.com/apvarun/toastify-js",
newWindow: true,
close: true,
gravity: "top", // `top` or `bottom`
position: "left", // `left`, `center` or `right`
@@ -87,10 +85,10 @@ Toastify({
background: "linear-gradient(to right, #00b09b, #96c93d)",
},
onClick: function(){} // Callback after click
}).showToast();
}).show();
```

> Toast messages will be centered on devices with screen width less than 360px.
> Toast messages will be centered on devices with screen width less than 480px.
* See the [changelog](https://github.com/apvarun/toastify-js/blob/master/CHANGELOG.md)

@@ -99,33 +97,17 @@ Toastify({
If you want to use custom classes on the toast for customizing (like info or warning for example), you can do that as follows:

```javascript
Toastify({
Toast({
text: "This is a toast",
className: "info",
style: {
background: "linear-gradient(to right, #00b09b, #96c93d)",
}
}).showToast();
}).show();
```

Multiple classes also can be assigned as a string, with spaces between class names.

### Add some offset

If you want to add offset to the toast, you can do that as follows:

```javascript
Toastify({
text: "This is a toast with offset",
offset: {
x: 50, // horizontal axis - can be a number or a string indicating unity. eg: '2em'
y: 10 // vertical axis - can be a number or a string indicating unity. eg: '2em'
},
}).showToast();
```

Toast will be pushed 50px from right in x axis and 10px from top in y axis.

**Note:**

If `position` is equals to `left`, it will be pushed from left.
@@ -138,25 +120,17 @@ If `gravity` is equals to `bottom`, it will be pushed from bottom.
| text | string | Message to be displayed in the toast | "Hi there!" |
| node | ELEMENT_NODE | Provide a node to be mounted inside the toast. `node` takes higher precedence over `text` | |
| duration | number | Duration for which the toast should be displayed.<br>-1 for permanent toast | 3000 |
| selector | string \| ELEMENT_NODE | ShadowRoot | CSS Selector or Element Node on which the toast should be added | body |
| destination | URL string | URL to which the browser should be navigated on click of the toast | |
| newWindow | boolean | Decides whether the `destination` should be opened in a new window or not | false |
| close | boolean | To show the close icon or not | false |
| gravity | "top" or "bottom" | To show the toast from top or bottom | "top" |
| position | "left" or "right" | To show the toast on left or right | "right" |
| backgroundColor | CSS background value | To be deprecated, use `style.background` option instead. Sets the background color of the toast | |
| avatar | URL string | Image/icon to be shown before text | |
| className | string | Ability to provide custom class name for further customization | |
| stopOnFocus | boolean | To stop timer when hovered over the toast (Only if duration is set) | true |
| callback | Function | Invoked when the toast is dismissed | |
| onClose | Function | Invoked when the toast is dismissed | |
| onClick | Function | Invoked when the toast is clicked | |
| offset | Object | Ability to add some offset to axis | |
| escapeMarkup | boolean | Toggle the default behavior of escaping HTML markup | true |
| style | object | Use the HTML DOM Style properties to add any style directly to toast | |
| ariaLive | string | Announce the toast to screen readers, see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions for options | "polite" |
| oldestFirst | boolean | Set the order in which toasts are stacked in page | true |

> Deprecated properties: `backgroundColor` - use `style.background` option instead

## Browsers support

60 changes: 60 additions & 0 deletions build/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const root = process.cwd();
import { join, parse } from 'path';
import { readFileSync, existsSync, mkdirSync } from 'fs';
import esbuild from 'esbuild';
function mkdir(path) {
return existsSync(path) || mkdirSync(path)
}
const sourcePath = join(root, 'src');
const packagePath = join(root, 'package.json');
const tsconfigPath = join(root, 'tsconfig.json');
const packageConfig = JSON.parse(readFileSync(packagePath, 'utf8'));
const tsConfig = JSON.parse(readFileSync(tsconfigPath, 'utf8'));
const distPath = parse(packageConfig.main).dir;
const mainPath = join(sourcePath, `${packageConfig.name}.ts`);
const cssPath = join(sourcePath, `${packageConfig.name}.css`);
mkdir(distPath);
esbuild.buildSync({
allowOverwrite: true,
entryPoints: [mainPath],
outfile: join(distPath, `${packageConfig.name}.js`),
minify: false,
sourcemap: false,
platform: 'browser',
format: 'iife',
target: tsConfig.target,
charset: 'utf8'
});
esbuild.buildSync({
allowOverwrite: true,
entryPoints: [mainPath],
outfile: join(distPath, `${packageConfig.name}.min.js`),
minify: true,
sourcemap: true,
platform: 'browser',
format: 'iife',
target: tsConfig.target,
charset: 'utf8'
});
esbuild.buildSync({
allowOverwrite: true,
entryPoints: [cssPath],
outfile: join(distPath, `${packageConfig.name}.css`),
minify: false,
loader: {
'.css': 'css'
},
platform: 'browser',
charset: 'utf8'
});
esbuild.buildSync({
allowOverwrite: true,
entryPoints: [cssPath],
outfile: join(distPath, `${packageConfig.name}.min.css`),
minify: true,
loader: {
'.css': 'css'
},
platform: 'browser',
charset: 'utf8'
});
140 changes: 140 additions & 0 deletions dist/toastify.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.toast-container {
position: fixed;
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
max-width: 100%;
box-sizing: border-box;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.toast-container.toast-top {
top: 0;
}
.toast-container.toast-bottom {
bottom: 0;
}
.toast-container.toast-left {
left: 0;
align-items: flex-start;
}
.toast-container.toast-center {
left: 50%;
transform: translateX(-50%);
align-items: center;
}
.toast-container.toast-right {
right: 0;
align-items: flex-end;
}
.toast {
position: relative;
background: rgb(55, 208, 255);
color: white;
padding: 1rem 2rem;
border-radius: 4px;
font-family: sans-serif;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
cursor: default;
transition: transform 0.5s ease, opacity 0.15s ease;
max-width: 480px;
}
.toast:hover {
z-index: 2147483647 !important;
transform: scale(1.25) !important;
will-change: transform;
}
.toast.toast-top.toast-left {
transform-origin: left center;
}
.toast.toast-top.toast-center {
transform-origin: top;
}
.toast.toast-top.toast-right {
transform-origin: right center;
}
.toast.toast-bottom.toast-left {
transform-origin: left center;
}
.toast.toast-bottom.toast-center {
transform-origin: bottom;
}
.toast.toast-bottom.toast-right {
transform-origin: right center;
}
.toast-container.toast-top .toast.show {
animation: toast-in-top 0.3s ease-in-out forwards;
}
.toast-container.toast-bottom .toast.show {
animation: toast-in-bottom 0.3s ease-in-out forwards;
}
.toast-container.toast-top .toast.hide {
animation: toast-out-top 0.15s ease-in-out forwards;
}
.toast-container.toast-bottom .toast.hide {
animation: toast-out-bottom 0.15s ease-in-out forwards;
}
@keyframes toast-in-top {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes toast-in-bottom {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes toast-out-top {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-100%);
}
}
@keyframes toast-out-bottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(100%);
}
}
.toast-close {
position: absolute;
top: 4px;
right: 4px;
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.toast-close:hover {
transform: scale(1.5);
}
@media (max-width: 480px) {
.toast-container {
width: 100%;
padding: 0.5rem;
}
.toast {
max-width: 100%;
width: 100%;
margin: 0.25rem;
}
}
178 changes: 178 additions & 0 deletions dist/toastify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use strict";
(() => {
var Toastify;
((Toastify2) => {
class Manager {
static timeoutMap = /* @__PURE__ */ new Map();
static containers = /* @__PURE__ */ new Map();
static getContainer(gravity, position) {
const containerId = `toast-container-${gravity}-${position}`;
if (this.containers.has(containerId)) {
return this.containers.get(containerId);
}
return this.createContainer(containerId, gravity, position);
}
static createContainer(id, gravity, position) {
const container = document.createElement("div");
container.classList.add("toast-container", id, `toast-${gravity}`, `toast-${position}`);
container.setAttribute("role", "region");
container.setAttribute("aria-label", `Toast notifications - ${gravity} ${position}`);
document.body.appendChild(container);
this.containers.set(id, container);
return container;
}
static addTimeout(toast, duration, callback) {
this.delTimeout(toast);
const timeoutId = window.setTimeout(() => {
callback();
this.delTimeout(toast);
}, duration);
this.timeoutMap.set(toast, timeoutId);
}
static delTimeout(toast) {
if (this.timeoutMap.has(toast)) {
clearTimeout(this.timeoutMap.get(toast));
this.timeoutMap.delete(toast);
}
}
}
class Builder {
static build(toast) {
this.applyBaseStyles(toast);
this.addContent(toast);
this.addInteractiveElements(toast);
}
static applyBaseStyles(toast) {
toast.element.setAttribute("aria-live", toast.ariaLive);
toast.element.classList.add(
"toast",
`toast-${toast.gravity}`,
`toast-${toast.position}`
);
if (toast.options.className) toast.element.classList.add(toast.options.className);
if (toast.options.style) this.applyCustomStyles(toast.element, toast.options.style);
}
static applyCustomStyles(element, styles) {
for (const key in styles) {
element.style[key] = styles[key];
}
}
static addContent(toast) {
if (toast.options.text) toast.element.textContent = toast.options.text;
if (toast.options.node) toast.element.appendChild(toast.options.node);
}
static addInteractiveElements(toast) {
if (toast.close) this.addCloseButton(toast);
if (toast.onClick) toast.element.addEventListener("click", (e) => toast.onClick?.(e));
}
static addCloseButton(toast) {
const closeBtn = document.createElement("span");
closeBtn.ariaLabel = "Close";
closeBtn.className = "toast-close";
closeBtn.textContent = "🗙";
closeBtn.addEventListener("click", (e) => toast.hide());
toast.element.appendChild(closeBtn);
}
}
class Toast2 {
defaults = {
duration: 3e3,
gravity: "top",
position: "right",
ariaLive: "polite",
close: false,
stopOnFocus: true,
oldestFirst: true
};
options;
element;
root;
gravity;
position;
ariaLive;
close;
oldestFirst;
stopOnFocus;
onClick;
onClose;
/**
* Create a Toastify instance
* @param options User configuration options
*/
constructor(options) {
this.options = {
...this.defaults,
...options
};
this.element = document.createElement("div");
this.gravity = this.options.gravity;
this.position = this.options.position;
this.root = this.options.root ?? Manager.getContainer(this.gravity, this.position);
this.close = this.options.close;
this.oldestFirst = this.options.oldestFirst;
this.stopOnFocus = this.options.stopOnFocus;
this.ariaLive = this.options.ariaLive;
if (this.options.onClick) this.onClick = this.options.onClick;
if (this.options.onClose) this.onClose = this.options.onClose;
Builder.build(this);
}
/**
* Display the Toast notification
* @returns this Instance for method chaining
*/
show() {
const elementToInsert = this.oldestFirst ? this.root.firstChild : this.root.lastChild;
this.root.insertBefore(this.element, elementToInsert);
if (!this.element.classList.replace("hide", "show")) {
this.element.classList.add("show");
}
if (this.options.duration && this.options.duration > 0) {
if (this.options.stopOnFocus) {
this.element.addEventListener("mouseover", () => {
Manager.delTimeout(this);
});
this.element.addEventListener("mouseleave", () => {
Manager.addTimeout(this, this.options.duration, () => this.hide());
});
}
Manager.addTimeout(this, this.options.duration, () => this.hide());
}
return this;
}
/**
* @deprecated This function is deprecated. Use the show() instead.
*/
showToast() {
return this.show();
}
/**
* Immediately hide the current Toast
* Triggers a CSS exit animation and removes the element after the animation completes
*/
hide() {
if (!this.element) return;
Manager.delTimeout(this);
const handleAnimationEnd = () => {
this.element?.removeEventListener("animationend", handleAnimationEnd);
this.element?.remove();
this.onClose?.();
};
this.element.addEventListener("animationend", handleAnimationEnd);
if (!this.element.classList.replace("show", "hide")) {
this.element.classList.add("hide");
}
}
/**
* @deprecated This function is deprecated. Use the hide() instead.
*/
hideToast() {
this.hide();
}
}
Toastify2.Toast = Toast2;
})(Toastify || (Toastify = {}));
function Toast(options) {
return new Toastify.Toast(options);
}
globalThis.Toast = Toast;
})();
1 change: 1 addition & 0 deletions dist/toastify.min.css
2 changes: 2 additions & 0 deletions dist/toastify.min.js
7 changes: 7 additions & 0 deletions dist/toastify.min.js.map
23 changes: 11 additions & 12 deletions example/script.js
Original file line number Diff line number Diff line change
@@ -4,33 +4,32 @@ var bgColors = [
],
i = 0;

Toastify({
Toast({
text: "Hi",
duration: 4500,
destination: "https://github.com/apvarun/toastify-js",
newWindow: true,
gravity: "top",
position: 'left',
}).showToast();
}).show();

setTimeout(function() {
Toastify({
Toast({
text: "Simple JavaScript Toasts",
gravity: "top",
position: 'center',
style: {
background: '#0f3443'
}
}).showToast();
}).show();
}, 1000);

// Options for the toast
var options = {
text: "Happy toasting!",
duration: 2500,
callback: function() {
onClose: function() {
console.log("Toast hidden");
Toastify.reposition();
},
close: true,
style: {
@@ -39,34 +38,34 @@ var options = {
};

// Initializing the toast
var myToast = Toastify(options);
var myToast = Toast(options);

// Toast after delay
setTimeout(function() {
myToast.showToast();
myToast.show();
}, 4500);

setTimeout(function() {
Toastify({
Toast({
text: "Highly customizable",
gravity: "bottom",
position: 'left',
close: true,
style: {
background: "linear-gradient(to right, #ff5f6d, #ffc371)",
}
}).showToast();
}).show();
}, 3000);

// Displaying toast on manual action `Try`
document.getElementById("new-toast").addEventListener("click", function() {
Toastify({
Toast({
text: "I am a toast",
duration: 3000,
close: i % 3 ? true : false,
style: {
background: bgColors[i % 2],
}
}).showToast();
}).show();
i++;
});
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -28,10 +28,10 @@ <h1>Toastify JS</h1>
<div class="docs">
<h2>Usage</h2>
<code>
<p>Toastify({</p>
<p>Toast({</p>
<p class="pad-left">text: "This is a toast",</p>
<p class="pad-left">duration: 3000</p>
<p>}).showToast();</p>
<p>}).show();</p>
</code>
</div>
<div class="repo">
@@ -42,7 +42,7 @@ <h2>Usage</h2>
</div>
</body>

<script type="text/javascript" src="src/toastify.js"></script>
<script type="text/javascript" src="dist/toastify.js"></script>
<script type="text/javascript" src="example/script.js"></script>

<script>
497 changes: 497 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
{
"name": "toastify-js",
"name": "toastify",
"version": "1.12.0",
"description":
"Toastify is a lightweight, vanilla JS toast notification library.",
"main": "./src/toastify.js",
"description": "Toastify is a lightweight, vanilla JS toast notification library.",
"main": "./dist/toastify.js",
"repository": {
"type": "git",
"url": "git+https://github.com/apvarun/toastify-js.git"
},
"keywords": ["toastify", "javascript", "notifications", "toast"],
"keywords": [
"toastify",
"javascript",
"notifications",
"toast"
],
"author": "Varun A P",
"license": "MIT",
"bugs": {
"url": "https://github.com/apvarun/toastify-js/issues"
},
"homepage": "https://github.com/apvarun/toastify-js#readme"
}
"homepage": "https://github.com/apvarun/toastify-js#readme",
"type": "module",
"devDependencies": {
"esbuild": "^0.25.0",
"typescript": "^5.7.2"
},
"scripts": {
"build": "node ./build/build.js"
}
}
466 changes: 0 additions & 466 deletions src/toastify-es.js

This file was deleted.

189 changes: 129 additions & 60 deletions src/toastify.css
Original file line number Diff line number Diff line change
@@ -1,85 +1,154 @@
/*!
* Toastify js 1.12.0
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/

.toastify {
padding: 12px 20px;
color: #ffffff;
display: inline-block;
box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3);
background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);
background: linear-gradient(135deg, #73a5ff, #5477f5);
.toast-container {
position: fixed;
opacity: 0;
transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
border-radius: 2px;
cursor: pointer;
text-decoration: none;
max-width: calc(50% - 20px);
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
max-width: 100%;
box-sizing: border-box;
transition: transform 0.2s ease, opacity 0.2s ease;
}

.toastify.on {
opacity: 1;
.toast-container.toast-top {
top: 0;
}

.toast-close {
background: transparent;
border: 0;
.toast-container.toast-bottom {
bottom: 0;
}

.toast-container.toast-left {
left: 0;
align-items: flex-start;
}
.toast-container.toast-center {
left: 50%;
transform: translateX(-50%);
align-items: center;
}
.toast-container.toast-right {
right: 0;
align-items: flex-end;
}

.toast {
position: relative;
background: rgb(55, 208, 255);
color: white;
cursor: pointer;
font-family: inherit;
font-size: 1em;
opacity: 0.4;
padding: 0 5px;
padding: 1rem 2rem;
border-radius: 4px;
font-family: sans-serif;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
cursor: default;
transition: transform 0.5s ease, opacity 0.15s ease;
max-width: 480px;
}

.toastify-right {
right: 15px;
.toast:hover {
z-index: 2147483647 !important;
transform: scale(1.25) !important;
will-change: transform;
}

.toastify-left {
left: 15px;
.toast.toast-top.toast-left {
transform-origin: left center;
}
.toast.toast-top.toast-center {
transform-origin: top;
}
.toast.toast-top.toast-right {
transform-origin: right center;
}

.toastify-top {
top: -150px;
.toast.toast-bottom.toast-left {
transform-origin: left center;
}
.toast.toast-bottom.toast-center {
transform-origin: bottom;
}
.toast.toast-bottom.toast-right {
transform-origin: right center;
}

.toastify-bottom {
bottom: -150px;
.toast-container.toast-top .toast.show {
animation: toast-in-top 0.3s ease-in-out forwards;
}
.toast-container.toast-bottom .toast.show {
animation: toast-in-bottom 0.3s ease-in-out forwards;
}
.toast-container.toast-top .toast.hide {
animation: toast-out-top 0.15s ease-in-out forwards;
}
.toast-container.toast-bottom .toast.hide {
animation: toast-out-bottom 0.15s ease-in-out forwards;
}

.toastify-rounded {
border-radius: 25px;
@keyframes toast-in-top {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.toastify-avatar {
width: 1.5em;
height: 1.5em;
margin: -7px 5px;
border-radius: 2px;
@keyframes toast-in-bottom {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.toastify-center {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
max-width: -moz-fit-content;
@keyframes toast-out-top {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-100%);
}
}

@keyframes toast-out-bottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(100%);
}
}

@media only screen and (max-width: 360px) {
.toastify-right, .toastify-left {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
.toast-close {
position: absolute;
top: 4px;
right: 4px;
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.toast-close:hover {
transform: scale(1.5);
}

@media (max-width: 480px) {
.toast-container {
width: 100%;
padding: 0.5rem;
}
.toast {
max-width: 100%;
width: 100%;
margin: 0.25rem;
}
}
445 changes: 0 additions & 445 deletions src/toastify.js

This file was deleted.

235 changes: 235 additions & 0 deletions src/toastify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
namespace Toastify {
type Gravity = "top" | "bottom";
type Position = "left" | "center" | "right";
type AriaLive = "off" | "polite" | "assertive";
type CSSProperties = Record<keyof CSSStyleDeclaration, string>;

/**
* Toastify configuration options interface
* @property {HTMLElement} [root] - Root element
* @property {string} [text] - Text content to display
* @property {Node} [node] - Custom DOM node as a text replacement
* @property {number} [duration=3000] - Auto-close delay (milliseconds)
* @property {boolean} [close] - Whether to show a close button
* @property {Gravity} [gravity="top"] - Display position (top/bottom)
* @property {Position} [position="left"] - Horizontal alignment
* @property {AriaLive} [ariaLive="polite"] - Screen reader announcement mode
* @property {string} [className] - Custom CSS class name
* @property {boolean} [stopOnFocus=true] - Pause auto-close on hover
* @property {() => void} [onClose] - Callback function after closing
* @property {(e: MouseEvent) => void} [onClick] - Click event callback
* @property {CSSProperties} [style] - Inline style configuration
* @property {boolean} [oldestFirst=true] - Notification order for new messages
*/
export interface Options {
root?: Element;
text?: string;
node?: Node;
duration?: number;
close?: boolean;
gravity?: Gravity;
position?: Position;
ariaLive?: AriaLive;
className?: string;
stopOnFocus?: boolean;
onClose?: () => void;
onClick?: (e: Event) => void;
style?: CSSProperties;
oldestFirst?: boolean;
}

class Manager {
private static timeoutMap = new Map<Toast, number>();
private static containers = new Map<string, HTMLElement>();

static getContainer(gravity: Gravity, position: Position): HTMLElement {
const containerId = `toast-container-${gravity}-${position}`;
if (this.containers.has(containerId)) {
return this.containers.get(containerId)!;
}
return this.createContainer(containerId, gravity, position);
}

private static createContainer(id: string, gravity: Gravity, position: Position): HTMLElement {
const container = document.createElement("div");
container.classList.add('toast-container', id, `toast-${gravity}`, `toast-${position}`);
container.setAttribute('role', 'region');
container.setAttribute('aria-label', `Toast notifications - ${gravity} ${position}`);
document.body.appendChild(container);
this.containers.set(id, container);
return container;
}

static addTimeout(toast: Toast, duration: number, callback: () => void) {
this.delTimeout(toast);
const timeoutId = window.setTimeout(() => {
callback();
this.delTimeout(toast);
}, duration);
this.timeoutMap.set(toast, timeoutId);
}

static delTimeout(toast: Toast) {
if (this.timeoutMap.has(toast)) {
clearTimeout(this.timeoutMap.get(toast)!);
this.timeoutMap.delete(toast);
}
}
}

class Builder {
static build(toast: Toast) {
this.applyBaseStyles(toast);
this.addContent(toast);
this.addInteractiveElements(toast);
}

private static applyBaseStyles(toast: Toast) {
toast.element.setAttribute('aria-live', toast.ariaLive);
toast.element.classList.add(
'toast',
`toast-${toast.gravity}`,
`toast-${toast.position}`
);
if (toast.options.className) toast.element.classList.add(toast.options.className);
if (toast.options.style) this.applyCustomStyles(toast.element, toast.options.style);
}
private static applyCustomStyles(element: HTMLElement, styles: CSSProperties) {
for (const key in styles) {
element.style[key] = styles[key];
}
}

private static addContent(toast: Toast) {
if (toast.options.text) toast.element.textContent = toast.options.text;
if (toast.options.node) toast.element.appendChild(toast.options.node);
}

private static addInteractiveElements(toast: Toast) {
if (toast.close) this.addCloseButton(toast);
if (toast.onClick) toast.element.addEventListener("click", e => toast.onClick?.(e));
}

private static addCloseButton(toast: Toast) {
const closeBtn = document.createElement("span");
closeBtn.ariaLabel = "Close";
closeBtn.className = "toast-close";
closeBtn.textContent = "🗙";
closeBtn.addEventListener("click", e => toast.hide());
toast.element.appendChild(closeBtn);
}
}

/**
* Toast
* @example
* new Toast({ text: "Hello World" }).show();
*/
export class Toast {
private readonly defaults: Options = {
duration: 3000,
gravity: "top",
position: 'right',
ariaLive: "polite",
close: false,
stopOnFocus: true,
oldestFirst: true,
};

public options: Options;

public element: HTMLElement;
public root: Element;
public gravity: Gravity;
public position: Position;
public ariaLive: AriaLive;
public close: boolean;
public oldestFirst: boolean;
public stopOnFocus: boolean;
public onClick?: (e: Event) => void;
public onClose?: () => void;

/**
* Create a Toastify instance
* @param options User configuration options
*/
constructor(options: Options) {
this.options = {
...this.defaults,
...options
};
this.element = document.createElement("div");
this.gravity = this.options.gravity!;
this.position = this.options.position!;
this.root = this.options.root ?? Manager.getContainer(this.gravity, this.position);
this.close = this.options.close!;
this.oldestFirst = this.options.oldestFirst!;
this.stopOnFocus = this.options.stopOnFocus!;
this.ariaLive = this.options.ariaLive!;
if (this.options.onClick) this.onClick = this.options.onClick;
if (this.options.onClose) this.onClose = this.options.onClose;
Builder.build(this);
}

/**
* Display the Toast notification
* @returns this Instance for method chaining
*/
public show(): this {
const elementToInsert = this.oldestFirst ? this.root.firstChild : this.root.lastChild;
this.root.insertBefore(this.element!, elementToInsert);
if (!this.element.classList.replace('hide', 'show')) {
this.element.classList.add('show')
}
if (this.options.duration && this.options.duration > 0) {
if (this.options.stopOnFocus) {
this.element.addEventListener("mouseover", () => {
Manager.delTimeout(this);
})
this.element.addEventListener("mouseleave",() => {
Manager.addTimeout(this, this.options.duration!, () => this.hide());
})
}
Manager.addTimeout(this, this.options.duration!, () => this.hide());
}
return this;
}
/**
* @deprecated This function is deprecated. Use the show() instead.
*/
public showToast() {
return this.show();
}

/**
* Immediately hide the current Toast
* Triggers a CSS exit animation and removes the element after the animation completes
*/
public hide(): void {
if (!this.element) return;
Manager.delTimeout(this);
const handleAnimationEnd = () => {
this.element?.removeEventListener('animationend', handleAnimationEnd);
this.element?.remove();
this.onClose?.();
};
this.element.addEventListener('animationend', handleAnimationEnd);
if (!this.element.classList.replace('show', 'hide')) {
this.element.classList.add('hide')
}
}
/**
* @deprecated This function is deprecated. Use the hide() instead.
*/
public hideToast(): void {
this.hide();
}
}
}
declare global {
function Toast(options: Toastify.Options): Toastify.Toast
}
export default function Toast(options: Toastify.Options): Toastify.Toast {
return new Toastify.Toast(options)
}
globalThis.Toast = Toast;
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["DOM","ES2022"],
"strict": true,
"newLine": "lf",
"moduleResolution": "node"
},
"include": [
"src/*.ts"
]
}