diff --git a/css/src/main.css b/css/src/main.css index a7ce408..c5651a0 100644 --- a/css/src/main.css +++ b/css/src/main.css @@ -526,6 +526,43 @@ button > * { margin: 0 5px; } +.alert { + position: fixed; + top: 0; + left: 50%; + width: 300px; + background: #666; + color: white; + font-family: "Open Sans", Verdana, sans-serif; + font-size: 0.8em; + line-height: 20px; + text-align: center; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + margin: 1em 0 1em -150px; + padding: 6px 20px; + border-radius: 5px; + border-top: 1px solid #555; + box-shadow: 10px 10px 10px -8px rgba(0, 0, 0, 0.3); +} + +.alert.error { + background: #d95b5b; + border-top: 1px solid #d05858; +} + +.alert button { + position: absolute; + top: -1px; + right: 0; + border: none; + margin: 0; + height: 100%; + background: none; + font-weight: normal; + font-size: 15px; +} + /* chart */ .ct-point { transition: 0.3s; diff --git a/js/src/alert.js b/js/src/alert.js new file mode 100644 index 0000000..4b661b9 --- /dev/null +++ b/js/src/alert.js @@ -0,0 +1,132 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import uUtils from './utils.js'; + +export default class uAlert { + + /** + * @typedef {Object} AlertOptions + * @property {number} [autoClose=0] Optional autoclose delay time in ms, default 0 – no autoclose + * @property {string} [id] Optional box id + * @property {string} [class] Optional box class + */ + + /** + * Builds alert box + * @param {string} message + * @param {AlertOptions} [options] Optional options + */ + constructor(message, options = {}) { + this.autoClose = options.autoClose || 0; + const html = `
${message}
`; + this.box = uUtils.nodeFromHtml(html); + if (options.id) { + this.box.id = options.id; + } + if (options.class) { + this.box.classList.add(options.class); + } + if (this.autoClose === 0) { + const button = document.createElement('button'); + button.setAttribute('type', 'button'); + button.textContent = '×'; + button.onclick = () => this.destroy(); + this.box.appendChild(button); + } + this.closeHandle = null; + } + + /** + * Calculate new box top offset + * @return {number} Top offset + */ + static getPosition() { + const boxes = document.querySelectorAll('.alert'); + const lastBox = boxes[boxes.length - 1]; + let position = 0; + if (lastBox) { + const maxPosition = document.body.clientHeight - 100; + position = lastBox.getBoundingClientRect().bottom; + if (position > maxPosition) { + position = maxPosition; + } + } + return position; + } + + render() { + const top = uAlert.getPosition(); + if (top) { + this.box.style.top = `${top}px`; + } + document.body.appendChild(this.box); + } + + destroy() { + if (this.closeHandle) { + clearTimeout(this.closeHandle); + this.closeHandle = null; + } + if (this.box) { + if (document.body.contains(this.box)) { + document.body.removeChild(this.box); + } + this.box = null; + } + } + + /** + * Show alert box + * @param {string} message + * @param {AlertOptions} [options] Optional options + * @return uAlert + */ + static show(message, options) { + const box = new uAlert(message, options); + box.render(); + if (box.autoClose) { + box.closeHandle = setTimeout(() => box.destroy(), box.autoClose); + } + return box; + } + + /** + * Show alert error box + * @param {string} message + * @param {Error=} e Optional error to be logged to console + * @return uAlert + */ + static error(message, e) { + if (e instanceof Error) { + console.error(`${e.name}: ${e.message} (${e.stack})`); + } + return this.show(message, { class: 'error' }); + } + + /** + * Show alert toast box + * @param {string} message + * @return uAlert + */ + static toast(message) { + return this.show(message, { class: 'toast', autoClose: 10000 }); + } + +} diff --git a/js/src/utils.js b/js/src/utils.js index 303d8cd..7cf527a 100644 --- a/js/src/utils.js +++ b/js/src/utils.js @@ -280,22 +280,6 @@ export default class uUtils { window.location.assign(url); } - /** - * @param {(Error|string)} e - * @param {string=} message - */ - static error(e, message) { - let details; - if (e instanceof Error) { - details = `${e.name}: ${e.message} (${e.stack})`; - } else { - details = e; - message = e; - } - console.error(details); - alert(message); - } - /** * Degrees to radians * @param {number} degrees diff --git a/js/test/alert.test.js b/js/test/alert.test.js new file mode 100644 index 0000000..6c3ff4c --- /dev/null +++ b/js/test/alert.test.js @@ -0,0 +1,143 @@ +/* + * μlogger + * + * Copyright(C) 2020 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import uAlert from '../src/alert.js'; + +describe('Alert tests', () => { + + const message = 'test message'; + let alert; + + afterEach(() => { + if (alert) { + alert.destroy(); + } + document.body.innerText = ''; + }); + + it('should create alert box with message', () => { + // when + alert = new uAlert(message); + const textEl = alert.box.firstChild; + // then + expect(textEl.innerText).toBe(message); + }); + + it('should create alert box with autoClose option', () => { + // given + const autoClose = 1; + const options = { autoClose } + // when + alert = new uAlert(message, options); + const textEl = alert.box.firstChild; + // then + expect(textEl.innerText).toBe(message); + expect(alert.autoClose).toBe(autoClose); + }); + + it('should create alert box with id option', () => { + // given + const id = 'testId'; + const options = { id } + // when + alert = new uAlert(message, options); + const boxEl = alert.box; + const textEl = alert.box.firstChild; + // then + expect(textEl.innerText).toBe(message); + expect(boxEl.id).toBe(id); + }); + + it('should create alert box with class option', () => { + // given + const className = 'test_class'; + const options = { class: className } + // when + alert = new uAlert(message, options); + const boxEl = alert.box; + const textEl = alert.box.firstChild; + // then + expect(textEl.innerText).toBe(message); + expect(boxEl.classList).toContain(className); + }); + + it('should render and destroy alert box', () => { + // given + const id = 'testId'; + const options = { id } + alert = new uAlert(message, options); + + // when + alert.render(); + // then + expect(document.querySelector(`#${id}`)).not.toBeNull(); + + // when + alert.destroy(); + // then + expect(document.querySelector(`#${id}`)).toBeNull(); + }); + + it('should show and autoclose alert box', (done) => { + // given + const id = 'testId'; + const options = { id: id, autoClose: 50 } + + // when + alert = uAlert.show(message, options); + // then + expect(document.querySelector(`#${id}`)).not.toBeNull(); + + setTimeout(() => { + expect(document.querySelector(`#${id}`)).toBeNull(); + done(); + }, 100); + }); + + it('should close alert box on close button click', (done) => { + // given + const id = 'testId'; + const options = { id } + alert = uAlert.show(message, options); + const closeButton = alert.box.querySelector('button'); + // when + closeButton.click(); + // then + setTimeout(() => { + expect(document.querySelector(`#${id}`)).toBeNull(); + done(); + }, 100); + }); + + it('should show error alert box', () => { + // when + alert = uAlert.error(message); + // then + expect(document.querySelector('.alert.error')).not.toBeNull(); + }); + + it('should show toast alert box', () => { + // when + alert = uAlert.toast(message); + // then + expect(document.querySelector('.alert.toast')).not.toBeNull(); + expect(alert.autoClose).toBeGreaterThan(0); + }); + +});