templates/FrontCommun/javascript-base.html.twig line 1

Open in your IDE?
  1. <script src="{{ asset("assets-commun/js/vue-#{app.environment}.min.js") }}"></script>
  2. {% if app.session.get('ROLE_SUPPER_ADMIN') %}
  3.     {# Panneau de visualisation des erreurs Vue.js #}
  4.     {% include 'FrontCommun/error-logger-panel.html.twig' %}
  5.     {# Système de journalisation des erreurs Vue.js - À charger EN DERNIER #}
  6.     <script src="{{ asset('assets-commun/js/error-logger/error-logger.js') }}"></script>
  7.     <script src="{{ asset('assets-commun/js/error-logger/error-handler-init.js') }}"></script>
  8. {% endif %}
  9. {#<script type="module">
  10.     import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';
  11. </script>#}
  12. <style>
  13.     /* Cœur rempli */
  14.     .heart-button .bi-heart-fill {
  15.         color: red;
  16.     }
  17.     /* Indicateur de chargement (spinner) */
  18.     .heart-button .loading-spinner {
  19.         display: none; /* Caché par défaut */
  20.         position: absolute;
  21.         top: 50%;
  22.         left: 50%;
  23.         transform: translate(-50%, -50%);
  24.         width: 20px;
  25.         height: 20px;
  26.         border: 2px solid #ccc;
  27.         border-top: 2px solid red;
  28.         border-radius: 50%;
  29.         animation: spin 1s linear infinite;
  30.     }
  31.     @keyframes spin {
  32.         0% {
  33.             transform: translate(-50%, -50%) rotate(0deg);
  34.         }
  35.         100% {
  36.             transform: translate(-50%, -50%) rotate(360deg);
  37.         }
  38.     }
  39. </style>
  40. <link rel="stylesheet" type="text/css" href="{{ asset('assets-commun/css/daterangepicker.css') }}"/>
  41. <script type="text/javascript" src="{{ asset('assets-commun/js/daterangepicker.min.js') }}"></script>
  42. <script type="text/javascript" src="{{ asset("assets-commun/js/jquery.number.js") }}"></script>
  43. <script type="text/javascript" src="{{ asset("assets-commun/js/autoNumeric.min.js") }}"></script>
  44. <link rel="stylesheet" href="{{ asset('assets-commun/css/jquery-confirm.min.css') }}">
  45. <script src="{{ asset('assets-commun/js/jquery-confirm.min.js') }}"></script>
  46. <script>
  47.     window.enabledHotelStayWorldwide = {{ app.request.server.get('ENABLED_HOTEL_STAY_WORLDWIDE') }};
  48.     if (typeof Vue != 'undefined')
  49.         Vue.options.delimiters = ['${', '}'];
  50.         {% if app.environment == 'prod' %}
  51.             Vue.config.devtools = false;
  52.             Vue.config.productionTip = false;
  53.         {% endif %}
  54.     /*Vue.config.errorHandler = (err, vm, info) => {
  55.         // err: error trace
  56.         // vm: component in which error occured
  57.         // info: Vue specific error information such as lifecycle hooks, events etc.
  58.         // TODO: Perform any custom logic or log to server
  59.     };*/
  60.     function flip(o) {
  61.         var newObj = {}
  62.         Object.keys(o).forEach((el, i) => {
  63.             newObj[o[el]] = el;
  64.         });
  65.         return newObj;
  66.     }
  67.     function autoNumeric(selector, options, version = 1) {
  68.         if ($(`${selector}:not(.formatted)`).length == 0)
  69.             return;
  70.         if (version == 1)
  71.             $(`${selector}:not(.formatted)`).number(true, options.decimalPlaces, options.decimalCharacter, options.digitGroupSeparator).addClass('formatted');
  72.         else if (version == 2)
  73.             document.querySelectorAll(`${selector}:not(.formatted)`).forEach(el => {
  74.                 let _options = Object.assign({}, options);
  75.                 if (el.dataset.currencySymbol !== undefined) {
  76.                     _options.currencySymbol = el.dataset.currencySymbol
  77.                     _options.currencySymbolPlacement = AutoNumeric.options.currencySymbolPlacement.suffix
  78.                 }
  79.                 if (el.dataset.unformatOnSubmit !== undefined) _options.unformatOnSubmit = (el.dataset.unformatOnSubmit == 'true' ? true : false)
  80.                 if (el.dataset.digitGroupSeparator !== undefined) _options.digitGroupSeparator = el.dataset.digitGroupSeparator
  81.                 if (el.dataset.decimalCharacter !== undefined) _options.decimalCharacter = el.dataset.decimalCharacter
  82.                 if (el.dataset.decimalPlaces !== undefined) _options.decimalPlaces = el.dataset.decimalPlaces
  83.                 if (el.dataset.minimumValue !== undefined) _options.minimumValue = el.dataset.minimumValue
  84.                 if (el.dataset.maximumValue !== undefined) _options.maximumValue = el.dataset.maximumValue
  85.                 new AutoNumeric(el, _options)
  86.                 el.classList.add("formatted")
  87.             })
  88.     }
  89.     defaultOptionsMoney = {
  90.         digitGroupSeparator: '',
  91.         decimalCharacter: '.',
  92.         decimalPlaces:{{ deviseAgence().scale }},
  93.         unformatOnSubmit: true
  94.     }
  95.     defaultOptionsPercent = {
  96.         digitGroupSeparator: '',
  97.         decimalCharacter: '.',
  98.         decimalPlaces: 2
  99.     }
  100.     $(document).ready(function () {
  101.         autoNumeric('input.money', defaultOptionsMoney);
  102.         autoNumeric('input.percent', defaultOptionsPercent);
  103.         autoNumeric('input.money-v2', defaultOptionsMoney, 2);
  104.         autoNumeric('input.percent-v2', defaultOptionsPercent, 2);
  105.         $('input.money').attr('maxlength', 11);
  106.         $('input.percent').attr('maxlength', 6);
  107.         // Récupérer la liste de souhaits depuis la session Symfony
  108.         window.wishlist = {{ app.session.get('wishlist', []) | json_encode | raw }};
  109.         // Attacher l'événement à un parent existant
  110.         if (document.getElementById('vjs-list-hotels') != null) {
  111.             document.getElementById('vjs-list-hotels').addEventListener('click', function (event) {
  112.                 // Vérifier si l'élément cliqué est un bouton "Like"
  113.                 if (event.target.closest('.heart-button')) {
  114.                     const heartButton = event.target.closest('.heart-button');
  115.                     const hotelId = parseInt(heartButton.getAttribute('data-hotel-id'));
  116.                     const heartIcon = heartButton.querySelector('.bi');
  117.                     const spinner = heartButton.querySelector('.loading-spinner');
  118.                     // Afficher le spinner et cacher l'icône de cœur
  119.                     heartIcon.style.display = 'none';
  120.                     spinner.style.display = 'block';
  121.                     // Envoyer une requête AJAX pour ajouter/retirer de la liste de souhaits
  122.                     fetch(`{{ path('wishlist_toggle',{hotelId:'_hotelId_'}) }}`.replace('_hotelId_', hotelId), {
  123.                         method: 'POST',
  124.                         headers: {
  125.                             'Content-Type': 'application/json',
  126.                             'X-Requested-With': 'XMLHttpRequest',
  127.                         },
  128.                     })
  129.                         .then(response => {
  130.                             if (!response.ok) {
  131.                                 throw new Error('Erreur lors de la requête');
  132.                             }
  133.                             return response.json();
  134.                         })
  135.                         .then(data => {
  136.                             if (data.success) {
  137.                                 // Mettre à jour l'icône du bouton
  138.                                 if (data.isInWishlist) {
  139.                                     heartIcon.classList.remove('bi-heart');
  140.                                     heartIcon.classList.add('bi-heart-fill');
  141.                                     // Ajouter l'hôtel à la liste de souhaits
  142.                                     wishlist.push(hotelId);
  143.                                 } else {
  144.                                     heartIcon.classList.remove('bi-heart-fill');
  145.                                     heartIcon.classList.add('bi-heart');
  146.                                     // Retirer l'hôtel de la liste de souhaits
  147.                                     wishlist.splice(wishlist.indexOf(hotelId), 1);
  148.                                 }
  149.                             } else {
  150.                                 throw new Error(data.message || 'Erreur inconnue');
  151.                             }
  152.                         })
  153.                         .catch(error => {
  154.                             console.error('Erreur:', error);
  155.                             // Afficher un message d'erreur à l'utilisateur
  156.                             alert('Une erreur est survenue : ' + error.message);
  157.                         })
  158.                         .finally(() => {
  159.                             // Cacher le spinner et réafficher l'icône de cœur
  160.                             spinner.style.display = 'none';
  161.                             heartIcon.style.display = 'block';
  162.                         });
  163.                 }
  164.             });
  165.         }
  166.     });
  167.     //================== Text Crope ==========================================
  168.     $.fn.extend({
  169.         toggleHtml: function (a, b) {
  170.             return this.html(this.html() == b ? a : b);
  171.         }
  172.     });
  173.     function checkEllipsisActive(div) {
  174.         var a = `<a href="javascript:void(0)" class="lire-la-suite" onclick="$(this).toggleHtml('Suite','Moins'); $(this).prev().toggleClass('text-crope')"><i>Suite</i></a>`
  175.         setTimeout(function () {
  176.             $(`${div} .text-crope`).each(function (i) {
  177.                 if ($(this).innerHeight() < $(this)[0].scrollHeight && $(this).parent().find('.lire-la-suite').length == 0)
  178.                     $(a).insertAfter($(this));
  179.             });
  180.             $(`${div} .text-crope`).each(function (i) {
  181.                 if ($(this).innerWidth() < $(this)[0].scrollWidth && $(this).parent().find('.lire-la-suite').length == 0)
  182.                     $(this).attr('title', $(this).html())
  183.             });
  184.         }, 500)
  185.     }
  186.     //checkEllipsisActive()
  187.     //================================================================================
  188.     {% if app.request.get('mdl') %}
  189.     $('#modal-login-register').modal()
  190.     $('#modal-login-register input.{{ app.request.get('mdl') }}').click()
  191.     {% endif %}
  192.     const slugify = str =>
  193.         str
  194.             .toLowerCase()
  195.             .trim()
  196.             .replace(/[^\w\s-]/g, '')
  197.             .replace(/[\s_-]+/g, '-')
  198.             .replace(/^-+|-+$/g, '');
  199.     function viewAlert(msg, tag, autoClose, fntAction, titre, _options) {
  200.         var color = 'dark';
  201.         var icon = '';
  202.         var title = '';
  203.         var animation = 'zoom';
  204.         var closeAnimation = 'scale';
  205.         if (tag == 'success') {
  206.             color = 'green';
  207.             icon = 'icon-checkmark4';
  208.             title = 'Succès';
  209.             animation = 'scale';
  210.             closeAnimation = 'scale';
  211.         } else if (tag == 'danger') {
  212.             color = 'red';
  213.             icon = 'icon-blocked';
  214.             title = 'Alert!';
  215.             animation = 'rotateY';
  216.             closeAnimation = 'rotateY';
  217.         } else if (tag == 'warning') {
  218.             color = 'orange';
  219.             icon = 'icon-warning';
  220.             title = 'Attention';
  221.             animation = 'rotateYR';
  222.             closeAnimation = 'rotateYR';
  223.         } else if (tag == 'info') {
  224.             color = 'blue';
  225.             icon = 'icon-info3';
  226.             title = 'Info';
  227.             animation = 'rotateX';
  228.             closeAnimation = 'rotateX';
  229.         } else if (tag == 'primary') {
  230.             color = 'purple';
  231.         }
  232.         if (titre !== undefined && titre != null)
  233.             title = titre;
  234.         var options = {
  235.             icon: icon,
  236.             closeIcon: true,
  237.             type: color,
  238.             title: title,
  239.             content: msg,
  240.             animationBounce: 2.5,
  241.             closeAnimation: closeAnimation,
  242.             animation: animation,
  243.             buttons: {
  244.                 btnOk: {
  245.                     text: 'OK',
  246.                     btnClass: 'btn-default',
  247.                     keys: ['enter', 'echap'],
  248.                 }
  249.             }
  250.         };
  251.         if (autoClose !== undefined && autoClose != null)
  252.             options.autoClose = 'btnOk|' + autoClose;
  253.         if (fntAction !== undefined && fntAction != null)
  254.             options.buttons.btnOk.action = fntAction;
  255.         if (_options !== undefined && _options != null)
  256.             $.each(_options, function (index, value) {
  257.                 options[value.key] = value.val;
  258.             });
  259.         $.alert(options);
  260.     }
  261.     function demandConfirm(title, content, txtConfirm, fntAction) {
  262.         $.confirm({
  263.             title: title,
  264.             content: content,
  265.             icon: 'icon-question3',
  266.             closeIcon: true,
  267.             type: 'blue',
  268.             buttons: {
  269.                 confirm: {
  270.                     text: txtConfirm,
  271.                     btnClass: 'btn-danger',
  272.                     keys: ['enter'],
  273.                     action: fntAction
  274.                 },
  275.                 cancel: {
  276.                     text: 'Non',
  277.                     btnClass: 'btn-default',
  278.                     keys: ['echap'],
  279.                 },
  280.             }
  281.         });
  282.     }
  283.     //================== Forcer le submit du formulaire une seul fois ==============================================
  284.     $(document).on('submit', 'form', function () {
  285.         form = $(this);
  286.         form.addClass('form-submitted');
  287.         setTimeout(function () {
  288.             form.removeClass('form-submitted');
  289.         }, 500);
  290.     })
  291.     $(window).on('beforeunload', function () {
  292.         $('.form-submitted').each(function () {
  293.             $(this).find('input').blur();
  294.             $(this).css('pointer-events', 'none');
  295.             btn = $(this).find('[type=submit]')
  296.             btn.prop('disabled', true)
  297.             btn.html('<i class="fa fa-spinner fa-spin"></i> <span class="text-loading">Veuillez patienter </span>')
  298.         })
  299.     });
  300.     //==============================================================================================================
  301.     function utf8_to_b64(str) {
  302.         return window.btoa(unescape(encodeURIComponent(str)));
  303.     }
  304.     function b64_to_utf8(str) {
  305.         return decodeURIComponent(escape(window.atob(str)));
  306.     }
  307.     Date.prototype.addDays = function (days) {
  308.         var date = new Date(this.valueOf());
  309.         date.setDate(date.getDate() + days);
  310.         return date;
  311.     }
  312.     String.prototype.removeSizeStyles = function () {
  313.         // Supprime tous les attributs class
  314.         let result = this.replace(/\s*class\s*=\s*["'][^"']*["']/gi, '');
  315.         // Supprime les propriétés de taille dans les attributs style
  316.         return result.replace(
  317.             /style\s*=\s*["'][^"']*?(width|height|min-width|min-height|max-width|max-height)\s*:\s*[^;"']+;?\s*[^"']*?["']/gi,
  318.             (match, p1) => {
  319.                 // Supprime les propriétés de taille tout en préservant les autres styles
  320.                 return match.replace(
  321.                     /(width|height|min-width|min-height|max-width|max-height)\s*:\s*[^;"']+;?\s*/gi,
  322.                     ''
  323.                 ).replace(/\s*style\s*=\s*["']\s*["']/i, ''); // Supprime style vide
  324.             }
  325.         );
  326.     };
  327.     Number.prototype.formatMoney = function (decimals = 3, decimal_separator = ".", thousands_separator = " ", round = false, devise = true) {
  328.         {% set coursEchange = getCoursEchange() %}
  329.         let montant = {{ coursEchange.montant }}
  330.         if (!devise)
  331.             montant = 1
  332.         decimals ={{ coursEchange.scale }}
  333.         if (round)
  334.             decimals = 0
  335.         var n = this {{ coursEchange.operation }} montant,
  336.             decimals = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals,
  337.             decimal_separator = decimal_separator == undefined ? "." : decimal_separator,
  338.             thousands_separator = thousands_separator == undefined ? " " : thousands_separator,
  339.             s = n < 0 ? "-" : "",
  340.             i = String(parseInt(n = Math.abs(Number(n) || 0).toFixed(decimals))),
  341.             j = (j = i.length) > 3 ? j % 3 : 0;
  342.         return s + (j ? i.substr(0, j) + thousands_separator : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands_separator) + (decimals ? decimal_separator + Math.abs(n - i).toFixed(decimals).slice(2) : "");
  343.     };
  344.     $("form#newsletter").submit(function (event) {
  345.         event.preventDefault();
  346.         $('i#load-newsletter').toggleClass('hidden')
  347.         $.ajax({
  348.             type: "POST",
  349.             url: "{{ path('newsletter') }}",
  350.             data: $(this).serialize(),
  351.         }).done(function (data) {
  352.             $('i#load-newsletter').toggleClass('hidden')
  353.             $("form#newsletter").get(0).reset()
  354.             viewAlert('Merci beaucoup ! Nous vous enverrons chaque mois les meilleures offres ', 'success')
  355.         });
  356.     });
  357.     {% for type,flash in app.session.flashbag.all %}
  358.     {% for msg in flash %}
  359.     viewAlert(`{{ msg|raw }}`, '{{ type }}');
  360.     {% endfor %}
  361.     {% endfor %}
  362.     {% for flashError in app.flashes('verify_email_error') %}
  363.     viewAlert("{{ flashError }}", 'danger');
  364.     {% endfor %}
  365.     $('.logo').on({
  366.         mousedown: function () {
  367.             $(this).data('timer', setTimeout(function () {
  368.                 window.location.href = '{{ path('redirect_dev_route') }}'
  369.             }, 3000));
  370.         },
  371.         mouseup: function () {
  372.             clearTimeout($(this).data('timer'));
  373.         }
  374.     });
  375.     function validatePassword() {
  376.         if (plain_password.value != confirm_password.value) {
  377.             confirm_password.setCustomValidity("Les mots de passe ne correspondent pas");
  378.             confirm_password.setAttribute('class', 'not_confirm_password form-control')
  379.         } else {
  380.             confirm_password.setCustomValidity('');
  381.             confirm_password.setAttribute('class', 'confirm_password form-control')
  382.         }
  383.     }
  384.     if ($('#plainPassword').length > 0 && $('#confirm_password').length > 0) {
  385.         var plain_password = document.getElementById("plainPassword")
  386.             , confirm_password = document.getElementById("confirm_password");
  387.         plain_password.onchange = validatePassword;
  388.         confirm_password.onkeyup = validatePassword;
  389.     }
  390.     const scrollSmoothToBottom = (id) => {
  391.         const element = $(`div#${id}`);
  392.         element.animate({
  393.             scrollTop: element.prop("scrollHeight")
  394.         }, 1000);
  395.     }
  396.     const scrollToBottom = (id) => {
  397.         const element = document.getElementById(id);
  398.         element.scrollTop = element.scrollHeight;
  399.     }
  400.     /**
  401.      * Fonction externe pour afficher la pile d'appels de manière simplifiée
  402.      * @param {string} functionName - Nom de la fonction d'où l'appel provient
  403.      * @param {string} customMessage - Message personnalisé optionnel
  404.      */
  405.     function logStackTrace(functionName = 'Unknown', customMessage = '') {
  406.         // Récupérer la pile d'appels
  407.         const stack = new Error().stack.split('\n');
  408.         // Filtrer et simplifier les appels (ignorer les appels Vue internes, webpack, etc.)
  409.         const relevantCalls = stack
  410.             .slice(2) // Ignorer Error et logStackTrace
  411.             .filter(line => {
  412.                 const ignored = ['vue-dev', 'webpack', 'installHook', 'evaluate', 'overrideMethod',
  413.                     'computedGetter', 'renderList', 'eval', 'Vue._render', 'vm9708'];
  414.                 return !ignored.some(term => line.includes(term));
  415.             })
  416.             .slice(0, 8) // Limiter à 8 appels pertinents
  417.             .map(line => {
  418.                 // Extraire et nettoyer les informations
  419.                 const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
  420.                 if (match) {
  421.                     const [, func, file, line, col] = match;
  422.                     const fileName = file.split('/').pop();
  423.                     return { func: func.trim(), file: fileName, line, col };
  424.                 }
  425.                 return null;
  426.             })
  427.             .filter(Boolean);
  428.         // Afficher le schéma simplifié
  429.         console.group(`%c🔍 Call Stack - ${functionName}`, 'color: #ff6b35; font-weight: bold; font-size: 13px;');
  430.         if (customMessage) {
  431.             console.log(`%c💬 ${customMessage}`, 'color: #004e89; font-style: italic; font-size: 11px;');
  432.         }
  433.         console.log('%c' + getStackDiagram(relevantCalls), 'font-family: monospace; color: #333; font-size: 11px; white-space: pre;');
  434.         // Afficher la pile complète dans un groupe collapsé
  435.         console.groupCollapsed('%c📋 Full Stack Trace', 'color: #666; font-size: 11px;');
  436.         console.trace(`Full trace from ${functionName}`);
  437.         console.groupEnd();
  438.         console.groupEnd();
  439.     }
  440.     /**
  441.      * Génère un schéma visuel de la pile d'appels
  442.      */
  443.     function getStackDiagram(calls) {
  444.         let diagram = '\n';
  445.         calls.forEach((call, index) => {
  446.             const isLast = index === calls.length - 1;
  447.             const prefix = isLast ? '└─' : '├─';
  448.             const connector = isLast ? '  ' : '│ ';
  449.             diagram += `${prefix} ${call.func} (${call.file}:${call.line})\n`;
  450.             if (index < calls.length - 1) {
  451.                 diagram += `${connector}\n`;
  452.             }
  453.         });
  454.         return diagram;
  455.     }
  456. </script>