// noinspection JSAssignmentUsedAsCondition const SIDEBAR_STATE_KEY = 'ui-sidebar-state'; class Application { constructor() { this.saveLock = false; this.formSubmitted = false; this.currentPage = null; this.widgets = {}; this.updateScreenDimensions(); this.initialize(); if (typeof console === "undefined") { console = {}; } } initialize() { document.addEventListener("DOMContentLoaded", () => { document.addEventListener('submit', this.onSubmit.bind(this)); document.addEventListener('click', this.onClick.bind(this)); document.addEventListener('keyup', this.onInputAutoExpand.bind(this)); document.addEventListener('change', this.onChange.bind(this)); // Disabled Inputs document.addEventListener('click', this.onInputOnDisabledInputs.bind(this)); document.addEventListener('beforeinput', this.onInputOnDisabledInputs.bind(this)); if (Commons.isMobile()) { document.querySelector(".side-menu.show")?.classList?.remove('show'); } document.querySelector(".dataTables_processing")?.classList?.add('alert', 'alert-warning'); document.querySelectorAll('[data-phone-field]').forEach(el => this.initPhoneInput(el)) document.querySelectorAll('[data-country-field]').forEach(el => this.populateCountryField(el)) document.querySelectorAll('[data-autocomplete]').forEach(el => this.initAutocomplete(el)) document.querySelectorAll('[data-patient-autocomplete]').forEach(el => this.initPatientAutocomplete(el)) if (screen.orientation) { screen.orientation.addEventListener('change', this.onScreenOrientationChanged.bind(this)); } else { document.addEventListener("orientationchange", this.onScreenOrientationChanged.bind(this)); } this.addWidget('Payment', new PaymentWidget()); this.addWidget('RichText', new RichTextWidget()); this.addWidget('Billing', new BillingWidget()); this.addWidget('PatientAutocomplete', new PatientAutocompleteWidget()); this.addWidget('Checkboxes', new CheckboxesWidget()); this.iOSFullHeight(); this.loadRecaptcha(document.querySelector("[data-recaptcha]")); Commons.sleep(500).then(() => { document.body.classList.remove("preload"); }); }); } iOSFullHeight() { let userAgent = window.navigator.userAgent; if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i)) { document.documentElement.style.setProperty('--vh', '0.90vh'); } } getOperationInProgress() { return this.saveLock; } /** * @param {KeyboardEvent} event */ onInputAutoExpand(event) { let element = event.target.closest('input.autoExpand') || event.target.closest('textarea.autoExpand') if (element === null) { return; } element.rows = element.value.split(/\r|\r\n|\n/).length; } /** * @param {KeyboardEvent} event * @return {boolean} */ onInputOnDisabledInputs(event) { if (event.target.closest('[data-input-disable]')) { event.stopImmediatePropagation(); event.preventDefault(); return false; } } /** * @param {SubmitEvent} event */ onSubmit(event) { let form = event.target.closest('form'); if (!form) { return; } AjaxForm.convertDate(form) if (undefined !== form.dataset.isAjax) { event.preventDefault(); return AjaxForm.post( form, form.getAttribute('action'), Commons.serializeForm(form)); } form.querySelectorAll("[data-disable-on-submit=true]").forEach(el => el.disabled = true); form.querySelectorAll("button").forEach(button => button.disabled = true); let submit = form.querySelector("button[type=submit]") if (submit) { Commons.setSpinner(submit); } this.formSubmitted = true; } /** * @param {MouseEvent} event */ onClick(event) { let target = event.target; let element; if (element = target.closest("[data-modal-submit]")) { console.log(event, element.dataset.targetId); this.handleAjaxFormModal(event, element.dataset.targetId); return; } if (element = target.closest(".clickable")) { return this.clickableElement(element); } if (target.closest('[data-recaptcha]')) { return this.onCaptchaSubmit(event); } if (target.closest('[data-sidebar-action-open]')) { this.openSidebar(); return; } if (target.closest('[data-sidebar-action-close]')) { this.closeSidebar(); return; } if (target.closest('[data-sidebar-action-toggle]')) { this.toggleSidebar(); return; } } /** * @param {HTMLElement} element * @return {Promise|boolean} */ clickableElement(element) { let url = element.dataset.url || element.dataset.postUrl; if (!url) { return false; } if ((element.dataset?.confirm ?? null) === 'true') { PageNotifier.confirmDangerousAction( element.dataset.confirmTitle, element.dataset.confirmContent, (element.dataset.confirmAction ?? null) ? ' ' + element.dataset.confirmAction : null, ).then(result => { if (result) { Commons.postBody(url); } }) return true; } if (element.dataset.postUrl) { return Commons.postBody(element.dataset.postUrl).then(result => { if (element.dataset.tableReload) { commons.getDatatables().forEach(table => table.ajax?.reload()); } return result; }); } location.href = url; } /** * @param {Event} event */ onChange(event) { let target = event.target; let element; if (element = target.closest("[data-check-tr]")) { let parent = element.closest('tr'); Commons.setClass(parent, 'checked', element.checked) } } /** * @param {boolean} refresh */ setRefreshingState(refresh) { this.saveLock = refresh; let body = document.body; if (refresh) { body.classList.add("app-loading"); } else { body.classList.remove("app-loading"); } } /** * @param {boolean} enabled */ formSubmitSpinner(enabled) { document.querySelectorAll('.submit-spinner').forEach( el => { if (enabled) { Commons.setSpinner(el); el.disabled = true; } else { Commons.unsetSpinner(el); el.disabled = false; } } ); } /** * @param {HTMLElement|null} element */ loadRecaptcha(element) { if (element) { const scriptElement = document.createElement("script"); scriptElement.type = "text/javascript"; scriptElement.async = true; scriptElement.src = 'https://www.google.com/recaptcha/api.js?render=' + element.dataset.sitekey; document.body.appendChild(scriptElement); } } refreshProgressBar() { document.querySelectorAll('progress') .forEach(el => { let max = el.max; let value = el.value; let percent = 0 === max ? 0 : value * 100.0 / max; el.title = `${value} / ${max}`; el.classList.remove('color-red', 'color-green', 'color-blue', 'd-none'); let newClass = percent < 100 ? 'color-blue' : 'color-green'; el.classList.add(percent < 20 ? 'color-red' : newClass) }) } /** * @param {MouseEvent} event * @param {string} modalId */ handleAjaxFormModal(event, modalId) { event.stopImmediatePropagation(); event.preventDefault(); let modal = document.getElementById(modalId); let form = modal.querySelector('form'); let dismiss = form.dataset.bsDismiss ?? true; if (this.getOperationInProgress()) { return; } this.setRefreshingState(true); AjaxForm.post( modal, form.getAttribute('action'), Commons.serializeForm(form), (data, enabled) => { PageNotifier.notify(data); if ("success" === data.type) { if (dismiss) { Commons.hideModal(modalId); } if (data.reload) { location.reload(); } } else { let result = modal.querySelector('[data-result-ajax]') if (result) { result.textContent = data.message; result.classList.remove('d-none'); } } this.setRefreshingState(false); }); } /** * @param {boolean} andStore */ openSidebar(andStore = true) { document.body.classList.add('opened-sidebar'); if (andStore) { window.localStorage.setItem(SIDEBAR_STATE_KEY, 'opened-sidebar'); } Commons.sleep(500).then(() => { document.dispatchEvent(new CustomEvent('menu-sidebar', {detail: {state: 'OPENED'}})); this.updateDatatablesLayout(); }) } /** * @param {boolean} andStore */ closeSidebar(andStore = true) { document.body.classList.remove('opened-sidebar'); if (andStore) { window.localStorage.setItem(SIDEBAR_STATE_KEY, ''); } Commons.sleep(500).then(() => { document.dispatchEvent(new CustomEvent('menu-sidebar', {detail: {state: 'CLOSED'}})); this.updateDatatablesLayout(); }) } restoreSidebar() { let sidebarStateStored = window.localStorage.getItem(SIDEBAR_STATE_KEY); if (sidebarStateStored !== null) { if (sidebarStateStored.length > 0) { document.body.classList.add(sidebarStateStored) } } } toggleSidebar() { if (document.body.classList.contains('opened-sidebar')) { this.closeSidebar(); } else { this.openSidebar(); } } toggleOverlay() { this.toggleDisplay(document.querySelector('.overlay')) this.toggleDisplay(document.querySelector('.please_wait')) } /** * @param {HTMLElement} element * @param {string} value */ toggleDisplay(element, value = null) { if (value) { element.style.display = value; } else if (window.getComputedStyle(element).display === "block") { element.style.display = "none"; } else { element.style.display = "block"; } } /** * @param {HTMLElement} element */ initPhoneInput(element) { window.intlTelInput(element, { initialCountry: "auto", preferredCountries: ["us", "gb"], formatOnDisplay: true, autoHideDialCode: false, nationalMode: false, utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js", geoIpLookup: (callback) => { let storageKey = "intlTelInputCountryIsoCode"; if (element.dataset.country) { console.log('Country detected on server'); callback(element.dataset.country); return; } if (window.localStorage.getItem(storageKey)) { console.log('Country detected on local storage'); callback(window.localStorage.getItem(storageKey)); return; } fetch("https://ipapi.co/json") .then(res => res.json()) .then(data => { console.log('Remotely fetched', data); window.localStorage.setItem(storageKey, data.country_code) callback(data.country_code); }) .catch(() => { callback("us"); }); } }); let mode = element.dataset.textfieldMode; let id = element.id; if ('floating' === mode) { let label = document.querySelector(`label[for=${id}]`); if (label) { element.parentElement.append(label); } } } /** * @param {HTMLElement} searchInput */ initPatientAutocomplete(searchInput) { // Récupérer les éléments du DOM const suggestionsContainer = document.getElementById(searchInput.dataset.destSuggestionId); let searchTimer; let minLength = searchInput.dataset.minLength; let delay = searchInput.dataset.delay; // Événement de saisie dans le champ de recherche searchInput.addEventListener('input', (event) => { // Effacer le délai de recherche précédent clearTimeout(searchTimer); // Définir un nouveau délai de recherche searchTimer = setTimeout(() => { const searchTerm = searchInput.value.toLowerCase(); // Effacer les suggestions précédentes suggestionsContainer.innerHTML = ''; // Vérifier si le terme de recherche est vide if (searchTerm.trim().length < minLength) { return; } this.fetchSuggestions(searchInput, searchTerm) .then(results => { // Afficher les suggestions filtrées results.forEach(result => { const item = document.createElement('div'); item.classList.add('suggestionItem'); item.textContent = result.firstname + " " + result.lastname; item.addEventListener('click', () => { searchInput.value = result.firstname + " " + result.lastname; document.getElementById(searchInput.dataset.destId).value = result.pid; document.dispatchEvent(new CustomEvent('patient-autocomplete', { detail: { target: searchInput, modal: searchInput.closest('.modal'), patient: result } })); app.toggleDisplay(suggestionsContainer, 'none'); suggestionsContainer.innerHTML = ''; }); suggestionsContainer.appendChild(item); }); if (results.length > 0) { app.toggleDisplay(suggestionsContainer, 'block'); } }) }, delay); }); } /** * @param {HTMLElement} searchInput */ initAutocomplete(searchInput) { // Récupérer les éléments du DOM const suggestionsContainer = document.getElementById('suggestionsContainer'); let searchTimer; let minLength = searchInput.dataset.minLength; let delay = searchInput.dataset.delay; // Événement de saisie dans le champ de recherche searchInput.addEventListener('input', (event) => { // Effacer le délai de recherche précédent clearTimeout(searchTimer); // Définir un nouveau délai de recherche searchTimer = setTimeout(() => { const searchTerm = searchInput.value.toLowerCase(); // Effacer les suggestions précédentes suggestionsContainer.innerHTML = ''; // Vérifier si le terme de recherche est vide if (searchTerm.trim().length < minLength) { return; } this.fetchSuggestions(searchInput, searchTerm) .then(results => { // Afficher les suggestions filtrées results.forEach(result => { const item = document.createElement('div'); item.classList.add('suggestionItem'); item.textContent = result; item.addEventListener('click', () => { searchInput.value = result; document.dispatchEvent(new CustomEvent('autocomplete', { detail: { modal: searchInput.closest('.modal'), result: result } })); app.toggleDisplay(suggestionsContainer); suggestionsContainer.innerHTML = ''; }); suggestionsContainer.appendChild(item); }); if (results.length > 0) { app.toggleDisplay(suggestionsContainer); } }) }, delay); }); } /** * @param {HTMLElement} searchInput * @param {string} searchTerm */ fetchSuggestions(searchInput, searchTerm) { let source = searchInput.dataset.source; let destId = searchInput.dataset.destId; let suggestionsContainer = document.getElementById(destId) return fetch(`${source}?term=${searchTerm}`) .then(response => response.json()) .then(json => json) } /** * @param {HTMLElement} element */ populateCountryField(element) { element.addEventListener('change', this.updateRegionOnCountryField.bind(this)); for (const [key, value] of Object.entries(geodatasource_data)) { let option = document.createElement('option'); option.value = key; option.textContent = value[0]; element.append(option) } element.value = element.dataset.selectedCountry ?? ''; this.updateRegionOnCountryField({target: element}); } /** * @param {Event} event */ updateRegionOnCountryField(event) { let countrySelect = event.target let selectedCountry = countrySelect.value let regionElement = document.getElementById(countrySelect.dataset.regionId); regionElement.innerHTML = ''; if (!geodatasource_data[selectedCountry]) { let option = document.createElement('option'); option.value = ''; option.textContent = ' - '; regionElement.append(option) return; } let regions = geodatasource_data[selectedCountry][1]; let regionArray = regions.split('|'); let selectedRegion = countrySelect.dataset.selectedRegion ?? ''; regionArray.forEach(value => { value = value.replace("'", '-'); let option = document.createElement('option'); option.value = value; option.textContent = value; regionElement.append(option) }) if (selectedRegion) { regionElement.value = selectedRegion } } updateScreenDimensions() { this.viewportWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; this.viewportHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; if (this.viewportWidth > 1200) { document.dispatchEvent(new CustomEvent('on-desktop')); } else { document.dispatchEvent(new CustomEvent('on-mobile')); } } updateDatatablesLayout() { Commons.sleep(250) .then(() => commons.getDatatables().forEach( table => table.columns.adjust().responsive.recalc() )); } /** * @param {Event} event */ onScreenOrientationChanged(event) { this.updateScreenDimensions(); this.updateDatatablesLayout(); document.dispatchEvent(new CustomEvent('screen-rotate', { detail: { target: event.target, width: this.viewportWidth, height: this.viewportHeight, } })); } /** * @param {boolean} sidebarEnabled * @param {string} state */ setSidebar(sidebarEnabled, state) { this.sidebarEnabled = sidebarEnabled; if (sidebarEnabled) { // No expand by default on device if (this.viewportWidth < 576) { return } // expand by default on desktop if (this.viewportWidth > 1100) { document.body.classList.add('opened-sidebar') return } let sidebarStateStored = window.localStorage.getItem(SIDEBAR_STATE_KEY); if (sidebarStateStored !== null) { if (sidebarStateStored.length > 0) { document.body.classList.add(sidebarStateStored) } return; } if (state && this.viewportWidth > 768) { document.body.classList.add(state) } } else { document.body.classList.add('no-sidebar') } } /** * @param {object} page */ setCurrentPage(page) { this.currentPage = page; } /** * @return {object} */ getCurrentPage() { return this.currentPage; } /** * @param {string} widgetName * @param {object} widget */ addWidget(widgetName, widget) { this.widgets[widgetName] = widget; } /** * @param {string} widgetName * @param {object|null} widget */ getWidget(widgetName) { return this.widgets[widgetName] ?? null; } /** * @return {object[]} */ getWidgets() { return this.widgets; } /** * @param {PointerEvent} event */ onCaptchaSubmit(event) { let dataset = event.target.dataset; event.preventDefault(); grecaptcha.ready(() => { grecaptcha.execute(dataset.sitekey, {action: dataset.action}) .then(token => { if (dataset.destId) { document.getElementById(dataset.destId).submit(); } else { event.target.closest('form').submit(); } }); }); } /** * @param {string} url * @param {function(boolean, string)} callback * @param {string|null} title * @param {string|null} message */ needConfirmationClickOnURL(url, callback = null, title = null, message = null) { PageNotifier.confirmDangerousAction( title ?? __('page.generic.confirmation_required'), message ?? __('app.messages.are_you_sure'), ).then((result) => { if (callback) { callback(result, url); return; } if (result) { Commons.postBody(url) } }); } } app = new Application();