uix-kit
Version:
A free web kits for fast web design and development, compatible with Bootstrap v5.
461 lines (367 loc) • 18.8 kB
JavaScript
/*
*************************************
* <!-- Toast -->
*************************************
*/
import {
UixModuleInstance,
} from '@uixkit/core/_global/js';
import '../scss/_style.scss';
export const TOAST = ( ( module, $, window, document ) => {
if ( window.TOAST === null ) return false;
module.TOAST = module.TOAST || {};
module.TOAST.version = '0.2.0';
module.TOAST.documentReady = function( $ ) {
const REFER_LIB = "funda-ui";
const REFER_COMPONENT = "toast";
// Constants
const DEFAULT_AUTO_CLOSE_TIME = 3000;
const ANIM_SPEED = 300;
/**
* Toast
* @since 20250417
* @based https://github.com/xizon/funda-ui/tree/main/packages/Toast
* @param {*} props
*/
/**
* Configuration options for the FU_Toast function
* @typedef {Object} ToastOptions
* @property {(string|number|null|undefined)} [actionId='default'] - The identifier of toast, which can be used to determine whether different toast calls
* @property {(string|boolean)} [title=false] - Specifies an alternate and title for the toast
* @property {(string|boolean)} [note=false] - A light-colored comment next to the title, which can be information such as time
* @property {(string)} [message=false] - Specifies the content, or HTML elements to the toast
* @property {(string)} [hint=false] - Hint's content
* @property {(boolean)} [hintAutoShow=false] - Whether or not to automatically display additional hints
* @property {('primary'|'secondary'|'success'|'info'|'warning'|'danger'|'light'|'dark')} [theme='dark'] - Specifies a theme to .toast. All these colors are available as a Sass map of Bootstrap
* @property {string} [wrapperClassName=''] - The class name of the toast wrapper
* @property {boolean} [onlyShowOne=false] - Only ever going to show one toast at a time
* @property {boolean} [lock=false] - You can not close pop-win when it is enabled
* @property {boolean} [cascading=false] - Whether to use cascading styles
* @property {string} [schemeBody] - Self-defined class name for body, such as: 'align-items-center text-white bg-primary border-0'
* @property {string} [schemeHeader] - Self-defined class name for header, such as: 'text-white bg-dark'
* @property {string} [closeBtnColor] - Set the color of the close button
* @property {boolean} [closeDisabled=false] - Disable the close button
* @property {boolean} [reverseDisplay=false] - Allow data to be reversed
* @property {('bottom-left'|'bottom-center'|'bottom-right'|'top-left'|'top-center'|'top-right'|'vertical-center')} [direction='bottom-center'] - The direction of the toast
* @property {(boolean|number)} [autoCloseTime=false] - Set an automatic closing time, multiple items will be accumulated in order. Amount of time measured in milliseconds. If false or without this attribute, Auto-Close will be disabled
* @property {function(HTMLDivElement, string): void} [onClose] - Callback function when toast is closed. Parameters: (currentElement, currentToastId)
*/
/**
* Creates and displays a toast notification
* @param {ToastOptions} options - The configuration options for the toast
* @param {function(string): void} [callback] - Optional callback function that receives the toast ID
* @returns {{close: function(): void}} Object containing methods to control the toast
*
*/
/*
@Examples:
const showCustomToast1 = () => {
window.UixToast({
actionId: 'test-1',
title: "Custom Toast 1",
message: `<div>Text here (${Math.random()})</div>`,
autoCloseTime: 1000,
theme: 'warning',
schemeHeader: 'bg-transparent text-light',
onlyShowOne: true
});
};
const showCustomToast2 = () => {
window.UixToast({
actionId: 'test-2',
title: "Custom Toast 2",
message: `<div style="color:orange">Text here (${Math.random()})</div>`,
theme: "dark",
autoCloseTime: 5000,
direction: 'bottom-center',
schemeBody: 'align-items-center text-white border-0 p-2',
closeBtnColor: '#333',
hint: 'There are some warnings that need your attention',
hintAutoShow: false,
onClose: (currentElement, currentToastId) => {
console.log(currentElement, currentToastId);
}
});
};
const showCustomToast3 = () => {
window.UixToast({
actionId: 'test-3',
note: "11 mins ago",
message: `<div style="color:orange">Text here (${Math.random()})</div>`,
autoCloseTime: 5000,
cascading: true,
direction: 'bottom-right',
closeBtnColor: '#333',
reverseDisplay: true
});
};
const showCustomToast4 = () => {
window.UixToast({
actionId: 'test-4',
title: "Note",
message: `<div>Text here (${Math.random()})</div>`,
autoCloseTime: 5000,
direction: 'vertical-center',
theme: 'light',
schemeBody: '',
closeBtnColor: '#333'
});
};
const showCustomToast5 = () => {
window.UixToast({
actionId: 'test-5',
closeDisabled: true,
message: `<div style="color:orange">Text here (${Math.random()})</div>`,
autoCloseTime: 5000,
direction: 'bottom-left',
schemeHeader: 'p-0',
schemeBody: 'align-items-center text-white bg-dark border-0'
});
};
const showCustomToast6 = () => {
window.UixToast({
actionId: 'test-6',
direction: 'top-right',
title: "Custom Toast 1",
message: `<div>Text here (${Math.random()})</div>`,
theme: 'success',
schemeHeader: 'bg-transparent text-light',
autoCloseTime: 3000,
}, () => {
console.log('The presentation is complete');
});
};
<button onclick="showCustomToast1()">Custom Toast 1 (Only one is shown)</button>
<button onclick="showCustomToast2()">Custom Toast 2</button>
<button onclick="showCustomToast3()">Custom Toast 3</button>
<button onclick="showCustomToast4()">Custom Toast 4</button>
<button onclick="showCustomToast5()">Custom Toast 5</button>
<button onclick="showCustomToast6()">Custom Toast 6</button>
*/
const $$FU_Toast = (options, callback) => {
// Default configuration
const defaults = {
wrapperClassName: '',
actionId: 'default',
cascading: false,
autoCloseTime: DEFAULT_AUTO_CLOSE_TIME,
direction: "top-center",
schemeBody: "align-items-center text-white border-0 p-2",
closeBtnColor: "#fff",
reverseDisplay: false,
onlyShowOne: false,
onClose: (currentElement, currentToastId) => {},
// single message
theme: 'dark',
title: false,
note: false,
hint: false,
hintAutoShow: false,
message: ''
};
// Merge options with defaults
const config = { ...defaults, ...options };
// Generate unique ID for the toast
const uniqueID = Math.random().toString(36).substring(2, 9);
const toastId = 'toast-' + uniqueID;
// Create or get toast wrapper
let rootRef = document.getElementById('uix-toasts__wrapper-global-js__' + config.actionId);
if (!rootRef) {
rootRef = document.createElement('div');
rootRef.id = 'uix-toasts__wrapper-global-js__' + config.actionId;
// Combine all class names
const wrapperClasses = [
'uix-toasts__wrapper',
`uix-toasts__wrapper--${config.direction}`,
config.wrapperClassName
].filter(Boolean);
rootRef.className = wrapperClasses.join(' ');
//
if (config.cascading) rootRef.classList.add('uix-toasts__wrapper--cascading');
if (config.onlyShowOne) rootRef.classList.add('uix-toasts__wrapper--only-one');
const toastsContainer = document.createElement('div');
toastsContainer.className = 'uix-toasts';
rootRef.appendChild(toastsContainer);
document.body.appendChild(rootRef);
} else {
const wrapperClasses = [
'uix-toasts__wrapper',
`uix-toasts__wrapper--${config.direction}`,
config.wrapperClassName,
config.cascading ? 'uix-toasts__wrapper--cascading' : '',
config.onlyShowOne ? 'uix-toasts__wrapper--only-one' : ''
].filter(Boolean);
rootRef.className = wrapperClasses.join(' ');
}
// Create toast element
const toast = document.createElement('div');
toast.id = toastId;
toast.dataset.toastId = uniqueID;
toast.className = `toast-container ${config.onlyOne ? 'only-one' : ''} animate-ready`;
// Get the number of toasts in the current container as an index
const currentToasts = rootRef.querySelectorAll('.toast-container');
const index = currentToasts.length;
const itemDepth = currentToasts.length + 1;
// Calculate item style based on cascading option
const itemStyle = config.cascading ?
`transform: perspective(100px) translateZ(-${2 * index}px) translateY(${35 * index}px);
z-index: ${itemDepth};` :
`z-index: ${itemDepth}`;
toast.setAttribute('style', itemStyle);
toast.dataset.index = index;
//
const hideTitle = (config.title === '' || config.title === false) && (config.note === '' || config.note === false);
// Generate hint content based on auto show option
const hintContent = config.hint && config.hint !== '' ?
config.hintAutoShow ?
`<div class="app-io-hint-detail">${config.hint}</div>` :
`<a href="#" class="app-io-hint-btn">Details</a><div class="app-io-hint-detail d-none">${config.hint}</div>` :
'';
// Generate close button if not locked or disabled
const closeButton = !config.lock && !config.closeDisabled ?
`<button data-close="1" tabindex="-1" type="button" class="btn-close">
<svg width="12px" height="12px" viewBox="0 0 16 16">
<path fill="${config.closeBtnColor || '#000000'}" d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z" fill-rule="evenodd"></path>
</svg>
</button>` :
'';
// Generate toast item HTML
toast.innerHTML = `
<div class="toast fade show ${config.schemeBody || ''} ${config.theme ? `bg-${config.theme}` : ''}" role="alert">
${!hideTitle ? `
<div class="toast-header ${config.schemeHeader || ''}">
<strong class="me-auto">${config.title || ''}</strong>
<small class="text-muted">${config.note || ''}</small>
${closeButton}
</div>
` : ''}
<div class="toast-body">
${config.message}
${hintContent}
${hideTitle ? `<div class="position-absolute top-0 end-0 me-2 mt-2">${closeButton}</div>` : ''}
<div class="toast-progress active toast-progress ${config.autoCloseTime === false ? 'd-none' : ''}" role="progressbar">
<div class="progress-bar"></div>
</div>
</div>
</div>
`;
// Handle only show one
if (config.onlyShowOne) {
const existingToasts = rootRef.querySelectorAll('.toast-container');
existingToasts.forEach(t => t.remove());
}
// After adding a new toast, update the index and style of all existing toasts
function updateToastIndexes() {
const allToasts = rootRef.querySelectorAll('.toast-container');
allToasts.forEach((t, idx) => {
const depth = allToasts.length - idx;
const style = config.cascading ?
`transform: perspective(100px) translateZ(-${2 * idx}px) translateY(${35 * idx}px);
z-index: ${depth};` :
`z-index: ${depth}`;
t.setAttribute('style', style);
t.dataset.index = idx;
});
}
// Add toast to container
const toastsContainer = rootRef.querySelector('.uix-toasts');
if (config.reverseDisplay) {
toastsContainer.appendChild(toast);
} else {
toastsContainer.insertBefore(toast, toastsContainer.firstChild);
}
// Update the index of all toasts
updateToastIndexes();
// Animation
requestAnimationFrame(() => {
setTimeout(() => {
toast.classList.add('animate-in');
}, 50);
});
// Progress bar and auto close
if (config.autoCloseTime !== false) {
const progressBar = toast.querySelector('.toast-progress');
let startTime = Date.now();
let isPaused = false;
const updateProgress = () => {
if (!isPaused) {
const elapsed = Date.now() - startTime;
const progress = Math.max(0, 100 - (elapsed / config.autoCloseTime * 100));
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', progress);
if (progress > 0) {
requestAnimationFrame(updateProgress);
} else {
closeToast();
}
}
};
toast.addEventListener('mouseenter', () => {
isPaused = true;
});
toast.addEventListener('mouseleave', () => {
isPaused = false;
startTime = Date.now() - (config.autoCloseTime * (1 - parseInt(progressBar.getAttribute('aria-valuenow')) / 100));
requestAnimationFrame(updateProgress);
});
requestAnimationFrame(updateProgress);
}
// Close button handler
const closeBtn = toast.querySelector('.btn-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => closeToast());
}
// Close toast function
function closeToast() {
toast.classList.add('hide-start');
setTimeout(() => {
toast.classList.add('hide-end');
setTimeout(() => {
toast.remove();
// Update the index of all toasts
updateToastIndexes();
if (rootRef.querySelector('.uix-toasts').children.length === 0) {
rootRef.remove();
}
// Callback
if (typeof config.onClose === 'function') {
config.onClose(toast, toast.dataset.toastId);
}
}, ANIM_SPEED);
}, ANIM_SPEED);
}
// Initialize hint buttons
rootRef.querySelectorAll('.app-io-hint-btn').forEach(btn => {
if (!btn.dataset.click) {
btn.addEventListener('click', function(e) {
e.preventDefault();
const detail = e.target.nextElementSibling;
if (detail?.classList.contains('app-io-hint-detail')) {
detail.style.setProperty('display', 'block', 'important');
}
e.target.style.display = 'none';
});
btn.dataset.click = '1';
}
});
// Execute callback with toast ID
if (typeof callback === 'function') {
callback(toastId);
}
// Return methods for controlling the toast
return {
close: closeToast
};
}
// Add to global scope
if (typeof window !== 'undefined') {
window.UixToast = $$FU_Toast;
}
};
module.components.documentReady.push( module.TOAST.documentReady );
return class TOAST {
constructor() {
this.module = module;
}
};
})( UixModuleInstance, jQuery, window, document );