-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7e696a2
commit d442034
Showing
9 changed files
with
421 additions
and
17 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import {safeText} from "../utils.js"; | ||
|
||
export default class SnackBar { | ||
/** | ||
* SnackBar is a tiny notification displayed at the bottom of the screen. It's allowed to have only one SnackBar in | ||
* the same time. Creating a new SnackBar will remove the old one. Default living time is 15 sec after which | ||
* SnackBar disappears. | ||
* Properties: | ||
* - msgText - main text (e.g. Smth was deleted) | ||
* - btnText - OK button text (e.g. Undo) | ||
* - btnCb - function to be called when OK btn pressed | ||
* - ttl - aka TimeToLive in milliseconds | ||
*/ | ||
/** @param {string} - needed in order to be able to create multiple snackbars on a page */ | ||
#snackBarId; | ||
/** @param {number} - needed in order to delete the previous timeout when a new one appears */ | ||
#timeoutId; | ||
/** @param {HTMLElement} */ | ||
#snackBarEl; | ||
#onButtonClickCb = ()=>{} | ||
|
||
/** @param {string} id */ | ||
constructor(id = 'snackBar') { | ||
this.#snackBarId = id; | ||
} | ||
|
||
/** | ||
* @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop | ||
*/ | ||
show(prop) { | ||
SnackBar.#validateProperties(prop); | ||
this.#onButtonClickCb = prop.btnCb; | ||
this.#removeExistingIfNeeded(); | ||
this.#snackBarEl = this.#createSnackBarElement(this.#snackBarId, prop); | ||
this.#setEventListeners(); | ||
this.#timeoutId = setTimeout(() => { | ||
this.#removeEl() | ||
}, prop.ttl || 15 * 1000); | ||
} | ||
|
||
#getOkButton() { | ||
return this.#snackBarEl.querySelector('[js-ok]'); | ||
} | ||
|
||
#getCloseButton() { | ||
return this.#snackBarEl.querySelector('[js-close]'); | ||
} | ||
|
||
#getSnackbarEl() { | ||
return document.getElementById(`${this.#snackBarId}`); | ||
} | ||
|
||
#removeExistingIfNeeded(){ | ||
const existing = this.#getSnackbarEl(); | ||
if (existing) | ||
existing.remove(); | ||
if (this.#timeoutId) | ||
window.clearTimeout(this.#timeoutId) | ||
} | ||
|
||
#setEventListeners() { | ||
const okButton = this.#getOkButton(); | ||
if (okButton) { | ||
okButton.addEventListener('click', () => { | ||
this.#removeEl(); | ||
this.#onButtonClickCb(); | ||
}) | ||
} | ||
const closeButton = this.#getCloseButton(); | ||
closeButton.addEventListener('click', () => {this.#removeEl()}) | ||
} | ||
|
||
#removeEl() { | ||
this.#snackBarEl.remove(); | ||
const containerEl = SnackBar.#getSnackBarContainerEl(); | ||
if (containerEl.children.length === 0) | ||
containerEl.remove(); | ||
} | ||
|
||
/** | ||
* @param {string} id | ||
* @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop | ||
* @return {HTMLDivElement} | ||
*/ | ||
#createSnackBarElement(id, prop) { | ||
const containerEl = SnackBar.#getSnackBarContainerEl(); | ||
containerEl.insertAdjacentHTML("beforeend", SnackBar.#htmlTemplate(prop.msgText, prop.btnText, this.#snackBarId)); | ||
return containerEl.lastElementChild; | ||
} | ||
|
||
/** | ||
* @param {string} messageText | ||
* @param {string?} buttonText | ||
* @param {string} elementId | ||
* @return {string} | ||
*/ | ||
static #htmlTemplate(messageText, buttonText, elementId) { | ||
return ` | ||
<div id="${safeText(elementId)}" class="snackbar"> | ||
<div class="snackbar__label">${safeText(messageText)}</div> | ||
<div class="snackbar__buttons"> | ||
${buttonText ? `<button class="snackbar__button-ok" js-ok>${safeText(buttonText)}</button>` : ''} | ||
<button class="snackbar__button-close material-symbols-outlined" js-close title="Close">close</button> | ||
</div> | ||
</div>` | ||
} | ||
|
||
/** @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop */ | ||
static #validateProperties(prop) { | ||
if (!prop) throw new Error('No SnackBar properties'); | ||
if (!prop.msgText) throw new Error('Empty SnackBar message text'); | ||
if (prop.btnText && !prop.btnCb) throw new Error('No callback for SnackBar button'); | ||
if (!prop.btnText && prop.btnCb) throw new Error('No SnackBar button text'); | ||
if (prop.btnCb && (typeof prop.btnCb !== "function")) throw new Error('Callback for SnackBar button is not a function'); | ||
if (prop.ttl && (typeof prop.ttl !== "number")) throw new Error('TTL is not a number'); | ||
} | ||
|
||
static #getSnackBarContainerEl(){ | ||
let container = document.body.querySelector(".snackbar-container"); | ||
if (!container){ | ||
document.body.insertAdjacentHTML("beforeend", "<div class='snackbar-container'/>"); | ||
container = document.body.lastElementChild; | ||
} | ||
return container; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
|
||
.snackbar-container { | ||
--snackbar-background-color: rgb(51, 51, 51); | ||
--snackbar-background-color-hover: rgb(84, 84, 84); | ||
--snackbar-text-label-color: rgba(255, 255, 255, 0.87); | ||
--snackbar-text-action-color: rgb(187, 134, 252); | ||
--snackbar-text-action-disabled-color: rgb(140, 140, 140); | ||
z-index : 8; | ||
margin : 0.5rem; | ||
position : fixed; | ||
right : 0; | ||
bottom : 0; | ||
left : 0; | ||
display : flex; | ||
align-items : start; | ||
justify-content : center; | ||
box-sizing : border-box; | ||
flex-direction: column; | ||
} | ||
|
||
.snackbar { | ||
margin: 0.5rem; | ||
background-color : var(--snackbar-background-color); | ||
min-width : 344px; | ||
box-shadow : var(--box-shadow); | ||
max-width : 672px; | ||
border-radius : 4px; | ||
display : flex; | ||
align-items : center; | ||
justify-content : flex-start; | ||
} | ||
|
||
@media (max-width : 480px), (max-width : 344px){ | ||
.snackbar__surface{ | ||
min-width : 100%; | ||
} | ||
} | ||
|
||
.snackbar__label{ | ||
color : var(--snackbar-text-label-color); | ||
} | ||
|
||
.snackbar__label{ | ||
font-size : 0.875rem; | ||
line-height : 1.25rem; | ||
font-weight : 400; | ||
letter-spacing : 0.012rem; | ||
flex-grow : 1; | ||
box-sizing : border-box; | ||
margin : 0; | ||
padding : 1rem; | ||
} | ||
|
||
|
||
.snackbar__buttons { | ||
margin-left : 0.5rem; | ||
margin-right : 0.5rem; | ||
display : flex; | ||
flex-shrink : 0; | ||
align-items : center; | ||
box-sizing : border-box; | ||
} | ||
|
||
.snackbar__button-ok { | ||
background: var(--snackbar-background-color); | ||
color : var(--snackbar-text-action-color); | ||
border: none; | ||
cursor: pointer; | ||
padding: 0.5rem 1rem; | ||
border-radius: 0.25rem; | ||
margin-right: 0.5rem; | ||
} | ||
|
||
.snackbar__button-ok:disabled, .snackbar__button-close:disabled { | ||
border: none; | ||
color: var(--snackbar-text-action-disabled-color); | ||
pointer-events: none; | ||
} | ||
|
||
.snackbar__button-ok:hover, .snackbar__button-close:hover{ | ||
background: var(--snackbar-background-color-hover); | ||
} | ||
|
||
.snackbar__button-close { | ||
background: var(--snackbar-background-color); | ||
color : var(--snackbar-text-label-color); | ||
border: none; | ||
cursor: pointer; | ||
font-size: 1rem; | ||
width: 1.5rem; | ||
height: 1.5rem; | ||
border-radius: 50%; | ||
padding: 0; | ||
} |
Oops, something went wrong.