PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / resources / toast.js

toast.js in VikBooking Hotel Booking Engine & PMS trunk, at admin/resources/toast.js

607 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, w) {
2 'use strict';
3
4 /**
5 * Class used to handle screen messages displayed
6 * using a "toast" layout.
7 *
8 * In case the system is able to display several messages,
9 * it is suggested to always enqueue the messages instead
10 * of immediately dispatching them.
11 *
12 * How to init the toast message:
13 *
14 * VBOToast.create();
15 *
16 * VBOToast.create(VBOToast.POSITION_BOTTOM_RIGHT);
17 *
18 * How to dispatch/enqueue a message:
19 *
20 * VBOToast.dispatch('This is a message');
21 *
22 * VBOToast.enqueue({
23 * text: 'This is a successful message',
24 * status: 1,
25 * delay: 2000,
26 * });
27 */
28 w['VBOToast'] = class VBOToast {
29 /**
30 * Initiliazes the class for being used.
31 * Creates the HTML of the toast.
32 * This method is executed only once.
33 *
34 * In case this method is invoked in the head of the
35 * document, it must be placed within a "onready" statement.
36 *
37 * @param string position The position of the toast.
38 * @param string container The container to which append the toast.
39 *
40 * @return void
41 *
42 * @see changePosition() in case it is needed to change the
43 * position of the toast if it has been
44 * already loaded.
45 */
46 static create(position, container) {
47 // check if the toast message has been already created
48 if ($('#vbo-toast-wrapper').length == 0) {
49
50 if (!position) {
51 // use default position in case the parameter was not specified
52 position = VBOToast.POSITION_BOTTOM_CENTER;
53 }
54
55 if (!container || $(container).length == 0) {
56 // fallback to body
57 container = 'body';
58 }
59
60 // append toast HTML to body
61 $(container).append(
62 '<div class="vbo-toast-wrapper ' + position + '" id="vbo-toast-wrapper">\n'+
63 ' <div class="toast-message">\n'+
64 ' <div class="toast-message-content"></div>\n'+
65 ' </div>\n'+
66 '</div>\n'
67 );
68
69 // handle hover/leave events to prevent the toast
70 // disposes itself when the mouse is focusing it
71 $('#vbo-toast-wrapper').hover(() => {
72 // register flag when hovering the mouse
73 // above the toast message
74 VBOToast.mouseHover = true;
75 }, () => {
76 if (VBOToast.mouseHover && VBOToast.disposeDelay) {
77 // reset timeout
78 clearTimeout(VBOToast.timerHandler);
79
80 // schedule timeout again to dispose the toast
81 VBOToast.timerHandler = setTimeout(VBOToast.dispose, VBOToast.disposeDelay);
82 }
83
84 // clear flag
85 VBOToast.mouseHover = false;
86 });
87 }
88 }
89
90 /**
91 * Changes the position of the toast.
92 *
93 * @param string position The position in which the toast
94 * message will be displayed. See
95 * class constants to check all the
96 * supported positions.
97 *
98 * @return void
99 */
100 static changePosition(position) {
101 if (position) {
102 $('#vbo-toast-wrapper').attr('class', 'vbo-toast-wrapper ' + position);
103 }
104 }
105
106 /**
107 * Immediately displays the message.
108 * In case the toast was already visible when calling
109 * this method, it will perform a shake effect.
110 *
111 * @param mixed message The message to display or an object with the data to use:
112 * - text string The message to display;
113 * - status string The message status: 0 for error,
114 * 1 for success, 2 for warning, 3 for notice;
115 * - delay integer The time for which the toast remains open;
116 * - action function If specified, the function to invoke when
117 * clicking the toast box;
118 * - callback function If specified, the callback to invoke after
119 * displaying the toast message.
120 * - style mixed Either a string or an object of styles to be
121 * applied to the toast message content box.
122 * - sound string The source path of the audio file to play.
123 *
124 * @return void
125 */
126 static dispatch(message) {
127 var toast = $('#vbo-toast-wrapper');
128 var content = toast.find('.toast-message-content');
129
130 // clear any action previously set
131 toast.off('click');
132
133 // create message object in case a string was passed
134 if (typeof message === 'string') {
135 message = {
136 text: message,
137 status: 1,
138 };
139 }
140
141 // attach click event to toast message if specified
142 if (message.hasOwnProperty('action') && typeof message.action === 'function') {
143 toast.addClass('clickable').on('click', message.action);
144 } else {
145 toast.removeClass('clickable');
146 }
147
148 // perform a "shake" effect in case the toast is already visible
149 if (VBOToast.timerHandler) {
150 clearTimeout(VBOToast.timerHandler);
151
152 toast.removeClass('do-shake').delay(200).queue(function(next) {
153 $(this).addClass('do-shake');
154 next();
155 });
156 }
157
158 try {
159 // try to append specified text as HTML
160 content.html(message.text);
161 } catch (err) {
162 // an error occurred, display generic message
163 console.warn('toast.dispatch.sethtml', err);
164 content.html('Unknown error.');
165 message.status = 0;
166 }
167
168 // remove all classes that might have been previosuly set
169 content.removeClass('error');
170 content.removeClass('success');
171 content.removeClass('warning');
172 content.removeClass('notice');
173
174 var delay = 0;
175
176 // fetch status class and related delay
177 switch (message.status) {
178 case VBOToast.ERROR_STATUS:
179 content.addClass('error');
180 delay = 4500;
181 break;
182
183 case VBOToast.SUCCESS_STATUS:
184 content.addClass('success');
185 delay = 2500;
186 break;
187
188 case VBOToast.WARNING_STATUS:
189 content.addClass('warning');
190 delay = 3500;
191 break;
192
193 case VBOToast.NOTICE_STATUS:
194 content.addClass('notice');
195 delay = 3500;
196 break;
197 }
198
199 // fetch message content style
200 var style = '';
201
202 if (message.hasOwnProperty('style')) {
203 // check if we received an object
204 if (typeof message.style === 'object') {
205 // iterate style properties
206 style = [];
207
208 for (var k in message.style) {
209 if (message.style.hasOwnProperty(k)) {
210 // append rule to string
211 style.push(k + ':' + message.style[k] + ';');
212 }
213 }
214
215 // implode the style array
216 style = style.join(' ');
217 }
218 // otherwise cast to string what we received
219 else {
220 style = message.style.toString();
221 }
222 }
223
224 content.attr('style', style);
225
226 // overwrite delay in case it was specified
227 if (message.hasOwnProperty('delay')) {
228 let ms = parseInt(message.delay);
229
230 if (isNaN(delay)) {
231 // do not auto-dispose
232 delay = 0;
233 } else {
234 // use the given delay
235 delay = Math.abs(message.delay);
236 }
237 }
238
239 // register delay
240 VBOToast.disposeDelay = delay;
241
242 if (VBOToast.disposeDelay) {
243 // register timer to dispose the toast message once the specified
244 // delay is passed
245 VBOToast.timerHandler = setTimeout(VBOToast.dispose, delay);
246 } else {
247 // flag timer handler to properly process the queue
248 VBOToast.timerHandler = true;
249
250 // dispose only after clicking the notification
251 toast.addClass('clickable').on('click', () => {
252 // force closure because we are above the toast
253 // and the mouseHover flag could be active
254 VBOToast.dispose(true);
255 });
256 }
257
258 setTimeout(() => {
259 // slide in the toast message
260 toast.addClass('toast-slide-in ready');
261 }, 32);
262
263 // execute callback, if specified
264 if (message.hasOwnProperty('callback') && typeof message.callback === 'function') {
265 message.callback(message);
266 }
267
268 if (message.sound) {
269 // auto-play the sound when the message slide in
270 VBOToastSound.play(message.sound, delay * 2);
271 }
272 }
273
274 /**
275 * Enqueues the message for being displayed once the
276 * current queue of messages is dispatched.
277 * In case the queue is empty, the message will be
278 * immediately displayed.
279 *
280 * @param mixed message The message to display or an object
281 * with the data to use.
282 *
283 * @return void
284 */
285 static enqueue(message) {
286 if (VBOToast.timerHandler == null) {
287 // dispatch directly as there is no active messages
288 VBOToast.dispatch(message);
289 return;
290 }
291
292 // push the message within the queue
293 VBOToast.queue.push(message);
294 }
295
296 /**
297 * Schedule the message to be enqueued at the specified date and time.
298 *
299 * @param mixed message The message to display or an object
300 * with the data to use.
301 *
302 * @return void
303 */
304 static schedule(message) {
305 if (message.datetime && !(message.datetime instanceof Date)) {
306 // create date instance
307 message.datetime = new Date(message.datetime);
308 }
309
310 if (!message.datetime || isNaN(message.datetime.getTime())) {
311 // invalid date time
312 throw 'ToastInvalidScheduleTime';
313 }
314
315 // calculate remaining seconds
316 let ms = message.datetime.getTime() - new Date().getTime();
317
318 if (ms <= 0) {
319 // immediately enqueue the message
320 VBOToast.enqueue(message);
321 } else {
322 setTimeout(() => {
323 // schedule the message
324 VBOToast.enqueue(message);
325 }, ms)
326 }
327 }
328
329 /**
330 * Disposes the current visible message.
331 * Once a message is closed, it will pop the
332 * first message in the queue, if any, for
333 * being immediately displayed.
334 *
335 * @param boolean force True to force the closure.
336 *
337 * @return void
338 */
339 static dispose(force) {
340 // do not dispose in case the mouse is above the toast
341 if (!VBOToast.mouseHover || force) {
342 // fade out the toast message
343 $('#vbo-toast-wrapper').removeClass('toast-slide-in').removeClass('do-shake');
344 // reset handler
345 clearTimeout(VBOToast.timerHandler);
346 VBOToast.timerHandler = null;
347 VBOToast.mouseHover = false;
348
349 // check if the queue is not empty
350 if (VBOToast.queue.length) {
351 // wait some time before displaying the new message
352 VBOToast.timerHandler = setTimeout(() => {
353 // get first message added
354 let message = VBOToast.queue.shift();
355
356 // unset timer to avoid adding shake effect
357 VBOToast.timerHandler = null;
358
359 // dispatch the message
360 VBOToast.dispatch(message);
361 }, 1000);
362 }
363 }
364 }
365 }
366
367 /**
368 * Environment variables.
369 */
370 VBOToast.timerHandler = null;
371 VBOToast.mouseHover = false;
372 VBOToast.disposeDelay = 0;
373 VBOToast.queue = [];
374
375 /**
376 * Toast positions constants.
377 */
378 VBOToast.POSITION_TOP_LEFT = 'top-left';
379 VBOToast.POSITION_TOP_CENTER = 'top-center';
380 VBOToast.POSITION_TOP_RIGHT = 'top-right';
381 VBOToast.POSITION_CENTER_CENTER = 'center-center';
382 VBOToast.POSITION_BOTTOM_LEFT = 'bottom-left';
383 VBOToast.POSITION_BOTTOM_CENTER = 'bottom-center';
384 VBOToast.POSITION_BOTTOM_RIGHT = 'bottom-right';
385
386 /**
387 * Toast status constants.
388 */
389 VBOToast.ERROR_STATUS = 0;
390 VBOToast.SUCCESS_STATUS = 1;
391 VBOToast.WARNING_STATUS = 2;
392 VBOToast.NOTICE_STATUS = 3;
393
394 /**
395 * Toast message decorator.
396 *
397 * In conjunction with the arguments supported by VBOToast.dispatch(), here's
398 * a list of properties that can be used to create a message template:
399 *
400 * - icon string|Image|null An optional icon to display on the left side.
401 * In case of a string, the system will auto-detect if
402 * we are dealing with an image path or with a font icon.
403 * - title string|null An optional plain title for the message.
404 * - body string|null An optional HTML body text for the message.
405 *
406 * How to create a new message decorator:
407 *
408 * new VBOToastMessage({
409 * title: 'Message title',
410 * body: 'This is the body of the message',
411 * icon: 'fas fa-bell',
412 * // icon: '/path/to/image.png',
413 * status: VBOToastMessage.NOTICE_STATUS, // (default)
414 * delay: 3500, // (default)
415 * sound: '/path/to/audio.mp3',
416 * action: () => { console.log('notification clicked'); },
417 * callback: (message) => { console.log('message displayed', message); },
418 * style: { padding: '10px' },
419 * });
420 */
421 w['VBOToastMessage'] = class VBOToastMessage {
422 /**
423 * Class constructor.
424 *
425 * @param object data The message data.
426 */
427 constructor(data) {
428 if (typeof data !== 'object') {
429 throw 'ToastMessageInvalidArgument';
430 }
431
432 // assign the specified properties to this class
433 Object.assign(this, data);
434
435 // text not specified, construct it
436 if (!this.text) {
437 // create message wrapper
438 this.text = $('<div class="vbo-pushnotif-wrapper"></div>');
439
440 if (this.icon) {
441 let icon;
442
443 if (this.icon instanceof Image) {
444 // we have an image instance
445 icon = $(this.icon);
446 } else if (typeof this.icon === 'string') {
447 if (this.icon.indexOf('/') !== -1) {
448 // we have an image URL
449 icon = $('<img>').attr('src', this.icon);
450 } else {
451 // we probably have a font icon
452 icon = $('<i></i>').addClass(this.icon);
453 }
454 }
455
456 if (icon) {
457 // append icon to message wrapper
458 this.text.append($('<div class="push-notif-icon"></div>').append(icon));
459 }
460 }
461
462 // create message inner box
463 let inner = $('<div class="push-notif-text"></div>');
464
465 if (this.title) {
466 // append message title to message wrapper
467 inner.append($('<div class="push-notif-title"></div>').text(this.title));
468 }
469
470 if (this.body) {
471 // append message body to message wrapper
472 inner.append($('<div class="push-notif-body"></div>').html(this.body));
473 }
474
475 // append inner message
476 this.text.append(inner);
477 }
478
479 if (this.status === undefined) {
480 // use default notice status
481 this.status = VBOToast.NOTICE_STATUS;
482 }
483
484 if (this.delay === 'auto') {
485 this.delay = {
486 // use by default a tolerance of 2.5 seconds
487 tolerance: 2500,
488 };
489 }
490
491 if (typeof this.delay === 'object') {
492 this.delay = this.fetchReadingTime(this.delay);
493 }
494 }
495
496 /**
497 * Calculates the estimated reading time based on the specified title, body and configuration options.
498 *
499 * @param object opts A registry of options.
500 * - tolerance int The milliseconds to add to the estimated time.
501 * - min int The reading time cannot be lower than this amount.
502 * - max int The reading time cannot be higher than this amount.
503 * - debug bool True to display some information within the console.
504 *
505 * @return int The reading time in milliseconds.
506 */
507 fetchReadingTime(opts) {
508 // build readable message
509 let text = [
510 this.title,
511 this.body,
512 ].filter(str => str).join(' ');
513
514 // split the words delimited by the punctuation
515 let words = text.match(/[\s,.;:-]+.(?!$)/g);
516
517 // count total number of words
518 let wordsCount = words ? words.length + 1 : 0;
519
520 // divide the words count by 200, a good compromise related to the
521 // average reading rate (238 words per minute)
522 let division = wordsCount / 200;
523
524 // multiply the result by 60 to convert the resulting minutes in seconds
525 let seconds = Math.floor(division) * 60;
526
527 // take the decimal points and multiply that number by 0.60 to
528 // obtain the remaining seconds
529 seconds += (division % 1) * 0.6 * 100;
530
531 if (opts.tolerance) {
532 // convert milliseconds in seconds
533 seconds += opts.tolerance / 1000;
534 }
535
536 // convert in milliseconds and get rid of decimals
537 let ms = Math.round(seconds * 1000);
538
539 if (opts.min) {
540 // cannot be lower than the specified amount
541 ms = Math.max(opts.min, ms);
542 }
543
544 if (opts.max) {
545 // cannot be higher than the specified amount
546 ms = Math.min(opts.max, ms);
547 }
548
549 if (opts.debug) {
550 // display debug info
551 console.log('words count: ' + wordsCount);
552 console.log('estimated reading time (seconds): ', seconds);
553 console.log('fetched delay (milliseconds): ', ms);
554 }
555
556 return ms;
557 }
558 }
559
560 /**
561 * Helper class used to play sounds.
562 */
563 w['VBOToastSound'] = class VBOToastSound {
564 /**
565 * Tries to play a sound.
566 * In case of success, it will be played only once within the specified milliseconds.
567 *
568 * @param string src The path of the audio to play.
569 * @param integer threshold The milliseconds in which the audio cannot be played again,
570 * since the last time is was played.
571 *
572 * @return mixed The audio element on success, null otherwise.
573 */
574 static play(src, threshold) {
575 let play = true;
576
577 if (threshold) {
578 // create pool of played sounds if undefined
579 if (typeof VBOToastSound.pool === 'undefined') {
580 VBOToastSound.pool = {};
581 }
582
583 // check if the audio is still in the pool
584 if (VBOToastSound.pool.hasOwnProperty(src)) {
585 // audio already played, don't play it again
586 play = false;
587
588 // reset current timer
589 clearTimeout(VBOToastSound.pool[src]);
590 }
591
592 // mark sound as played
593 VBOToastSound.pool[src] = setTimeout(function() {
594 // auto-delete sound from pool on time expiration
595 delete VBOToastSound.pool[src];
596 }, Math.abs(threshold));
597 }
598
599 if (play) {
600 // create audio element and auto-play
601 return new Audio(src).play();
602 }
603
604 return null;
605 }
606 }
607 })(jQuery, window);