PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
1.8.16 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 All 37 releases
vikbooking / admin / resources / toast.js

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

584 lines 16.2 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) {
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 // slide in the toast message
227 toast.addClass('toast-slide-in');
228
229 // overwrite delay in case it was specified
230 if (message.hasOwnProperty('delay')) {
231 delay = message.delay;
232 }
233
234 // execute callback, if specified
235 if (message.hasOwnProperty('callback') && typeof message.callback === 'function') {
236 message.callback(message);
237 }
238
239 if (message.sound) {
240 // auto-play the sound when the message slide in
241 VBOToastSound.play(message.sound, delay * 2);
242 }
243
244 // register delay
245 VBOToast.disposeDelay = delay;
246
247 // register timer to dispose the toast message once the specified
248 // delay is passed
249 VBOToast.timerHandler = setTimeout(VBOToast.dispose, delay);
250 }
251
252 /**
253 * Enqueues the message for being displayed once the
254 * current queue of messages is dispatched.
255 * In case the queue is empty, the message will be
256 * immediately displayed.
257 *
258 * @param mixed message The message to display or an object
259 * with the data to use.
260 *
261 * @return void
262 */
263 static enqueue(message) {
264 if (VBOToast.timerHandler == null) {
265 // dispatch directly as there is no active messages
266 VBOToast.dispatch(message);
267 return;
268 }
269
270 // push the message within the queue
271 VBOToast.queue.push(message);
272 }
273
274 /**
275 * Schedule the message to be enqueued at the specified date and time.
276 *
277 * @param mixed message The message to display or an object
278 * with the data to use.
279 *
280 * @return void
281 */
282 static schedule(message) {
283 if (message.datetime && !(message.datetime instanceof Date)) {
284 // create date instance
285 message.datetime = new Date(message.datetime);
286 }
287
288 if (!message.datetime || isNaN(message.datetime.getTime())) {
289 // invalid date time
290 throw 'ToastInvalidScheduleTime';
291 }
292
293 // calculate remaining seconds
294 let ms = message.datetime.getTime() - new Date().getTime();
295
296 if (ms <= 0) {
297 // immediately enqueue the message
298 VBOToast.enqueue(message);
299 } else {
300 setTimeout(() => {
301 // schedule the message
302 VBOToast.enqueue(message);
303 }, ms)
304 }
305 }
306
307 /**
308 * Disposes the current visible message.
309 * Once a message is closed, it will pop the
310 * first message in the queue, if any, for
311 * being immediately displayed.
312 *
313 * @param boolean force True to force the closure.
314 *
315 * @return void
316 */
317 static dispose(force) {
318 // do not dispose in case the mouse is above the toast
319 if (!VBOToast.mouseHover || force) {
320 // fade out the toast message
321 $('#vbo-toast-wrapper').removeClass('toast-slide-in').removeClass('do-shake');
322 // reset handler
323 clearTimeout(VBOToast.timerHandler);
324 VBOToast.timerHandler = null;
325 VBOToast.mouseHover = false;
326
327 // check if the queue is not empty
328 if (VBOToast.queue.length) {
329 // wait some time before displaying the new message
330 VBOToast.timerHandler = setTimeout(() => {
331 // get first message added
332 let message = VBOToast.queue.shift();
333
334 // unset timer to avoid adding shake effect
335 VBOToast.timerHandler = null;
336
337 // dispatch the message
338 VBOToast.dispatch(message);
339 }, 1000);
340 }
341 }
342 }
343 }
344
345 /**
346 * Environment variables.
347 */
348 VBOToast.timerHandler = null;
349 VBOToast.mouseHover = false;
350 VBOToast.disposeDelay = 0;
351 VBOToast.queue = [];
352
353 /**
354 * Toast positions constants.
355 */
356 VBOToast.POSITION_TOP_LEFT = 'top-left';
357 VBOToast.POSITION_TOP_CENTER = 'top-center';
358 VBOToast.POSITION_TOP_RIGHT = 'top-right';
359 VBOToast.POSITION_BOTTOM_LEFT = 'bottom-left';
360 VBOToast.POSITION_BOTTOM_CENTER = 'bottom-center';
361 VBOToast.POSITION_BOTTOM_RIGHT = 'bottom-right';
362
363 /**
364 * Toast status constants.
365 */
366 VBOToast.ERROR_STATUS = 0;
367 VBOToast.SUCCESS_STATUS = 1;
368 VBOToast.WARNING_STATUS = 2;
369 VBOToast.NOTICE_STATUS = 3;
370
371 /**
372 * Toast message decorator.
373 *
374 * In conjunction with the arguments supported by VBOToast.dispatch(), here's
375 * a list of properties that can be used to create a message template:
376 *
377 * - icon string|Image|null An optional icon to display on the left side.
378 * In case of a string, the system will auto-detect if
379 * we are dealing with an image path or with a font icon.
380 * - title string|null An optional plain title for the message.
381 * - body string|null An optional HTML body text for the message.
382 *
383 * How to create a new message decorator:
384 *
385 * new VBOToastMessage({
386 * title: 'Message title',
387 * body: 'This is the body of the message',
388 * icon: 'fas fa-bell',
389 * // icon: '/path/to/image.png',
390 * status: VBOToastMessage.NOTICE_STATUS, // (default)
391 * delay: 3500, // (default)
392 * sound: '/path/to/audio.mp3',
393 * action: () => { console.log('notification clicked'); },
394 * callback: (message) => { console.log('message displayed', message); },
395 * style: { padding: '10px' },
396 * });
397 */
398 w['VBOToastMessage'] = class VBOToastMessage {
399 /**
400 * Class constructor.
401 *
402 * @param object data The message data.
403 */
404 constructor(data) {
405 if (typeof data !== 'object') {
406 throw 'ToastMessageInvalidArgument';
407 }
408
409 // assign the specified properties to this class
410 Object.assign(this, data);
411
412 // text not specified, construct it
413 if (!this.text) {
414 // create message wrapper
415 this.text = $('<div class="vbo-pushnotif-wrapper"></div>');
416
417 if (this.icon) {
418 let icon;
419
420 if (this.icon instanceof Image) {
421 // we have an image instance
422 icon = $(this.icon);
423 } else if (typeof this.icon === 'string') {
424 if (this.icon.indexOf('/') !== -1) {
425 // we have an image URL
426 icon = $('<img>').attr('src', this.icon);
427 } else {
428 // we probably have a font icon
429 icon = $('<i></i>').addClass(this.icon);
430 }
431 }
432
433 if (icon) {
434 // append icon to message wrapper
435 this.text.append($('<div class="push-notif-icon"></div>').append(icon));
436 }
437 }
438
439 // create message inner box
440 let inner = $('<div class="push-notif-text"></div>');
441
442 if (this.title) {
443 // append message title to message wrapper
444 inner.append($('<div class="push-notif-title"></div>').text(this.title));
445 }
446
447 if (this.body) {
448 // append message body to message wrapper
449 inner.append($('<div class="push-notif-body"></div>').html(this.body));
450 }
451
452 // append inner message
453 this.text.append(inner);
454 }
455
456 if (this.status === undefined) {
457 // use default notice status
458 this.status = VBOToast.NOTICE_STATUS;
459 }
460
461 if (this.delay === 'auto') {
462 this.delay = {
463 // use by default a tolerance of 2.5 seconds
464 tolerance: 2500,
465 };
466 }
467
468 if (typeof this.delay === 'object') {
469 this.delay = this.fetchReadingTime(this.delay);
470 }
471 }
472
473 /**
474 * Calculates the estimated reading time based on the specified title, body and configuration options.
475 *
476 * @param object opts A registry of options.
477 * - tolerance int The milliseconds to add to the estimated time.
478 * - min int The reading time cannot be lower than this amount.
479 * - max int The reading time cannot be higher than this amount.
480 * - debug bool True to display some information within the console.
481 *
482 * @return int The reading time in milliseconds.
483 */
484 fetchReadingTime(opts) {
485 // build readable message
486 let text = [
487 this.title,
488 this.body,
489 ].filter(str => str).join(' ');
490
491 // split the words delimited by the punctuation
492 let words = text.match(/[\s,.;:-]+.(?!$)/g);
493
494 // count total number of words
495 let wordsCount = words ? words.length + 1 : 0;
496
497 // divide the words count by 200, a good compromise related to the
498 // average reading rate (238 words per minute)
499 let division = wordsCount / 200;
500
501 // multiply the result by 60 to convert the resulting minutes in seconds
502 let seconds = Math.floor(division) * 60;
503
504 // take the decimal points and multiply that number by 0.60 to
505 // obtain the remaining seconds
506 seconds += (division % 1) * 0.6 * 100;
507
508 if (opts.tolerance) {
509 // convert milliseconds in seconds
510 seconds += opts.tolerance / 1000;
511 }
512
513 // convert in milliseconds and get rid of decimals
514 let ms = Math.round(seconds * 1000);
515
516 if (opts.min) {
517 // cannot be lower than the specified amount
518 ms = Math.max(opts.min, ms);
519 }
520
521 if (opts.max) {
522 // cannot be higher than the specified amount
523 ms = Math.min(opts.max, ms);
524 }
525
526 if (opts.debug) {
527 // display debug info
528 console.log('words count: ' + wordsCount);
529 console.log('estimated reading time (seconds): ', seconds);
530 console.log('fetched delay (milliseconds): ', ms);
531 }
532
533 return ms;
534 }
535 }
536
537 /**
538 * Helper class used to play sounds.
539 */
540 w['VBOToastSound'] = class VBOToastSound {
541 /**
542 * Tries to play a sound.
543 * In case of success, it will be played only once within the specified milliseconds.
544 *
545 * @param string src The path of the audio to play.
546 * @param integer threshold The milliseconds in which the audio cannot be played again,
547 * since the last time is was played.
548 *
549 * @return mixed The audio element on success, null otherwise.
550 */
551 static play(src, threshold) {
552 let play = true;
553
554 if (threshold) {
555 // create pool of played sounds if undefined
556 if (typeof VBOToastSound.pool === 'undefined') {
557 VBOToastSound.pool = {};
558 }
559
560 // check if the audio is still in the pool
561 if (VBOToastSound.pool.hasOwnProperty(src)) {
562 // audio already played, don't play it again
563 play = false;
564
565 // reset current timer
566 clearTimeout(VBOToastSound.pool[src]);
567 }
568
569 // mark sound as played
570 VBOToastSound.pool[src] = setTimeout(function() {
571 // auto-delete sound from pool on time expiration
572 delete VBOToastSound.pool[src];
573 }, Math.abs(threshold));
574 }
575
576 if (play) {
577 // create audio element and auto-play
578 return new Audio(src).play();
579 }
580
581 return null;
582 }
583 }
584 })(jQuery, window);