From 1c486354516a196553ce2727ba8fd8efa2481848 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 27 Apr 2024 23:55:47 +0300 Subject: [PATCH] Added new options (isHidePrev, isHideTitle, maxOpened); possibility to insert HTML into in the title and message block; added the ability to pass a validation function to the popup dialog --- README.md | 173 +++++++++---- demo.css | 318 ++++++++++++++++++------ demo.js | 129 ++++++++++ demo_img/404-error.png | Bin 0 -> 6993 bytes demo_img/info.png | Bin 0 -> 3168 bytes demo_img/send.png | Bin 0 -> 3007 bytes demo_img/success.png | Bin 0 -> 12694 bytes demo_img/warning.png | Bin 0 -> 9998 bytes index.html | 151 ++--------- notification-es.js | 299 ++++++++++++++++++++++ notification.css | 553 +++++++++++++++++++---------------------- notification.js | 292 +++++++++++++--------- 12 files changed, 1248 insertions(+), 667 deletions(-) create mode 100644 demo.js create mode 100644 demo_img/404-error.png create mode 100644 demo_img/info.png create mode 100644 demo_img/send.png create mode 100644 demo_img/success.png create mode 100644 demo_img/warning.png create mode 100644 notification-es.js diff --git a/README.md b/README.md index 0b1deb3..4ff944c 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,153 @@ -# notification -**Pop-up notifications** is a Javascript library for pop-up messages on a web page. Pure javascript and css, any dependencies. +# Notification +![Built with JavaScript](https://img.shields.io/badge/Built%20with-JavaScript-green?logo=javascript) +**Popup notifications** is a lightweight Javascript library for popup messages on a web page. Pure javascript and css, any dependencies. You can use it as toast messages or a single notification. -## Installation -Download files, then: -1. Link to notification.css `` +## Live Demo +[Click here](https://amaterasusan.github.io/notification/) -2. Link to notification.js `` +## Getting Started +### Just include files in the HTML: +``\ +`` + +### Or use it as an ES module in your project: +```javascript +import Notification from 'path/to/notification-es.js'; +``` + +## options +All options are optional +* **position**\ + top-right (default value)\ + bottom-right\ + top-left\ + bottom-left\ + center +* **duration**\ + default 5000\ + time in milliseconds the notification will be displayed\ + if duration is 0 - popup notification will be displayed all the time +* **isHidePrev**\ + default false\ + hide previous popup(s) or not +* **isHideTitle**\ + default false\ + hide title block or not\ + if it is set to true and the duration is 0,\ + an X close button will appear on the right side of the notification body to allow the popup to close. +* **maxOpened**\ + default 5, the maximum value can be set to 10 ## Usage -### options -``` -position - can have such values : - top-right //default value - bottom-right - top-left - bottom-left - center -duration time in milliseconds the notification will be displayed - default 4000 - -if duration is 0 - popup notification will be displayed all the time -``` ``` const popup = Notification({ position: 'top-left', - duration: 7000 + duration: 4000, + isHidePrev: false, + isHideTitle: false, + maxOpened: 3, }); -``` -### notification type -``` - info - success - warning - error - dialog +// or +const popup = Notification(); // options will be set by default + +// then later you can set any options like +popup.setProperty({ + duration: 5000, + isHidePrev: true, +}); ``` -### Info notification +### the following popup methods are available: +* error +* warning +* info +* success +* dialog +* setProperty +* hide + ``` +// error +popup.error({ + title: 'Oops', + message: `An error has occurred"`, +}); + +// or even insert HTML +popup.error({ + title: `
Oops
`, + message: `
+
+
An error has occurred
`, +}); + +// info popup.info({ title: 'Info', message: 'Info message' }); -``` -### Success notification -``` -popup.success({ - title: 'Success', - message: 'Success message' -}); -``` - -### Warning notification -``` +// warning popup.warning({ title: 'Warning', message: 'Warning message' }); -``` -### Error notification -``` -popup.error({ - title: 'Error', - message: 'Error message' +// success +popup.success({ + title: 'Success', + message: 'Success message' }); ``` -### Confirmation Dialog -If use "Confirmation dialog" two buttons are available [Ok] and [Cancel]. -The display time of the notification will not matter here, even if it has been set. -The third parameter is a callback function that is called when any of the buttons is pressed. +### Dialog +If use "Confirmation dialog" two buttons are available [Ok] and [Cancel].\ +The popup display time here will not matter even if it has been set,\ +callback function is called when any of the buttons is pressed.\ +You can also insert HTML. ``` popup.dialog({ title: 'Confirm', message: 'Confirm message', - (result) => { + callback: (result) => { console.log('result = ', result) } }); -``` \ No newline at end of file + +/* + Example with HTML + you can pass a validation function to be able to check the filled fields in the form and + not to close the popup immediately after clicking [Ok] +*/ +const validTextarea = () => { + let valid = true; + const textarea = document.querySelector('textarea[name="your_mess"]'); + if (textarea.value.trim() === '') { + valid = false; + textarea.focus(); + } + return valid; +}; + +popup.dialog({ + title:
Send
', + message: `
Your message*:
+ `, + callback:(result) => { + console.log('result = ', result) + }, + validFunc: validTextarea, +}); +``` + +## Authors + +👤 **Helen Nikitina** + +- Twitter: [@twitterhandle](https://twitter.com/@HelenNikit1ina ) + +## License +[![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/amaterasusan/notification/blob/master/LICENSE) + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/amaterasu.san) diff --git a/demo.css b/demo.css index e5ce3b3..551ec2d 100644 --- a/demo.css +++ b/demo.css @@ -1,41 +1,41 @@ -html, -body { - border: 0; +* { + box-sizing: border-box; margin: 0; + padding: 0; } body { font-family: 'Roboto', sans-serif; - width: 100%; - height: 100%; + font-size: 1rem; background-color: #1d1d22; + letter-spacing: 1px; } .container-form { position: relative; overflow: hidden; - height: 100%; - width: 70%; - margin: 25px auto; + height: auto; + width: 98vw; + max-width: 800px; + margin: 10px auto; display: flex; - background-color: #0d1117; - border: 1px solid #c6c6c6; + background-color: #0b101a; + border: 1px solid rgb(108, 117, 125); border-radius: 5px; - box-shadow: 0 0 8px#999999; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.4); } form { - width: 50%; + width: 100%; min-width: 200px; - padding: 15px 35px; + padding: 15px 20px; color: #fdfeff; - box-sizing: border-box; justify-content: center; flex-grow: 1; } form h1 { - font-size: 3em; + font-size: 2em; font-weight: 300; text-align: center; color: #2196f3; @@ -43,89 +43,117 @@ form h1 { margin-top: 0; } -.code { - position: relative; - overflow: auto; - width: 50%; - color: #f8f8f2; - font-size: 1.2em; - border-left: 1px solid #c6c6c6; - box-shadow: 0 0 8px#999999; - justify-content: left; - flex-grow: 1; - display: flex; - align-items: left; - padding: 0 !important; - margin: 0 !important; -} - -pre { - margin: 0 !important; - padding: 0 !important; - white-space: pre; -} - input, -select { +select, +textarea { border: 1px solid #c6c6c6; border-radius: 5px; text-align: left; width: 100%; - box-sizing: border-box; } .group { position: relative; - margin: 50px 0; - box-sizing: border-box; + margin: 35px 15px; } -.group input, +.group input:not([type="checkbox"]), .group select { - background: none; + background-color: transparent; color: #c6c6c6; - font-size: 18px; - padding: 10px 10px 10px 5px; + font-size: 1em; + padding: 10px; + padding-left: 5px; display: block; border: none; + border: 0; + outline: 0; border-radius: 0; - border-bottom: 1px solid #c6c6c6; + border-bottom: 1px solid rgba(108, 117, 125, 0.4); +} + +/* Select only webkit browsers */ +@media screen and (-webkit-min-device-pixel-ratio:0) { + select { + appearance: none; + padding-right: 15px; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center right; + } } .group select option { - color: #ededee; + color: #c5e4f9; background-color: #22232a; } .group input:focus, .group select:focus { - outline: none; + outline: none !important; } -.group input:focus ~ label, -.group select:focus ~ label, -.group input:valid ~ label, -.group select:valid ~ label { - top: -14px; - font-size: 12px; - color: #2196f3; - /*color: #04c484;*/ -} - -.group input:focus ~ .bar:before, -.group select:focus ~ .bar:before { +.group input:focus~.bar:before, +.group select:focus~.bar:before { width: 100%; } .group label { - color: #c6c6c6; - font-size: 16px; - font-weight: normal; position: absolute; - pointer-events: none; + top: -14px; left: 5px; - top: 10px; - transition: 300ms ease all; + font-size: 12px; + color: #2196f3; + font-weight: normal; + pointer-events: none; +} + +.group.inline { + margin: 15px; + display: flex; + justify-content: flex-start; + align-items: center; + border-bottom: 1px solid rgba(108, 117, 125, 0.4); + padding: 10px 0; +} + +.group.inline label { + position: relative; + top: 0; + font-size: 14px; + margin-left: 8px; + pointer-events: all; +} + +.wrapper-checkbox { + width: 30px !important; +} + +.custom-checkbox { + position: relative; + font-size: 1em; + height: 30px; + width: 30px; + padding: 0; + appearance: none; + border: 1px solid rgba(70, 96, 120, 0.9) !important; + border-radius: 6px !important; + background: transparent; +} + +.custom-checkbox:checked:before, +.custom-checkbox:checked:after { + content: "\2714"; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + font-size: 1.4em; + height: 100%; + width: 100%; + color: #2196f3; } .group .bar { @@ -140,32 +168,32 @@ select { bottom: 0px; position: absolute; background-color: #2196f3; - /*background-color: #04c484;*/ transition: 300ms ease all; left: 0%; } .btn-box { text-align: center; - margin: 20px 10px; + margin: 10px; } .btn { cursor: pointer; - background-color: #2196f3; + background-color: #0875ce; color: #deeffd; border: 0; - padding: 10px 20px; + padding: 18px 30px; font-size: 1.1em; border-radius: 3px; letter-spacing: 0.06em; text-decoration: none; outline: none; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + user-select: none; } .btn:hover { - background: #0875ce; + background-color: #2196f3; color: #fff; } @@ -197,3 +225,151 @@ select { opacity: 0.3; transition: 0s; } + + +/* examples insert html*/ +.wrapper-notification { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 10px; + height: 100%; + letter-spacing: 2px; +} + +.icons { + display: flex; + justify-content: center; + align-items: center; + width: 70px; + height: 70px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.icons.icon-info { + background-image: url('demo_img/info.png'); +} + +.icons.icon-warning { + background-image: url('demo_img/warning.png'); +} + +.icons.icon-success { + background-image: url('demo_img/success.png'); +} + +.icons.icon-error { + background-image: url('demo_img/404-error.png'); + width: 100px; +} + +.icons.small { + width: 24px; + height: 24px; +} + +.icons.medium { + width: 38px; + height: 38px; +} + +.icons.small.icon-send { + background-image: url('demo_img/send.png'); +} + +.wrapper-notification .title-cust { + color: #fff; + font-size: 1.1em; + font-weight: 600; +} + +.wrapper-notification .title-error { + -webkit-text-stroke: 1px red; + -webkit-text-fill-color: transparent; +} + +.wrapper-notification .title-info { + -webkit-text-stroke: 1px #084981; + -webkit-text-fill-color: transparent; +} + +.wrapper-notification .title-warning { + -webkit-text-stroke: 1px #ff4500; + -webkit-text-fill-color: transparent; +} + +.wrapper-notification .title-success { + -webkit-text-stroke: 1px #037a4a; + -webkit-text-fill-color: transparent; +} + +.wrapper-notification .title-dialog { + color: #02659b; + font-size: 1em; + font-weight: 600; +} + +.wrapper-notification .message { + flex: 1; +} + +.wrapper-notification .message-text-error { + color: rgb(66, 67, 67); + font-weight: 500; +} + +.label-message { + margin-bottom: 5px; + color: #033e5e; +} + +.asterisk { + color: #fa6868; + margin-left: 4px; +} + +textarea.popup-textarea { + font-weight: 500; + font-size: 0.9em; + color: #777; + outline: none; + border: 1px solid rgba(3, 62, 94, 0.15); + background: #f4f4f4; + border-radius: 5px; + padding: 0.6em; + width: 100%; + max-height: 150px; + resize: vertical; +} + +.popup-textarea::placeholder { + color: rgb(136, 136, 136, 0.7); + font-size: 0.88em; +} + +.popup-textarea.invalid { + animation: pulse-border 400ms 1; +} + +.popup-textarea.invalid::placeholder { + color: rgb(255, 0, 0); +} + +@keyframes pulse-border { + 0% { + border-color: gba(3, 62, 94, 0.15); + background: #f4f4f4; + } + + 50% { + border-color: rgba(255, 0, 0, 0.7); + background-color: rgb(255, 240, 240, 0.7); + } + + 100% { + border-color: gba(3, 62, 94, 0.15); + background: #f4f4f4; + } +} \ No newline at end of file diff --git a/demo.js b/demo.js new file mode 100644 index 0000000..9737fec --- /dev/null +++ b/demo.js @@ -0,0 +1,129 @@ +const defaultText = { + info: { + defaultTitle: 'Info', + defaultMessage: 'Default Info Message', + htmlTitle: '
Info
', + html: '
Please read the description carefully
', + }, + success: { + defaultTitle: 'Success', + defaultMessage: 'Default Success Message', + htmlTitle: '
OK
', + html: '
Your message has been sent successfully
', + }, + warning: { + defaultTitle: 'Warning', + defaultMessage: 'Default Warning Message', + htmlTitle: '
Warning
', + html: `
Don't forget to save your data, otherwise it may be lost
`, + }, + error: { + defaultTitle: 'Error', + defaultMessage: 'An error has occurred', + htmlTitle: '
Oops
', + html: `
The Page you're looking for isn't here
`, + }, + dialog: { + defaultTitle: 'Confirm', + defaultMessage: 'Default Confirm message', + htmlTitle: + '
Send
', + html: `
Your message*:
`, + }, +}; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function validTextarea() { + let valid = true; + const textarea = document.querySelector('textarea[name="your_mess"]'); + if (!textarea) { + return valid; + } + if (textarea.value.trim() === '') { + valid = false; + textarea.focus(); + textarea.classList.add('invalid'); + textarea.placeholder = 'This field cannot be empty!'; + setTimeout(() => { + textarea.classList.remove('invalid'); + }, 400); + } + return valid; +} + +window.addEventListener('DOMContentLoaded', function () { + const form = document.querySelector('form'); + const titleEl = form.querySelector('#title'); + const messageEl = form.querySelector('#message'); + const positionEl = form.querySelector('#position'); + const durationEl = form.querySelector('#duration'); + const typeEl = form.querySelector('#type'); + const textOrHtmlEl = form.querySelector('#use-html'); + const hidePrevEl = form.querySelector('#hide-prev'); + const hideTitleEl = form.querySelector('#hide-title'); + const btn = form.querySelector('#show-notification'); + + // create popup with default option, now we can set it later + const popup = Notification(); + + typeEl.addEventListener('change', () => { + titleEl.value = defaultText[typeEl.value].defaultTitle; + messageEl.value = defaultText[typeEl.value].defaultMessage; + }); + + // show popup + btn.addEventListener('click', function (e) { + e.preventDefault(); + btn.disabled = true; + sleep(500).then(() => (btn.disabled = false)); + + // Form values + let title = titleEl.value; + let message = messageEl.value; + const type = typeEl.value; + + // set need property + popup.setProperty({ + position: positionEl.value, + duration: durationEl.value, + isHidePrev: hidePrevEl.checked, + isHideTitle: hideTitleEl.checked, + // maxOpened: 3, + }); + + let callback = null; + let validFunc = null; + if (textOrHtmlEl.checked) { + title = defaultText[type].htmlTitle || title; + message = defaultText[type].html || message; + if (type === 'dialog') { + validFunc = validTextarea; + } + } + + if (type === 'dialog') { + callback = (result) => { + console.log('result = ', result); + }; + } + + if (!popup[type]) { + popup.error({ + title: 'Error', + message: `Notification has no such method "${type}"`, + }); + return; + } + + popup[type]({ + title: title, + message: message, + callback: callback, + validFunc: validFunc, + }); + return false; + }); +}); diff --git a/demo_img/404-error.png b/demo_img/404-error.png new file mode 100644 index 0000000000000000000000000000000000000000..2f15196f77e7e55ca53b4c9740b24d62898b6078 GIT binary patch literal 6993 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 literal 0 HcmV?d00001 diff --git a/demo_img/warning.png b/demo_img/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..5012cdc2f461a313f9fba16652f3ecf57f1712f9 GIT binary patch literal 9998 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index a589f48..46d4900 100644 --- a/index.html +++ b/index.html @@ -8,13 +8,24 @@ Notifications -

Notifications

+
+
+ +
+
+
+ +
+
+
+ +
@@ -26,17 +37,17 @@
- +
@@ -45,138 +56,22 @@
+
- +
-
-
-            
-/*
-  position may be:
-    top-right //default value
-    bottom-right
-    top-left
-    bottom-left
-    center
-  default duration value: 4000(ms)
-*/
-// Example of using
-const popup = Notification({
-  position: 'top-left',
-  duration: 3500
-});
-
-popup.error({
-  title: 'Error',
-  message: 'An error has occurred'
-});
-/*
-  Available methods:
-    error
-    warning
-    success
-    info
-    dialog 
-    
-  If you use dialog - 
-    the third parameter is the callback function
-*/  
-        
-      
-
-
+ - - - + \ No newline at end of file diff --git a/notification-es.js b/notification-es.js new file mode 100644 index 0000000..ce2ef7b --- /dev/null +++ b/notification-es.js @@ -0,0 +1,299 @@ +/* + * Notification js + * https://amaterasusan.github.io/notification + * @license MIT licensed + * + * Copyright (C) 2024 Helen Nikitina + */ + +import './notification.css'; +export default function Notification(options = {}) { + let opts = {}; + let timeouts = {}; + const defNumberOpened = 5; + const defMaxNumberOpened = 10; + const defDuration = 5000; + const allowedPosition = ['top-right', 'bottom-right', 'top-left', 'bottom-left', 'center']; + const defaultOpts = { + position: 'top-right', + duration: defDuration, + isHidePrev: false, + isHideTitle: false, + maxOpened: defNumberOpened, + }; + + const setProperty = (obj = {}) => { + const defOpts = Object.keys(opts).length ? opts : defaultOpts; + opts = !!obj && obj.constructor.name === 'Object' ? Object.assign({}, defOpts, obj) : defOpts; + // check position + if (!allowedPosition.includes(opts.position)) { + opts.position = allowedPosition[0]; + } + // check duration + opts.duration = parseInt(opts.duration); + if (isNaN(opts.duration) || opts.duration < 1000) { + opts.duration = defDuration; + } + // check maxOpened + opts.maxOpened = parseInt(opts.maxOpened); + if (isNaN(opts.maxOpened) || opts.maxOpened < 1 || opts.maxOpened > defMaxNumberOpened) { + opts.maxOpened = defNumberOpened; + } + }; + + setProperty(options); + + // selectors + const classContainer = 'notification-container'; + const classPopup = 'notification'; + const animationInClass = 'animation-slide-in'; + const animationOutClass = 'animation-slide-out'; + const animationFadeInClass = 'animation-fade-in'; + const animationFadeOutClass = 'animation-fade-out'; + const titleTextSel = '.notification-title .title'; + const descSel = '.notification-desc'; + const closeSel = '.notification-close'; + const actionButSel = '.notification-action'; + const cancelButSel = '.notification-cancel'; + const overlayClass = 'overlay'; + + // class, defaultTitle and defaultMessage + const dataByType = { + dialog: { + classType: 'notification-dialog', + defaultTitle: 'Confirm', + defaultMessage: 'Default Confirm message', + }, + info: { + classType: 'notification-info', + defaultTitle: 'Info', + defaultMessage: 'default Info', + }, + success: { + classType: 'notification-success', + defaultTitle: 'Success', + defaultMessage: 'default Success', + }, + warning: { + classType: 'notification-warning', + defaultTitle: 'Warning', + defaultMessage: 'default Warning', + }, + error: { + classType: 'notification-error', + defaultTitle: 'Error', + defaultMessage: 'An error has occurred', + }, + }; + + const dialogButtons = () => { + return `
+ + +
`; + }; + + const createOverlay = () => { + if (!document.querySelector(`.${overlayClass}`)) { + const overlayEl = document.createElement('div'); + overlayEl.classList.add(overlayClass); + document.body.appendChild(overlayEl); + } + document.querySelector(`.${overlayClass}`).classList.add('active'); + }; + + const tempatePopup = (type) => { + const closeEl = ``; + const titleBlock = `
${closeEl}
`; + + return `${opts.isHideTitle ? '' : titleBlock} +
+
+ ${opts.isHideTitle && opts.duration === 0 && type !== 'dialog' ? closeEl : ''} +
+ ${type === 'dialog' ? dialogButtons() : ''}`; + }; + + const createMainContainer = () => { + let container = document.querySelector(`.${classContainer}.${opts.position}`); + + if (!container) { + container = document.createElement('div'); + container.classList = `${classContainer} ${opts.position}`; + document.body.appendChild(container); + } + + return container; + }; + + const createPopup = (type) => { + const container = createMainContainer(); + if (container.childElementCount >= opts.maxOpened) { + if (opts.position.includes('bottom')) { + hidePopUp(container.lastChild); + } else { + hidePopUp(container.firstChild); + } + } + + const elPopup = document.createElement('div'); + elPopup.classList.add( + classPopup, + opts.position === 'center' ? animationFadeInClass : animationInClass, + dataByType[type].classType + ); + elPopup.dataset.type = type; + elPopup.dataset.position = opts.position; + elPopup.dataset.id = new Date().getTime(); + + // insert template in element + elPopup.insertAdjacentHTML('beforeend', tempatePopup(type)); + + // create overlay + if (type === 'dialog') { + createOverlay(); + } + + // add element to container in the required sequence + if (opts.position.includes('bottom')) { + container.prepend(elPopup); + } else { + container.appendChild(elPopup); + } + + return elPopup; + }; + + const showPopup = ({ type, title, message, callback = null, validFunc = null } = {}) => { + if (opts.isHidePrev) { + hide(); + } + + const elPopup = createPopup(type); + + // set title and message + const elTitle = elPopup.querySelector(titleTextSel); + const elText = elPopup.querySelector(descSel); + if (elTitle) { + elTitle.innerHTML = title || dataByType[type].defaultTitle; + } + elText.innerHTML = message || dataByType[type].defaultMessage; + + if (type === 'dialog') { + // set buttons click event + setButtonsEvent(elPopup, callback, validFunc); + } else if (opts.duration) { + // store new timeout to timeouts obj if type is not dialog + const timeout = setTimeout(() => hidePopUp(elPopup), opts.duration); + timeouts[elPopup.dataset.id] = timeout; + } + + // add click event to close element + setCloseEvent(elPopup); + }; + + const setButtonsEvent = (elPopup, callback = null, validFunc = null) => { + const elAction = elPopup.querySelector(actionButSel); + elAction?.addEventListener( + 'click', + function handlerAction(event) { + event.stopPropagation(); + event.preventDefault(); + let valid = true; + if (validFunc) { + valid = validFunc(); + } + if (!valid) { + return false; + } + hidePopUp(elPopup); + + elAction.removeEventListener('click', handlerAction, false); + if (callback) { + return callback('ok'); + } + return false; + }, + false + ); + + const elCancel = elPopup.querySelector(cancelButSel); + elCancel?.addEventListener( + 'click', + function handlerCancel(event) { + event.stopPropagation(); + event.preventDefault(); + hidePopUp(elPopup); + + elCancel.removeEventListener('click', handlerCancel, false); + if (callback) { + return callback('cancel'); + } + return false; + }, + false + ); + }; + + const setCloseEvent = (elPopup) => { + const elClose = elPopup.querySelector(closeSel); + elClose?.addEventListener( + 'click', + function handlerClose() { + hidePopUp(elPopup); + elClose.removeEventListener('click', handlerClose, false); + }, + false + ); + }; + + const hidePopUp = (elPopup) => { + const container = document.querySelector(`.${classContainer}.${elPopup.dataset.position}`); + + clearTimeout(timeouts[elPopup.dataset.id]); + delete timeouts[elPopup.dataset.id]; + + // change animation class + elPopup.classList.remove(elPopup.dataset.position === 'center' ? animationFadeInClass : animationInClass); + elPopup.classList.add(elPopup.dataset.position === 'center' ? animationFadeOutClass : animationOutClass); + + if (elPopup.dataset.type === 'dialog') { + document.querySelector(`.${overlayClass}`)?.classList.remove('active'); + } + + setTimeout(function () { + if (elPopup.parentNode === container) { + container.removeChild(elPopup); + } + + // Remove container if it empty + if (!container?.hasChildNodes() && container?.parentElement === document.body) { + document.body.removeChild(container); + } + }, 400); + }; + + const hide = () => { + const containers = document.querySelectorAll(`.${classContainer}`); + document.querySelector(`.${overlayClass}`)?.classList.remove('active'); + + for (const key in timeouts) { + clearTimeout(timeouts[key]); + } + timeouts = {}; + + containers.forEach((container) => { + if (container && container.parentElement === document.body) { + document.body.removeChild(container); + } + }); + }; + + const dialog = ({ title, message, callback = null, validFunc = null }) => + showPopup({ type: 'dialog', title, message, callback, validFunc }); + const info = ({ title, message }) => showPopup({ type: 'info', title, message }); + const success = ({ title, message }) => showPopup({ type: 'success', title, message }); + const warning = ({ title, message }) => showPopup({ type: 'warning', title, message }); + const error = ({ title, message }) => showPopup({ type: 'error', title, message }); + return { dialog, info, success, warning, error, setProperty, hide }; +} diff --git a/notification.css b/notification.css index 65600c9..6991bbe 100644 --- a/notification.css +++ b/notification.css @@ -1,99 +1,101 @@ /* Notification */ -:root { - --notification-border-radius: 6px; - --notification-border-width: 1px; - --notification-border-left-width: 6px; - --notification-box-shadow: rgba(36, 30, 30, 0.3); - --notification-hover-shadow: rgba(36, 30, 30, 0.5); - --notification-close-x-color-light: #ffffff; - --notification-close-x-color-dark: #bd362f; - --overlay-bg: rgba(0, 0, 0, 0.1); - /* default */ - --notification-default-bg: rgba(255, 255, 255, 0.99); - --notification-default-color: rgba(0, 0, 0, 0.7); - --notification-default-lborder-color: #519798; - --notification-default-border-color: #8e9d9e; - --notification-default-title-color: #427071; - --notification-default-desc-color: #427071; - --notification-default-icon-color: #dea90b; - --notification-default-close-x-color: #519798; +.notification-container { + --popup-min-width: 200px; + --popup-max-height: 400px; + --min-header-height: 42px; + --popup-border-radius: 8px; + --popup-border-width: 1px; + --popup-border-left-width: 6px; + --popup-border-color: transparent; + --popup-box-shadow: 0 0 20px rgba(0, 0, 0, 0.6); + --popup-dividing-border: 1px solid rgba(0, 0, 0, 0.05); + /* dialog */ + --popup-dialog-bg: rgba(255, 255, 255, 0.93); + --popup-dialog-border-width: 2px; + --popup-dialog-border-left-width: 8px; + --popup-dialog-lborder-color: #0b8dad; + --popup-dialog-border-color: rgba(0, 0, 0, 0.2); + --popup-dialog-title-color: #222; + --popup-dialog-desc-color: #444; + --popup-dialog-close-x-color: #a61818; + --popup-dialog-box-shadow: -1px -1px 10px rgba(255, 255, 255, 0.4), + 5px 5px 15px rgba(0, 0, 0, 0.8); /* dialog buttons */ - --notification-buttons-tborder-color: #e6e2e2; - --notification-cancel-rborder-color: #d7d4d4; - --notification-button-bg: transparent; - --notification-button-color: #000000; - --notification-button-hover-bg: #ffffff; - --notification-action-color: #008060; - --notification-action-hover-color: #04adad; - --notification-cancel-color: #8b0000; - --notification-cancel-hover-color: #ff0000; + --popup-btns-dividing-border: none; + --popup-btn-border-width: 2px; + --popup-btn-min-height: 38px; + --popup-btn-min-width: 80px; + --popup-btn-radus: 8px; + --popup-btn-text-shadow: 1px 1px rgba(0, 0, 0, 0.6); + --popup-action-border: #12ba82; + --popup-action-color: #12ba82; + --popup-cancel-border: #bbb; + --popup-cancel-color: #a61818; + --popup-cancel-hover-color: #ff0000; + --popup-action-hover-color: #14d796; /* error */ - --notification-error-bg: #fff0f0; - --notification-error-color: #9f3a38; - --notification-error-border-color: #e0b4b4; - --notification-error-lborder-color: #ca0e0e; - --notification-error-desc-color: #994140; - --notification-error-icon-color: #951e1e; - --notification-error-close-x-color: #ca0e0e; + --popup-error-bg: #fff0f0; + --popup-error-color: #9f3a38; + --popup-error-border-color: #e0b4b4; + --popup-error-lborder-color: #ca0e0e; + --popup-error-title-color: #f01111; + --popup-error-close-x-color: #ca0e0e; /* warning */ - --notification-warning-bg: #fffaf3; - --notification-warning-color: #997240; - --notification-warning-border-color: #c9ba9b; - --notification-warning-lborder-color: #f7a307; - --notification-warning-desc-color: #997240; - --notification-warning-icon-color: #f7a307; - --notification-warning-close-x-color: #997240; + --popup-warning-bg: #fffaf3; + --popup-warning-color: #997240; + --popup-warning-border-color: #c9ba9b; + --popup-warning-lborder-color: #f7a307; + --popup-warning-title-color: #f07911; + --popup-warning-close-x-color: #997240; /* success */ - --notification-success-bg: #fcfff5; - --notification-success-color: #1a531b; - --notification-success-border-color: #6da16d; - --notification-success-lborder-color: #1e9520; - --notification-success-desc-color: #627961; - --notification-success-icon-color: #1e9520; - --notification-success-close-x-color: #6a9469; + --popup-success-bg: #fcfff5; + --popup-success-color: #1a531b; + --popup-success-border-color: #6da16d; + --popup-success-lborder-color: #1e9520; + --popup-success-title-color: #12ba82; + --popup-success-close-x-color: #6a9469; /* info */ - --notification-info-bg: #f8ffff; - --notification-info-color: #0e566c; - --notification-info-border-color: #558798; - --notification-info-lborder-color: #468498; - --notification-info-desc-color: #4c7786; - --notification-info-icon-color: #3492b1; - --notification-info-close-x-color: #659aaa; + --popup-info-bg: #f8ffff; + --popup-info-color: #064b84; + --popup-info-border-color: #295b83; + --popup-info-lborder-color: #0b86ea; + --popup-info-title-color: #2984c4; + --popup-info-close-x-color: #5a7184; } .notification-container { position: fixed; - font-family: 'Roboto', sans-serif; - font-size: 1.1em; - box-sizing: border-box; - z-index: 1000; - transition: all 0.5s linear; + overflow: hidden; + font-size: 1em; + z-index: 1001; + transition: all 0.4s linear; } .notification-container.center { - top: 50%; + top: 30%; left: 50%; - transform: translate(-50%, -50%); + transform: translate(-50%, -30%); + height: fit-content; } .notification-container.top-left { - left: 10px; - top: 10px; + left: 1vw; + top: 2vh; } .notification-container.bottom-left { - left: 10px; - bottom: 10px; + left: 1vw; + bottom: 2vh; } .notification-container.top-right { - right: 10px; - top: 10px; + right: 1vw; + top: 2vh; } .notification-container.bottom-right { - right: 10px; - bottom: 10px; + right: 1vw; + bottom: 2vh; } .notification-container.top-right .notification, @@ -104,90 +106,125 @@ .notification { position: relative; overflow: hidden; - max-height: 500px; + width: 350px; + min-width: var(--popup-min-width); + max-width: 98vw; + max-height: var(--popup-max-height); + margin-bottom: 6px; + border: var(--popup-border-width) solid var(--popup-border-color); + border-left-width: var(--popup-border-left-width); + border-radius: var(--popup-border-radius); + box-shadow: var(--popup-box-shadow); transition-property: all; transition-duration: 0.5s; transition-timing-function: cubic-bezier(0, 1, 0.5, 1); - margin: 0 0 6px; - opacity: 0.9; - border-width: var(--notification-border-width); - border-left-width: var(--notification-border-left-width); - border-style: solid; - border-color: transparent; - border-radius: var(--notification-border-radius); - box-shadow: 0 0 30px var(--notification-box-shadow); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; - z-index: 100; + z-index: 1001; } -.notification:hover { - box-shadow: 0 0 30px var(--notification-hover-shadow); - opacity: 1; - cursor: pointer; +.notification-title { + display: flex; + justify-content: space-between; + align-items: center; + min-height: var(--min-header-height); + line-height: var(--min-header-height); + border-bottom: var(--popup-dividing-border); } -.overlay { - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - background: var(--overlay-bg); - z-index: 10; - display: none; +.notification-title .title { + padding-left: 12px; + flex: 1; + font-size: 1.2em; + font-weight: 500; } -.overlay.active { - display: block; +.notification-body { + position: relative; + display: flex; + justify-content: flex-start; + align-items: center; + min-height: 56px; + padding: 0; + word-break: break-word !important; } -.notification:not(.notification-default) { - width: 310px; +.notification-body .notification-close { + position: absolute; + top: 4px; + right: 4px; } -.notification-default { - background: var(--notification-default-bg); - color: var(--notification-default-color); - border-color: var(--notification-default-border-color); - border-left-color: var(--notification-default-lborder-color); - width: 350px; +.notification-desc { + position: relative; + overflow: hidden; + flex: 1; + font-weight: 500; + line-height: 2em; + padding: 16px 12px; + max-height: calc(var(--popup-max-height) - var(--min-header-height)); +} + +/* spec */ +.notification-dialog { + position: relative; + background-color: var(--popup-dialog-bg); + color: var(--popup-dialog-desc-color); + border-color: var(--popup-dialog-border-color); + border-width: var(--popup-dialog-border-width); + border-left-width: var(--popup-dialog-border-left-width); + border-left-color: var(--popup-dialog-lborder-color); + box-shadow: var(--popup-dialog-box-shadow); +} + +.notification-dialog .notification-title { + color: var(--popup-dialog-title-color); } .notification-error { - background: var(--notification-error-bg); - color: var(--notification-error-color); - border-color: var(--notification-error-border-color); - border-left-color: var(--notification-error-lborder-color); + background-color: var(--popup-error-bg); + color: var(--popup-error-color); + border-color: var(--popup-error-border-color); + border-left-color: var(--popup-error-lborder-color); +} + +.notification-error .notification-title { + color: var(--popup-error-title-color); } .notification-success { - background: var(--notification-success-bg); - color: var(--notification-success-color); - border-color: var(--notification-success-border-color); - border-left-color: var(--notification-success-lborder-color); + background-color: var(--popup-success-bg); + color: var(--popup-success-color); + border-color: var(--popup-success-border-color); + border-left-color: var(--popup-success-lborder-color); +} + +.notification-success .notification-title { + color: var(--popup-success-title-color); } .notification-warning { - background: var(--notification-warning-bg); - color: var(--notification-warning-color); - border-color: var(--notification-warning-border-color); - border-left-color: var(--notification-warning-lborder-color); + background: var(--popup-warning-bg); + color: var(--popup-warning-color); + border-color: var(--popup-warning-border-color); + border-left-color: var(--popup-warning-lborder-color); +} + +.notification-warning .notification-title { + color: var(--popup-warning-title-color); } .notification-info { - background: var(--notification-info-bg); - color: var(--notification-info-color); - border-color: var(--notification-info-border-color); - border-left-color: var(--notification-info-lborder-color); + background: var(--popup-info-bg); + color: var(--popup-info-color); + border-color: var(--popup-info-border-color); + border-left-color: var(--popup-info-lborder-color); +} + +.notification-info .notification-title { + color: var(--popup-info-title-color); } .notification-close { - position: absolute; - right: 8px; - top: 8px; display: block; height: 24px; width: 24px; @@ -200,193 +237,92 @@ } .notification-close .close-x { - stroke: var(--notification-close-x-color-dark); fill: transparent; stroke-linecap: round; stroke-width: 5; } -.notification-default .close-x { - stroke: var(--notification-default-close-x-color); +.notification-dialog .close-x { + stroke: var(--popup-dialog-close-x-color); } .notification-error .close-x { - stroke: var(--notification-error-close-x-color); + stroke: var(--popup-error-close-x-color); } .notification-warning .close-x { - stroke: var(--notification-warning-close-x-color); + stroke: var(--popup-warning-close-x-color); } .notification-success .close-x { - stroke: var(--notification-success-close-x-color); + stroke: var(--popup-success-close-x-color); } .notification-info .close-x { - stroke: var(--notification-info-close-x-color); -} - -.notification-body { - align-items: center; - display: flex; - min-height: 56px; - /* width: 290px; */ - padding: 10px; - letter-spacing: 1px; - word-break: break-word !important; -} - -.notification-icon { - font-size: 3rem; - line-height: 3rem; - text-align: center; - padding: 0; - margin: 0; - line-height: 1; - vertical-align: middle; - text-align: left; - opacity: 0.8; -} - -.notification-error .notification-icon { - color: var(--notification-error-icon-color); -} - -.notification-error .notification-icon::before { - content: '\26A0'; -} - -.notification-default .notification-icon { - color: var(--notification-default-icon-color); -} - -.notification-default .notification-icon::before { - content: '\1F514'; -} - -.notification-warning .notification-icon { - color: var(--notification-warning-icon-color); -} - -.notification-warning .notification-icon::before { - content: '\26A0'; -} - -.notification-success .notification-icon { - color: var(--notification-success-icon-color); -} - -.notification-success .notification-icon::before { - content: '\2714'; -} - -.notification-info .notification-icon { - color: var(--notification-info-icon-color); -} - -.notification-info .notification-icon::before { - content: '\24D8'; -} - -.notification-body > div { - padding: 4px; - height: 100%; -} - -.notification .notification-title { - font-size: 18px; - font-weight: 600; - padding: 4px; - margin-top: -2px; -} - -.notification-default .notification-title { - color: var(--notification-default-title-color); -} - -.notification-desc { - padding: 4px; - font-size: smaller; - line-height: 1.6em; -} - -.notification-error .notification-desc { - color: var(--notification-error-desc-color); -} - -.notification-default .notification-desc { - color: var(--notification-default-desc-color); -} - -.notification-warning .notification-desc { - color: var(--notification-warning-desc-color); -} - -.notification-success .notification-desc { - color: var(--notification-success-desc-color); -} - -.notification-info .notification-desc { - color: var(--notification-info-desc-color); + stroke: var(--popup-info-close-x-color); } .bottom-right .notification.animation-slide-in, .top-right .notification.animation-slide-in { - animation: right-slide-in 0.5s forwards, bounceIn 0.7s forwards; + animation: slide-in 0.4s forwards, bounceIn 0.7s forwards; transform: translateX(100%); } .bottom-right .notification.animation-slide-out, .top-right .notification.animation-slide-out { - animation: right-slide-out 0.5s forwards; + animation: right-slide-out 0.4s forwards; } .top-left .notification.animation-slide-in, .bottom-left .notification.animation-slide-in { - animation: left-slide-in 0.5s forwards, bounceIn 0.7s forwards; + animation: slide-in 0.4s forwards, bounceIn 0.7s forwards; transform: translateX(-100%); } .top-left .notification.animation-slide-out, .bottom-left .notification.animation-slide-out { - animation: left-slide-out 0.5s forwards; + animation: left-slide-out 0.4s forwards; } .notification.animation-fade-in { - animation: fade-in 0.5s forwards, bounceIn 0.7s forwards; - opacity: 0; + animation: fade-in 0.4s forwards; } .notification.animation-fade-out { - animation: fade-out 0.5s forwards; + animation: fade-out 0.4s forwards; } .notification-buttons { display: flex; - justify-content: center; -} - -.notification-default .notification-buttons { - border-top: 1px solid var(--notification-buttons-tborder-color); + justify-content: space-around; + padding: 4px 0; + border-top: var(--popup-btns-dividing-border); } .notification-button { - background: var(--notification-button-bg); - align-items: center; - color: var(--notification-button-color); - display: flex; - justify-content: center; - flex-grow: 1; - min-height: 40px; - font-size: 24px; + position: relative; + display: inline-block; + cursor: pointer; + min-height: var(--popup-btn-min-height); + min-width: var(--popup-btn-min-width); font-weight: 600; - cursor: pointer; - cursor: pointer; + text-align: center; + vertical-align: middle; + border: var(--popup-btn-border-width) solid transparent; + border-radius: var(--popup-btn-radus); + text-shadow: var(--popup-btn-text-shadow); } -.notification-button:hover { - background: var(--notification-button-hover-bg); - text-decoration: none; +.notification-button::before { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.32em; } .notification-cancel:before { @@ -394,12 +330,12 @@ } .notification-cancel { - color: var(--notification-cancel-color); - border-right: 1px solid var(--notification-cancel-rborder-color); + color: var(--popup-cancel-color); + border-color: var(--popup-cancel-border); } .notification-cancel:hover { - color: var(--notification-cancel-hover-color); + color: var(--popup-cancel-hover-color); } .notification-action:before { @@ -407,14 +343,56 @@ } .notification-action { - color: var(--notification-action-color); + color: var(--popup-action-color); + border-color: var(--popup-action-border); } .notification-action:hover { - color: var(--notification-action-hover-color); + color: var(--popup-action-hover-color); } -@keyframes left-slide-in { +.overlay { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: rgba(0, 0, 0, 0.2); + z-index: 1000; + display: none; +} + +.overlay.active { + display: block; +} + +@keyframes fade-in { + 0% { + transform: scale(0); + visibility: hidden; + opacity: 0; + max-height: 0; + } + + 100% { + transform: scale(1); + visibility: visible; + opacity: 1; + max-height: var(--popup-max-height); + } +} + +@keyframes fade-out { + to { + transform: scale(0); + max-height: 0; + opacity: 0; + visibility: hidden; + } +} + + +@keyframes slide-in { to { transform: translateX(0); } @@ -422,29 +400,17 @@ @keyframes left-slide-out { to { - transform: translateX(-100%); - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; + transform: translateX(-120%); + margin: 0; max-height: 0; opacity: 0; } } -@keyframes right-slide-in { - to { - transform: translateX(0); - } -} - @keyframes right-slide-out { to { - transform: translateX(100%); - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; + transform: translateX(120%); + margin: 0; max-height: 0; opacity: 0; } @@ -455,29 +421,14 @@ transform: scale(0.84); opacity: 0.5; } + 75% { transform: scale(0.99); opacity: 0.9; } + 100% { transform: scale(1); opacity: 1; } -} - -@keyframes fade-in { - to { - opacity: 1; - } -} - -@keyframes fade-out { - to { - margin-top: 0; - margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; - max-height: 0; - opacity: 0; - } -} +} \ No newline at end of file diff --git a/notification.js b/notification.js index b8f932b..1bb75ca 100644 --- a/notification.js +++ b/notification.js @@ -1,32 +1,64 @@ -function Notification(opts) { +/* + * Notification js + * https://amaterasusan.github.io/notification + * @license MIT licensed + * + * Copyright (C) 2024 Helen Nikitina + */ +function Notification(options = {}) { + let opts = {}; + let timeouts = {}; + const defNumberOpened = 5; + const defMaxNumberOpened = 10; + const defDuration = 5000; + const allowedPosition = ['top-right', 'bottom-right', 'top-left', 'bottom-left', 'center']; const defaultOpts = { position: 'top-right', - duration: 4000, + duration: defDuration, + isHidePrev: false, + isHideTitle: false, + maxOpened: defNumberOpened, }; - opts = Object.assign({}, defaultOpts, opts); - opts.duration = parseInt(opts.duration) || 0; - const timeouts = []; + const setProperty = (obj = {}) => { + const defOpts = Object.keys(opts).length ? opts : defaultOpts; + opts = !!obj && obj.constructor.name === 'Object' ? Object.assign({}, defOpts, obj) : defOpts; + // check position + if (!allowedPosition.includes(opts.position)) { + opts.position = allowedPosition[0]; + } + // check duration + opts.duration = parseInt(opts.duration); + if (isNaN(opts.duration) || opts.duration < 1000) { + opts.duration = defDuration; + } + // check maxOpened + opts.maxOpened = parseInt(opts.maxOpened); + if (isNaN(opts.maxOpened) || opts.maxOpened < 1 || opts.maxOpened > defMaxNumberOpened) { + opts.maxOpened = defNumberOpened; + } + }; + + setProperty(options); // selectors - const classMainSelector = 'notification-container'; + const classContainer = 'notification-container'; const classPopup = 'notification'; const animationInClass = 'animation-slide-in'; const animationOutClass = 'animation-slide-out'; const animationFadeInClass = 'animation-fade-in'; const animationFadeOutClass = 'animation-fade-out'; - const titleSelector = '.notification-title'; - const descSelector = '.notification-desc'; - const closeSelector = '.notification-close'; - const actionButSelector = '.notification-action'; - const cancelButSelector = '.notification-cancel'; + const titleTextSel = '.notification-title .title'; + const descSel = '.notification-desc'; + const closeSel = '.notification-close'; + const actionButSel = '.notification-action'; + const cancelButSel = '.notification-cancel'; const overlayClass = 'overlay'; - const overlaySelector = `.${overlayClass}`; // class, defaultTitle and defaultMessage const dataByType = { dialog: { - classType: 'notification-default', + classType: 'notification-dialog', defaultTitle: 'Confirm', defaultMessage: 'Default Confirm message', }, @@ -52,26 +84,6 @@ function Notification(opts) { }, }; - const setPosition = (newPosition) => { - opts.position = newPosition; - }; - - const tempatePopup = () => { - return ` - - - - - -
-
-
-
-
-
-
`; - }; - const dialogButtons = () => { return `
@@ -79,12 +91,33 @@ function Notification(opts) {
`; }; - const createMainContainer = (position) => { - let container = document.querySelector(`.${classMainSelector}.${position}`); + const createOverlay = () => { + if (!document.querySelector(`.${overlayClass}`)) { + const overlayEl = document.createElement('div'); + overlayEl.classList.add(overlayClass); + document.body.appendChild(overlayEl); + } + document.querySelector(`.${overlayClass}`).classList.add('active'); + }; + + const tempatePopup = (type) => { + const closeEl = ``; + const titleBlock = `
${closeEl}
`; + + return `${opts.isHideTitle ? '' : titleBlock} +
+
+ ${opts.isHideTitle && opts.duration === 0 && type !== 'dialog' ? closeEl : ''} +
+ ${type === 'dialog' ? dialogButtons() : ''}`; + }; + + const createMainContainer = () => { + let container = document.querySelector(`.${classContainer}.${opts.position}`); if (!container) { container = document.createElement('div'); - container.classList = classMainSelector + ' ' + position; + container.classList = `${classContainer} ${opts.position}`; document.body.appendChild(container); } @@ -92,28 +125,31 @@ function Notification(opts) { }; const createPopup = (type) => { - const container = createMainContainer(opts.position); + const container = createMainContainer(); + if (container.childElementCount >= opts.maxOpened) { + if (opts.position.includes('bottom')) { + hidePopUp(container.lastChild); + } else { + hidePopUp(container.firstChild); + } + } const elPopup = document.createElement('div'); - - // add classes - elPopup.classList.add(classPopup); - elPopup.classList.add(opts.position === 'center' ? animationFadeInClass : animationInClass); - elPopup.classList.add(dataByType[type].classType); + elPopup.classList.add( + classPopup, + opts.position === 'center' ? animationFadeInClass : animationInClass, + dataByType[type].classType + ); + elPopup.dataset.type = type; + elPopup.dataset.position = opts.position; + elPopup.dataset.id = new Date().getTime(); // insert template in element - elPopup.insertAdjacentHTML('beforeend', tempatePopup()); + elPopup.insertAdjacentHTML('beforeend', tempatePopup(type)); - // add buttons if confirm dialog + // create overlay if (type === 'dialog') { - elPopup.insertAdjacentHTML('beforeend', dialogButtons()); - if (!document.querySelector(overlaySelector)) { - const overlayEl = document.createElement('div'); - overlayEl.classList.add(overlayClass); - document.body.appendChild(overlayEl); - } - - document.querySelector(overlaySelector).classList.add('active'); + createOverlay(); } // add element to container in the required sequence @@ -126,13 +162,48 @@ function Notification(opts) { return elPopup; }; - const setButtonsEvent = (elPopup, callback = null) => { - const elAction = elPopup.querySelector(actionButSelector); - elAction.addEventListener( + const showPopup = ({ type, title, message, callback = null, validFunc = null } = {}) => { + if (opts.isHidePrev) { + hide(); + } + + const elPopup = createPopup(type); + + // set title and message + const elTitle = elPopup.querySelector(titleTextSel); + const elText = elPopup.querySelector(descSel); + if (elTitle) { + elTitle.innerHTML = title || dataByType[type].defaultTitle; + } + elText.innerHTML = message || dataByType[type].defaultMessage; + + if (type === 'dialog') { + // set buttons click event + setButtonsEvent(elPopup, callback, validFunc); + } else if (opts.duration) { + // store new timeout to timeouts obj if type is not dialog + const timeout = setTimeout(() => hidePopUp(elPopup), opts.duration); + timeouts[elPopup.dataset.id] = timeout; + } + + // add click event to close element + setCloseEvent(elPopup); + }; + + const setButtonsEvent = (elPopup, callback = null, validFunc = null) => { + const elAction = elPopup.querySelector(actionButSel); + elAction?.addEventListener( 'click', function handlerAction(event) { event.stopPropagation(); event.preventDefault(); + let valid = true; + if (validFunc) { + valid = validFunc(); + } + if (!valid) { + return false; + } hidePopUp(elPopup); elAction.removeEventListener('click', handlerAction, false); @@ -144,8 +215,8 @@ function Notification(opts) { false ); - const elCancel = elPopup.querySelector(cancelButSelector); - elCancel.addEventListener( + const elCancel = elPopup.querySelector(cancelButSel); + elCancel?.addEventListener( 'click', function handlerCancel(event) { event.stopPropagation(); @@ -162,62 +233,11 @@ function Notification(opts) { ); }; - const hidePopUp = (elPopup) => { - const container = document.querySelector(`.${classMainSelector}.${opts.position}`); - - const firstTimeout = timeouts.shift(); - clearTimeout(firstTimeout); - - // change animation class - elPopup.classList.remove(opts.position === 'center' ? animationFadeInClass : animationInClass); - - elPopup.classList.add(opts.position === 'center' ? animationFadeOutClass : animationOutClass); - - setTimeout(function () { - if (elPopup.parentNode == container) { - container.removeChild(elPopup); - - if (opts.type === 'dialog') { - document.querySelector(overlaySelector).classList.remove('active'); - } - } - - // Remove container if it empty - if (!container.hasChildNodes()) { - document.body.removeChild(container); - } - }, 500); - }; - - const showPopup = ({ type, title, message, callback = null } = {}) => { - opts.type = type; - const elPopup = createPopup(type); - - // set title and message to created element - const elTitle = elPopup.querySelector(titleSelector); - const elText = elPopup.querySelector(descSelector); - - const titlePopup = title || dataByType[type].defaultTitle; - const messagePopup = message || dataByType[type].defaultMessage; - - elTitle.innerText = titlePopup; - elText.innerText = messagePopup; - - // click event - if (type === 'dialog') { - // set buttons click event - setButtonsEvent(elPopup, callback); - } else if (opts.duration) { - // push new timeout to timeouts array if type is not dialog - const timeout = setTimeout(() => hidePopUp(elPopup), opts.duration); - timeouts.push(timeout); - } - - // add click event to close element - const elClose = elPopup.querySelector(closeSelector); - elClose.addEventListener( + const setCloseEvent = (elPopup) => { + const elClose = elPopup.querySelector(closeSel); + elClose?.addEventListener( 'click', - function handlerClose(event) { + function handlerClose() { hidePopUp(elPopup); elClose.removeEventListener('click', handlerClose, false); }, @@ -225,11 +245,53 @@ function Notification(opts) { ); }; - const dialog = ({ title, message, callback = null }) => - showPopup({ type: 'dialog', title, message, callback }); + const hidePopUp = (elPopup) => { + const container = document.querySelector(`.${classContainer}.${elPopup.dataset.position}`); + + clearTimeout(timeouts[elPopup.dataset.id]); + delete timeouts[elPopup.dataset.id]; + + // change animation class + elPopup.classList.remove(elPopup.dataset.position === 'center' ? animationFadeInClass : animationInClass); + elPopup.classList.add(elPopup.dataset.position === 'center' ? animationFadeOutClass : animationOutClass); + + if (elPopup.dataset.type === 'dialog') { + document.querySelector(`.${overlayClass}`)?.classList.remove('active'); + } + + setTimeout(function () { + if (elPopup.parentNode === container) { + container.removeChild(elPopup); + } + + // Remove container if it empty + if (!container?.hasChildNodes() && container?.parentElement === document.body) { + document.body.removeChild(container); + } + }, 400); + }; + + const hide = () => { + const containers = document.querySelectorAll(`.${classContainer}`); + document.querySelector(`.${overlayClass}`)?.classList.remove('active'); + + for (const key in timeouts) { + clearTimeout(timeouts[key]); + } + timeouts = {}; + + containers.forEach((container) => { + if (container && container.parentElement === document.body) { + document.body.removeChild(container); + } + }); + }; + + const dialog = ({ title, message, callback = null, validFunc = null }) => + showPopup({ type: 'dialog', title, message, callback, validFunc }); const info = ({ title, message }) => showPopup({ type: 'info', title, message }); const success = ({ title, message }) => showPopup({ type: 'success', title, message }); const warning = ({ title, message }) => showPopup({ type: 'warning', title, message }); const error = ({ title, message }) => showPopup({ type: 'error', title, message }); - return { dialog, info, success, warning, error, setPosition }; + return { dialog, info, success, warning, error, setProperty, hide }; }