Add spinner
This commit is contained in:
parent
e3e524f406
commit
b8d5a92fc6
@ -472,16 +472,6 @@ button > * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loader {
|
||||
animation: blink 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#configForm label {
|
||||
display: block;
|
||||
}
|
||||
@ -502,8 +492,6 @@ button > * {
|
||||
width: 150px;
|
||||
margin: 3px 0;
|
||||
padding: 2px 4px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -526,6 +514,8 @@ button > * {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* alert */
|
||||
|
||||
.alert {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -545,6 +535,8 @@ button > * {
|
||||
border-top: 1px solid #555;
|
||||
box-shadow: 10px 10px 10px -8px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100000;
|
||||
opacity: 0;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
@ -552,6 +544,14 @@ button > * {
|
||||
border-top: 1px solid #d05858;
|
||||
}
|
||||
|
||||
.alert.in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.alert.out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.alert button {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
@ -564,6 +564,67 @@ button > * {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.alert.spinner {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.alert.spinner > span {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: spinner-dot 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.alert.spinner > span::after {
|
||||
left: 15px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: spinner-dot 1s infinite alternate;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.alert.spinner > span::before, .alert.spinner > span::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.alert.spinner > span::before {
|
||||
left: -15px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #9880ff;
|
||||
color: #9880ff;
|
||||
animation: spinner-dot 1s infinite alternate;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes spinner-dot {
|
||||
0% {
|
||||
background-color: #9880ff;
|
||||
}
|
||||
|
||||
50%, 100% {
|
||||
background-color: #ebe6ff;
|
||||
}
|
||||
}
|
||||
|
||||
/* chart */
|
||||
.ct-point {
|
||||
transition: 0.3s;
|
||||
@ -592,7 +653,7 @@ button > * {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* openlayers 3 popup */
|
||||
/* openlayers popup */
|
||||
.ol-popup {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
|
@ -24,8 +24,10 @@ export default class uAlert {
|
||||
/**
|
||||
* @typedef {Object} AlertOptions
|
||||
* @property {number} [autoClose=0] Optional autoclose delay time in ms, default 0 – no autoclose
|
||||
* @property {boolean} [hasButton] Optional can be closed by button click, default true when autoClose not set
|
||||
* @property {string} [id] Optional box id
|
||||
* @property {string} [class] Optional box class
|
||||
* @property {boolean} [fixed=false] Optional set fixed position, default false
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -35,6 +37,8 @@ export default class uAlert {
|
||||
*/
|
||||
constructor(message, options = {}) {
|
||||
this.autoClose = options.autoClose || 0;
|
||||
this.hasButton = typeof options.hasButton !== 'undefined' ? options.hasButton : this.autoClose === 0
|
||||
this.fixedPosition = options.fixed || false;
|
||||
const html = `<div class="alert"><span>${message}</span></div>`;
|
||||
this.box = uUtils.nodeFromHtml(html);
|
||||
if (options.id) {
|
||||
@ -43,7 +47,7 @@ export default class uAlert {
|
||||
if (options.class) {
|
||||
this.box.classList.add(options.class);
|
||||
}
|
||||
if (this.autoClose === 0) {
|
||||
if (this.hasButton) {
|
||||
const button = document.createElement('button');
|
||||
button.setAttribute('type', 'button');
|
||||
button.textContent = '×';
|
||||
@ -72,11 +76,18 @@ export default class uAlert {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.fixedPosition) {
|
||||
const top = uAlert.getPosition();
|
||||
if (top) {
|
||||
this.box.style.top = `${top}px`;
|
||||
}
|
||||
}
|
||||
document.body.appendChild(this.box);
|
||||
setTimeout(() => {
|
||||
if (this.box) {
|
||||
this.box.classList.add('in');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -84,10 +95,14 @@ export default class uAlert {
|
||||
clearTimeout(this.closeHandle);
|
||||
this.closeHandle = null;
|
||||
}
|
||||
if (this.box) {
|
||||
if (document.body.contains(this.box)) {
|
||||
document.body.removeChild(this.box);
|
||||
}
|
||||
if (this.box && document.body.contains(this.box)) {
|
||||
const element = this.box;
|
||||
requestAnimationFrame(() => {
|
||||
element.classList.add('out');
|
||||
setTimeout(() => {
|
||||
element.remove();
|
||||
}, 1000);
|
||||
});
|
||||
this.box = null;
|
||||
}
|
||||
}
|
||||
@ -129,4 +144,8 @@ export default class uAlert {
|
||||
return this.show(message, { class: 'toast', autoClose: 10000 });
|
||||
}
|
||||
|
||||
static spinner() {
|
||||
return this.show('', { class: 'spinner', hasButton: false, fixed: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -129,11 +129,18 @@ export default class GoogleMapsApi {
|
||||
* Display track
|
||||
* @param {uPositionSet} track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
* @return {Promise.<void>}
|
||||
*/
|
||||
displayTrack(track, update) {
|
||||
if (!track || !track.hasPositions) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promise = new Promise((resolve) => {
|
||||
google.maps.event.addListenerOnce(this.map, 'tilesloaded', () => {
|
||||
console.log('tilesloaded');
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
// init polyline
|
||||
const polyOptions = {
|
||||
strokeColor: config.strokeColor,
|
||||
@ -184,6 +191,7 @@ export default class GoogleMapsApi {
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -424,11 +424,18 @@ export default class OpenLayersApi {
|
||||
* Display track
|
||||
* @param {uPositionSet} track Track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
* @return {Promise.<void>}
|
||||
*/
|
||||
displayTrack(track, update) {
|
||||
if (!track || !track.hasPositions) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promise = new Promise((resolve) => {
|
||||
this.map.once('rendercomplete', () => {
|
||||
console.log('rendercomplete');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
let start = this.layerMarkers ? this.layerMarkers.getSource().getFeatures().length : 0;
|
||||
if (start > 0) {
|
||||
this.removePoint(--start);
|
||||
@ -457,6 +464,7 @@ export default class OpenLayersApi {
|
||||
extent = this.fitToExtent(extent);
|
||||
}
|
||||
this.setZoomToExtent(extent);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,7 +120,7 @@ export default class MapViewModel extends ViewModel {
|
||||
this.api.zoomToBounds(this.savedBounds);
|
||||
}
|
||||
if (this.state.currentTrack) {
|
||||
this.api.displayTrack(this.state.currentTrack, this.savedBounds === null);
|
||||
this.displayTrack(this.state.currentTrack, this.savedBounds === null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,14 +135,20 @@ export default class MapViewModel extends ViewModel {
|
||||
this.api.clearMap();
|
||||
if (track) {
|
||||
uObserve.observe(track, 'positions', () => {
|
||||
this.api.displayTrack(track, false);
|
||||
this.displayTrack(track, false);
|
||||
this.api.zoomToExtent();
|
||||
});
|
||||
this.api.displayTrack(track, true);
|
||||
this.displayTrack(track, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayTrack(track, update) {
|
||||
this.state.jobStart();
|
||||
this.api.displayTrack(track, update)
|
||||
.finally(() => this.state.jobStop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popup html
|
||||
* @param {number} id Position index
|
||||
|
42
js/src/spinner.js
Normal file
42
js/src/spinner.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* μ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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import uAlert from './alert.js';
|
||||
|
||||
export default class uSpinner {
|
||||
|
||||
constructor(state) {
|
||||
this.spinner = null;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.state.onChanged('activeJobs', (jobs) => {
|
||||
if (jobs > 0) {
|
||||
if (!this.spinner) {
|
||||
this.spinner = uAlert.spinner();
|
||||
}
|
||||
} else if (this.spinner) {
|
||||
this.spinner.destroy();
|
||||
this.spinner = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import uObserve from './observe.js';
|
||||
* @property {?uUser} currentUser
|
||||
* @property {boolean} showLatest
|
||||
* @property {boolean} showAllUsers
|
||||
* @property {number} activeJobs
|
||||
*/
|
||||
export default class uState {
|
||||
|
||||
@ -33,6 +34,15 @@ export default class uState {
|
||||
this.currentUser = null;
|
||||
this.showLatest = false;
|
||||
this.showAllUsers = false;
|
||||
this.activeJobs = 0;
|
||||
}
|
||||
|
||||
jobStart() {
|
||||
this.activeJobs++;
|
||||
}
|
||||
|
||||
jobStop() {
|
||||
this.activeJobs--;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,6 +173,7 @@ export default class TrackViewModel extends ViewModel {
|
||||
uAlert.error($._('notauthorized'));
|
||||
return;
|
||||
}
|
||||
this.state.jobStart();
|
||||
uTrack.import(form, auth.user)
|
||||
.then((trackList) => {
|
||||
if (trackList.length) {
|
||||
@ -186,6 +187,7 @@ export default class TrackViewModel extends ViewModel {
|
||||
.catch((e) => uAlert.error(`${$._('actionfailure')}\n${e.message}`, e))
|
||||
.finally(() => {
|
||||
this.model.inputFile = '';
|
||||
this.state.jobStop();
|
||||
});
|
||||
}
|
||||
|
||||
@ -199,6 +201,7 @@ export default class TrackViewModel extends ViewModel {
|
||||
if (!track) {
|
||||
this.state.currentTrack = null;
|
||||
} else if (!track.isEqualTo(this.state.currentTrack)) {
|
||||
this.state.jobStart();
|
||||
track.fetchPositions().then(() => {
|
||||
console.log(`currentTrack id: ${track.id}, loaded ${track.length} positions`);
|
||||
this.state.currentTrack = track;
|
||||
@ -206,7 +209,8 @@ export default class TrackViewModel extends ViewModel {
|
||||
this.model.showLatest = false;
|
||||
}
|
||||
})
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); });
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); })
|
||||
.finally(() => this.state.jobStop());
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,6 +247,7 @@ export default class TrackViewModel extends ViewModel {
|
||||
* Handle last position of all users request
|
||||
*/
|
||||
loadAllUsersPosition() {
|
||||
this.state.jobStart();
|
||||
uPositionSet.fetchLatest()
|
||||
.then((_track) => {
|
||||
if (_track) {
|
||||
@ -251,10 +256,12 @@ export default class TrackViewModel extends ViewModel {
|
||||
this.state.currentTrack = _track;
|
||||
}
|
||||
})
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); });
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); })
|
||||
.finally(() => this.state.jobStop());
|
||||
}
|
||||
|
||||
loadTrackList() {
|
||||
this.state.jobStart();
|
||||
uTrack.fetchList(this.state.currentUser)
|
||||
.then((_tracks) => {
|
||||
this.model.trackList = _tracks;
|
||||
@ -269,7 +276,8 @@ export default class TrackViewModel extends ViewModel {
|
||||
this.model.currentTrackId = '';
|
||||
}
|
||||
})
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); });
|
||||
.catch((e) => { uAlert.error(`${$._('actionfailure')}\n${e.message}`, e); })
|
||||
.finally(() => this.state.jobStop());
|
||||
}
|
||||
|
||||
showDialog() {
|
||||
|
@ -25,6 +25,7 @@ import MapViewModel from './mapviewmodel.js';
|
||||
import TrackViewModel from './trackviewmodel.js';
|
||||
import UserViewModel from './userviewmodel.js';
|
||||
import uAlert from './alert.js';
|
||||
import uSpinner from './spinner.js';
|
||||
import uState from './state.js';
|
||||
|
||||
const domReady = uInitializer.waitForDom();
|
||||
@ -39,13 +40,14 @@ Promise.all([ domReady, initReady ])
|
||||
|
||||
function start() {
|
||||
const state = new uState();
|
||||
|
||||
const spinner = new uSpinner(state);
|
||||
const mainVM = new MainViewModel(state);
|
||||
const userVM = new UserViewModel(state);
|
||||
const trackVM = new TrackViewModel(state);
|
||||
const mapVM = new MapViewModel(state);
|
||||
const chartVM = new ChartViewModel(state);
|
||||
const configVM = new ConfigViewModel(state);
|
||||
spinner.init();
|
||||
mainVM.init();
|
||||
userVM.init();
|
||||
trackVM.init();
|
||||
|
@ -24,6 +24,10 @@ describe('Alert tests', () => {
|
||||
const message = 'test message';
|
||||
let alert;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(window, 'requestAnimationFrame').and.callFake((callback) => callback());
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (alert) {
|
||||
alert.destroy();
|
||||
@ -79,6 +83,7 @@ describe('Alert tests', () => {
|
||||
|
||||
it('should render and destroy alert box', () => {
|
||||
// given
|
||||
spyOn(window, 'setTimeout').and.callFake((callback) => callback());
|
||||
const id = 'testId';
|
||||
const options = { id }
|
||||
alert = new uAlert(message, options);
|
||||
@ -96,6 +101,7 @@ describe('Alert tests', () => {
|
||||
|
||||
it('should show and autoclose alert box', (done) => {
|
||||
// given
|
||||
jasmine.clock().install();
|
||||
const id = 'testId';
|
||||
const options = { id: id, autoClose: 50 }
|
||||
|
||||
@ -103,7 +109,8 @@ describe('Alert tests', () => {
|
||||
alert = uAlert.show(message, options);
|
||||
// then
|
||||
expect(document.querySelector(`#${id}`)).not.toBeNull();
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
jasmine.clock().uninstall();
|
||||
setTimeout(() => {
|
||||
expect(document.querySelector(`#${id}`)).toBeNull();
|
||||
done();
|
||||
@ -112,12 +119,15 @@ describe('Alert tests', () => {
|
||||
|
||||
it('should close alert box on close button click', (done) => {
|
||||
// given
|
||||
jasmine.clock().install();
|
||||
const id = 'testId';
|
||||
const options = { id }
|
||||
alert = uAlert.show(message, options);
|
||||
const closeButton = alert.box.querySelector('button');
|
||||
// when
|
||||
closeButton.click();
|
||||
jasmine.clock().tick(5000);
|
||||
jasmine.clock().uninstall();
|
||||
// then
|
||||
setTimeout(() => {
|
||||
expect(document.querySelector(`#${id}`)).toBeNull();
|
||||
|
@ -281,7 +281,7 @@ describe('Google Maps map API tests', () => {
|
||||
expect(api.polies[0].path.length).toBe(track.length);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.length);
|
||||
expect(google.maps.Map.prototype.fitBounds).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.event.addListenerOnce).not.toHaveBeenCalled();
|
||||
expect(google.maps.event.addListenerOnce).toHaveBeenCalledTimes(1);
|
||||
expect(setTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -56,7 +56,7 @@ describe('MapViewModel tests', () => {
|
||||
'cleanup': { /* ignored */ },
|
||||
'zoomToBounds': { /* ignored */ },
|
||||
'zoomToExtent': { /* ignored */ },
|
||||
'displayTrack': { /* ignored */ },
|
||||
'displayTrack': Promise.resolve(),
|
||||
'clearMap': { /* ignored */ },
|
||||
'updateSize': { /* ignored */ }
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user