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);
+ });
+
+});