// requires jquery 1.7+ // JS s'execute: Retire l'avertissement 'Javascript est requis' (de fait, on sait que JS est actif...) document.body.classList.remove("nojs"); //### Helpers handlebars personnalisés Handlebars.registerHelper('lower', function (options) { return options.fn(this).toLowerCase(); }); Handlebars.registerHelper('repuri', function (find, replace, options) { return encodeURI(options.fn(this).replace(find, replace).toString()); }); Handlebars.registerHelper('todate', function (options) { var d = options.fn(this); var dt = new Date(Number.parseInt(d) * 1); return dt.toLocaleString(); }); Handlebars.registerHelper('timestamp', function (options) { return Number.parseInt(options.fn(this)); }); Handlebars.registerHelper('if_eq', function (a, opts) { var b = localStorage.hasOwnProperty("contact") ? localStorage.getItem("contact") : ""; if (a === b) // Or === depending on your needs return opts.fn(this); else return opts.inverse(this); }); //### Initialisation // ************** CONFIGURATION ***************** // Nom et version de la base de données locale à créer/utiliser var db_name = "ModelePWA" var db_version = "3" // Si deleteOnSuccess est vrai, les données synchronisées seront supprimées de la base locale. var deleteOnSync = true; // Les stores de la liste doNotSync ne seront pas synchronisés. var doNotSync = ["params"]; var postUrl = "/api/modelepwa"; // ********************************************** var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB; var request; // Installe le service worker if ('serviceWorker' in navigator) { console.log("[ServiceWorker] Installe"); navigator.serviceWorker.register('../sw.js'); } var sectionId; var section; var objId; var submitHandler; var deleteHandler; var editHandler; // Charge la page en fonction de l'url actuelle var load = function () { // Reinitialise l'affichage des sections $("#sync").hide(); $("#main").empty(); $("#main").show(); // Parse l'url actuelle var base = window.location.hash.split('?')[0]; sectionId = base.length > 0 ? base : "#index"; section = $("body").find(sectionId); var qry = {}; var qrystr = window.location.hash.split('?')[1]; if (qrystr) { var definitions = qrystr.split('&'); definitions.forEach(function (val, key) { var parts = val.split('=', 2); qry[parts[0]] = parts[1]; }); } // Met à jour le contenu de #main avec la section demandée dans l'url/ // Si l'attribut 'model' existe pour cette section, charge les données depuis la localDb // et compile la section avec handlebars. if ($(section).attr("model")) { var model = $(section).attr("model"); var template = Handlebars.compile(section.html()); // Charge la base de données, et le stockage courant request = indexedDB.open(db_name, db_version); var db; var txs; var store; // Cree les stockages necessaires si ceux ci sont manquants request.onupgradeneeded = function () { var db = request.result; db.createObjectStore(model, { keyPath: "guid" }); console.log("[DB] Cree '" + model + "'"); }; // Si l'accès a la db s'est bien passé: request.onsuccess = function () { var data; db = request.result; txs = db.transaction(model, "readonly"); store = txs.objectStore(model); // Si un id specifique a été demandé, on charge specifiquement cet objet. // Sinon: on les charge tous en memoire. if (qry["id"]) { store.get(qry["id"]).onsuccess = function (event) { data = { data: event.target.result }; $("#main").html(template(data)); populateForm(data['data']); } } else { store.getAll().onsuccess = function (event) { data = { data: event.target.result }; $("#main").html(template(data)); }; } }; request.onerror = function () { console.log("Error while loading the db '" + model + "'"); $("#main").html(template({})); return; }; // (Re)définit la fonction d'interception de la soumission de formulaires submitHandler = function (event) { var form = $("#main").find("form")[0]; // Stop the form from submitting since we’re handling that with AJAX. event.preventDefault(); // Call our function to get the form data. var data = formToJSON(form.elements); data.guid = createGuid(); txs = db.transaction(model, "readwrite"); store = txs.objectStore(model); store.put(data); }; // (Re)définit la fonction de suppression des lignes de l'index deleteHandler = function(event) { var del = $(this); if (confirm("Supprimer la selection?") == true) { $(del).prop("disabled", true); $(".ui-selected").each(function () { var elt = $(this); var id = $(elt).data("id"); txs = db.transaction(model, "readwrite"); store = txs.objectStore(model); store.delete(id).onsuccess = function (evt) { $(elt).remove(); $(".del").prop('disabled', false); }; }); } }; // (Re)définit la fonction de suppression d'edition editHandler = function(event) { var edit = $(this); var id = $(".ui-selected:first").data("id") txs = db.transaction(model, "readonly"); store = txs.objectStore(model); var obj = store.get(id); obj.onsuccess = function (event) { window.location = "/#activites?id=" + id; }; }; } else { // Pas de modele, on charge simplement le html de la section. $("#main").html(section.html()); } } load(); // Recharge dynamiquement le contenu HTML à chaque changement d'url $(window).on('hashchange', function () { load(); }); // Met a jour le contenu des champs d'un formulaire // Le binbding se fait via l'id du champs var populateForm = function (data) { if ($("form").length) { $("input, select, textarea").each(function () { var input = $(this); var fieldName = input.attr('id'); input.val(data[fieldName]); }); } } // ########################### // ### Interactions // Intercepte la soumission de formulaires $("#main").on("submit", "form", function (event) { submitHandler(event); window.location = "/#index"; }); // Gere le clic sur un bouton Supprimer $("#main").on("click", ".del", function (event) { deleteHandler(event); }); // Gere le clic sur un bouton Editer $("#main").on("click", ".edit", function (event) { editHandler(event); }); // Gere le clic sur le bouton de menu pour afficher ou masquer la sidebar $('.bt-menu').on('click', 'svg', function () { $(this).closest('nav').find('div:not(:first)').toggleClass('sidebar'); }); $(document).on('click', '.sidebar', function () { $(this).closest('nav').find('div:not(:first)').toggleClass('sidebar'); }); // Affiche ou masque le bouton de sync selon que le poste est en ligne ou non if (navigator.onLine) { $(".start-sync").removeAttr("disabled"); } else { if (!$(".start-sync").is(":disabled")) { $(".start-sync").attr("disabled") } } // Affiche ou masque, et gere le clic sur le bouton de retour au haut de page. window.onscroll = function (ev) { document.getElementById("back-top").className =(window.pageYOffset > 100) ? "": "hidden"; }; $('#back-top').on('click', function () { $('html, body').animate({ scrollTop: 0 }, 200); }); // Rend les lignes des tables .selectable selectionnables mo = new MutationObserver(function (mutations, observer) { $(".selectable > tbody").bind("mousedown", function (e) { e.metaKey = true; }).selectable({ filter: "tr", stop: function () { $(".del").prop('disabled', ($(".ui-selected").length == 0)); $(".edit").prop('disabled', ($(".ui-selected").length != 1)); }, }); }) mo.observe(document.querySelector('#main'), { childList: true }); // ### Synchronisation des données $(".start-sync").on("click", function () { // (!) voir en debut de fichier pour la configuration de la synchro (cf. les variables doNotSync et deleteOnSync) // Affiche la page de synchro $("#main").hide(); $("#sync").show(); // Verifie l'acces à la LocalDb if (!request) { request = indexedDB.open(db_name, db_version); request.onerror = function () { console.log("Error while accessing the db"); $("#sync").find(".status").html("
Erreur de synchronisation: impossible d'accéder à la base de données locale.
"); return; }; } var db = request.result; $("#sync").find(".status").html("Synchronisation en cours, veuillez patienter...
"); var model; // Parcourt les differents stores for (var i = 0; i < db.objectStoreNames.length; i++) { model = db.objectStoreNames[i]; if (doNotSync.indexOf(model) >= 0) { console.log("Ignored: " + model); continue; } console.log("Sync: " + model); var txs = db.transaction(model, "readonly"); var stores = txs.objectStore(model); stores.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { cursor.value.model = model; var id = cursor.value.guid; var posting = $.post(postUrl, { data: JSON.stringify(cursor.value) }); // Put the results in a div posting.done(function (data) { if (deleteOnSync & data) { var tx = db.transaction(model, "readwrite"); var store = tx.objectStore(model); store.delete(id).onsuccess = function (evt) { }; } }); cursor.continue(); } }; stores.openCursor().onerror = function (event) { console.log(event); } } console.log("Synchro ok"); $("#sync").find(".end-sync").show(); $("#sync").find(".status").html("Synchronisation terminée.
"); }); $(".end-sync").click(function () { load(); }); //###### TOOLBOX ###### function createGuid() { return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) } function getLocation() { try { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(showPosition); } else { console.log("Geolocation is not supported by this browser."); return 0; } } catch (e) { console.log("Geolocation: error"); } } function showPosition(position) { $("input[name='coordinates']").val(position.coords.latitude + "," + position.coords.longitude); console.log(position.coords); } // ### Serialization /** * Checks that an element has a non-empty `name` and `value` property. * @param {Element} element the element to check * @return {Bool} true if the element is an input, false if not */ var isValidElement = function isValidElement(element) { return element.name && element.value; }; /** * Checks if an element’s value can be saved (e.g. not an unselected checkbox). * @param {Element} element the element to check * @return {Boolean} true if the value should be added, false if not */ var isValidValue = function isValidValue(element) { return !['checkbox', 'radio'].includes(element.type) || element.checked; }; /** * Checks if an input is a checkbox, because checkboxes allow multiple values. * @param {Element} element the element to check * @return {Boolean} true if the element is a checkbox, false if not */ var isCheckbox = function isCheckbox(element) { return element.type === 'checkbox'; }; //var isHidden = function isHidden(element) { // return element.type === 'hidden'; //}; /** * Checks if an input is a `select` with the `multiple` attribute. * @param {Element} element the element to check * @return {Boolean} true if the element is a multiselect, false if not */ var isMultiSelect = function isMultiSelect(element) { return element.options && element.multiple; }; /** * Retrieves the selected options from a multi-select as an array. * @param {HTMLOptionsCollection} options the options for the select * @return {Array} an array of selected option values */ var getSelectValues = function getSelectValues(options) { return [].reduce.call(options, function (values, option) { return option.selected ? values.concat(option.value) : values; }, []); }; /** * A more verbose implementation of `formToJSON()` to explain how it works. * * NOTE: This function is unused, and is only here for the purpose of explaining how * reducing form elements works. * * @param {HTMLFormControlsCollection} elements the form elements * @return {Object} form data as an object literal */ var formToJSON_deconstructed = function formToJSON_deconstructed(elements) { // This is the function that is called on each element of the array. var reducerFunction = function reducerFunction(data, element) { // Add the current field to the object. data[element.name] = element.value; // For the demo only: show each step in the reducer’s progress. console.log(JSON.stringify(data)); return data; }; // This is used as the initial value of `data` in `reducerFunction()`. var reducerInitialValue = {}; // To help visualize what happens, log the inital value, which we know is `{}`. console.log('Initial `data` value:', JSON.stringify(reducerInitialValue)); // Now we reduce by `call`-ing `Array.prototype.reduce()` on `elements`. var formData = [].reduce.call(elements, reducerFunction, reducerInitialValue); // The result is then returned for use elsewhere. return formData; }; /** * Retrieves input data from a form and returns it as a JSON object. * @param {HTMLFormControlsCollection} elements the form elements * @return {Object} form data as an object literal */ var formToJSON = function formToJSON(elements) { return [].reduce.call(elements, function (data, element) { // Make sure the element has the required properties and should be added. if (isValidElement(element) && isValidValue(element)) { /* * Some fields allow for more than one value, so we need to check if this * is one of those fields and, if so, store the values as an array. */ if (isCheckbox(element)) { data[element.name] = (data[element.name] || []).concat(element.value); } else if (isMultiSelect(element)) { data[element.name] = getSelectValues(element); } else { data[element.name] = element.value; } } return data; }, {}); };