diff --git a/README.md b/README.md
index 80a255e..e0a5071 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,63 @@
-# Module de gestion de matériels pour Paheko
+# Module de gestion de matériels et de réservations pour Paheko
+Le module original permet de gérer les matériels de l'association.
+La source et la documentation se trouve [ici](https://gitea.zaclys.com/lesanges/equipment).
+Merci à l'auteur pour ce superbe module !
-Ce module permet de gérer les matériels de l'association: stock,
-entrées, sorties, classés par catégorie.
-
-Un matériel peut entrer dans l'association :
-- temporairement (location, emprunt, ...)
-- définitivement (achat, ...)
-
-Dans les deux cas, il est possible de lui associer une écriture de la
-compta ou un fichier.
-
-Un matériel appartenant à l'association peut sortir :
-- temporairement (prêt, ...) : il est possible de lui associer un lieu
- de stockage ainsi qu'un membre dépositaire du matériel et une date
- de retour ; il peut ensuite revenir dans l'association.
-- définitivement (vente, ...) : il est possible de lui associer un
- membre bénéficiaire du matériel.
-
-Il est possible de suivre les matériels prêtés ou loués et de savoir
-si la date de retour est proche ou dépassée.
-
-Un matériel présent temporairement dans l'association peut être
-retourné à son propriétaire.
-
-Un matériel dont il n'existe plus d'exemplaire en stock peut être archivé.
-
-On peut lister l'ensemble des mouvements des matériels ou seulement
-ceux d'un matériel donné.
-
-Il est possible de créer des catégories de matériels, des types
-d'entrées et sorties, des lieux de stockage.
+Ce **module** permet de gérer en plus des réservations.
+Les changements proposés par le module original sont répercutés fréquemment dans cette version.
+Dans la description du module sous Paheko, vous trouverez la version du module original et la version de ce module.
+[Télécharger ici](https://git.kaz.bzh/bountz/equipment_resa)
## Réservations de matériel
-Il est possible de réserver le matériel depuis le site web en mode anonyme
-ou depuis l'interface des membres. Les réservations doivent être validées par un administrateur.
+Réservation de matériel depuis le site web en mode anonyme ou depuis l'interface des membres.
+Les réservations doivent être validées par un administrateur.
+
+## Fonctionnalités
++ Réservation "_anonyme_" depuis le site web (rattachée à un membre par l'admin)
++ Réservation par les membres connectés
++ Convention de prêt/location
++ Gestion des réservations par l'admin (acceptation, refus,...)
++ Calendrier des réservations
++ Ajout de champs personnalisés sur le matériel (ex: tarif de location)
+
+### Roadmap
++ Utiliser le module agenda associatif à la place du JS "fullcalendar"
+
+## Installation
+
+### Module principal
+Télécharger le module depuis le GIT et le décompresser dans un répertoire nommé "modules".
+Décompresser le ZIP dans le répertoire "modules" et déplacer le ZIP.
+A ce stade, vous devez avoir une arborescence comme suit : modules --> equipment_resa --> [fichiers]
+Compresser le répertoire "modules" en le nommant equipment_resa.zip par exemple.
+Aller dans l'interface d'admin Paheko, sous Configuration --> Extensions et tout en bas, cliquer sur le bouton **|importer un module| **
+Une fois le module chargé dans Paheko, activer le, puis aller dans sa configuration et cliquer sur l'onglet [Réservation].
+Activer la réservation en cochant la case : Activer les réservations
+Si vous avez le module d'origine de _Gestion des Matériels_, vous pouvez importer les données : Cliquer sur le bouton **|importer|** en bas.
+
+### Installation des pages de réservation sur le site Web.
+Les fichiers suivants sont présents dans le code du module réservation sous le répertoire "_web_exemple" pour information/exemple.
+Cette partie est exclue du support puisqu'elle touche l'implémentation de la fonctionnalité sur votre site.
+Dans le code du module "Site web", importer les fichiers avec les noms suivants :
+- equipment.html
+- equipment_no_image.png
+- equipment_styles.css
+- global.js
+
+Modifier ensuite les fichiers existants suivants :
+
+- config.html (complet + modif)
+- contact.html (complet + modif)
+- _head.html (ne contient que la partie à ajouter)
+- _breadcrumbs.html (complet + modif)
+
+### Configuration
+Site Web : (Configuration du site web) : Cocher "Accès aux réservations"
+
+## Dépendances
+Site Web : (Configuration du site web) : Cocher "Formulaire de contact"
## Droits d'accès
-Le module est accessible uniquement pour les membres ayant au moins le
-droit d'écriture en gestion des membres.
\ No newline at end of file
+Admin : Le module est accessible uniquement pour les membres ayant au moins le droit d'écriture en gestion des membres.
+Membres connectés : Accès à la liste du matériel, au détail d'un matériel et à sa réservation.
+Visiteurs (Web) : Accès à la liste du matériel, au détail d'un matériel et à sa réservation.
\ No newline at end of file
diff --git a/_web_exemple/_breadcrumbs.html b/_web_exemple/_breadcrumbs.html
new file mode 100644
index 0000000..49c0b06
--- /dev/null
+++ b/_web_exemple/_breadcrumbs.html
@@ -0,0 +1,20 @@
+{{if !$parent && !$custom}}
+ {{:error message="Missing parent|custom parameters"}}
+{{/if}}
+
+
+
+
\ No newline at end of file
diff --git a/_web_exemple/_head.html b/_web_exemple/_head.html
new file mode 100644
index 0000000..eddf6fd
--- /dev/null
+++ b/_web_exemple/_head.html
@@ -0,0 +1,7 @@
+{{* Bloc à positionner dans la partie
de la page *}}
+ {{if $module.config.show_equipment !== false}}
+ {{* Include JS et CSS pour le calendrier des réservations *}}
+ {{:include file="/equipment/_web_calendar.html" title=$page.title|remove_leading_number}}
+
+
+ {{/if}}
diff --git a/_web_exemple/config.html b/_web_exemple/config.html
new file mode 100644
index 0000000..0917522
--- /dev/null
+++ b/_web_exemple/config.html
@@ -0,0 +1,179 @@
+{{:admin_header title="Configuration du site web"}}
+
+{{#form on="save"}}
+ {{:assign uri=null}}
+ {{if $_POST.home_sticky_uri|trim}}
+ {{:assign uri=$_POST.home_sticky_uri|trim|regexp_replace:'!^.*?([a-zA-Z0-9_-]+)$!':"$1"}}
+ {{#pages uri=$uri}}
+ {{else}}
+ {{:error message="Cette page n'existe pas ou est en brouillon : %s"|args:$uri}}
+ {{/pages}}
+ {{/if}}
+
+ {{:assign var="checkboxes.show_search" value=null}}
+ {{:assign var="checkboxes.show_hours" value=null}}
+ {{:assign var="checkboxes.show_bookings" value=null}}
+ {{:assign var="checkboxes.show_address" value=null}}
+ {{:assign var="checkboxes.show_email" value=null}}
+ {{:assign var="checkboxes.show_phone" value=null}}
+ {{:assign var="checkboxes.show_marker" value=null}}
+ {{:assign var="checkboxes.contact_form" value=null}}
+ {{:assign var="checkboxes.show_login" value=null}}
+ {{:assign var="checkboxes.show_equipment" value=null}}
+
+ {{#foreach from=$checkboxes key="name"}}
+ {{:assign var="present" from="_POST.%s_present"|args:$name}}
+ {{:assign var="value" from="_POST.%s"|args:$name}}
+ {{if $present}}
+ {{:assign var="checkboxes.%s"|args:$name value=$value|boolval}}
+ {{/if}}
+ {{/foreach}}
+
+ {{:save key="config"
+ color1=null
+ color2=null
+ hue1=$_POST.hue1|intval
+ hue2=$_POST.hue2|intval
+ layout=$_POST.layout|trim|or:null
+ background=$_POST.background|trim|or:null
+ show_search=$checkboxes.show_search
+ show_hours=$checkboxes.show_hours
+ show_bookings=$checkboxes.show_bookings
+ show_address=$checkboxes.show_address
+ show_email=$checkboxes.show_email
+ show_phone=$checkboxes.show_phone
+ show_marker=$checkboxes.show_marker
+ contact_form=$checkboxes.contact_form
+ show_login=$checkboxes.show_login
+ show_equipment=$checkboxes.show_equipment
+ home_sticky_uri=$uri|or:null
+ }}
+ {{:redirect to="?ok=1"}}
+{{/form}}
+
+{{if $_GET.ok}}
+ Configuration enregistrée.
+{{/if}}
+
+{{:form_errors}}
+
+
+
+{{:admin_footer}}
\ No newline at end of file
diff --git a/_web_exemple/contact.html b/_web_exemple/contact.html
new file mode 100644
index 0000000..7f127fa
--- /dev/null
+++ b/_web_exemple/contact.html
@@ -0,0 +1,70 @@
+{{if !$module.config.contact_form || !$config.org_email}}
+ {{:redirect to="./"}}
+{{/if}}
+{{if $logged_user.id }}
+ {{:assign user=$logged_user.nom email=$logged_user.email}}
+{{/if}}
+{{:assign var=sujets a="Question générale" b="Proposition de matériel" c="Problème sur le site" d="Réservation de matériel" e="Vélo" f="Autre"}}
+{{* Si demande de réservation de matériel NON propriété de l'association *}}
+{{if $_GET.fonc|trim === 'resa'}}
+ {{:assign var=sujet value="d"}}
+ {{* Charge le matériel *}}
+ {{#load module="equipment" key=$_GET.key assign="equipment"}}
+ {{:assign var=message value="Bonjour, je souhaite me renseigner pour la réservation de %s du __/__/__ au __/__/__ ."|args:$name}}
+ {{/load}}
+{{elseif $_GET.fonc|trim === 'velo'}}
+ {{:assign var=sujet value="e"}}
+ {{#select id, modele FROM plugin_stock_velos WHERE id = :id; :id=$_GET.key }}
+ {{:assign var=message value="Bonjour, je suis intéressé.e par le vélo %s n° %s ."|args:$modele:$id}}
+ {{/select}}
+{{/if}}
+{{#form on="send"}}
+ {{:captcha verify=true}}
+ {{if $_POST.name|trim === ''}}
+ {{:error message="Merci de renseigner un nom."}}
+ {{elseif $_POST.name|regexp_match:'/https?:|www\./'}}
+ {{:error message="Le nom ne peut pas comporter d'adresse web"}}
+ {{elseif $_POST.email|regexp_match:'/https?:|www\./'}}
+ {{:error message="L'adresse e-mail ne peut pas comporter d'adresse web"}}
+ {{elseif $_POST.message|trim === ''}}
+ {{:error message="Merci de renseigner un message."}}
+ {{elseif !$_POST.email|trim === ''}}
+ {{:error message="Merci d'indiquer votre adresse e-mail."}}
+ {{elseif !$_POST.email|check_email}}
+ {{:error message="L'adresse e-mail renseignée est invalide."}}
+ {{/if}}
+ {{if $_POST.subject|trim === ''}}
+ {{:error message="Merci de choisir un sujet."}}
+ {{else}}
+ {{:assign var="sujet" from="sujets.%s"|args:$_POST.subject}}
+ {{/if}}
+ {{:assign body="Message de :\n%s\n%s\n\n---\n\n%s"|args:$_POST.name:$_POST.email:$_POST.message}}
+ {{:mail to=$config.org_email subject=$sujet|trim body=$body replyto=$_POST.email|trim}}
+ {{:redirect to="%s?sent=ok"|args:$request_url}}
+{{/form}}
+
+{{:include file="./_head.html" title="Contact"}}
+
+
+ Contactez nous
+ {{if $_GET.sent}}
+ Votre message a bien été transmis !
+ {{else}}
+
+ {{/if}}
+
+
+{{:include file="./_foot.html"}}
\ No newline at end of file
diff --git a/_web_exemple/equipment.html b/_web_exemple/equipment.html
new file mode 100644
index 0000000..2ddb3ac
--- /dev/null
+++ b/_web_exemple/equipment.html
@@ -0,0 +1,55 @@
+{{* -*- brindille -*- *}}
+{{:assign var="page.title" value="Matériel à disposition"}}
+{{:assign var="page.path" value="./equipment.html"}}
+
+{{if $_GET.fonc=="save_resa"}}
+ {{** No header allowed to download the PDF *}}
+ {{*:assign var=page.title value="Matériel réservé ! Voici votre convention de prêt/location."*}}
+{{else}}
+ {{:include file="./_head.html" title=$page.title|remove_leading_number}}
+ {{:assign var="custom." _title="🧰 Matériel" _url=$page.path}}
+ {{if !$_GET.fonc || $_GET.fonc=="inv2" || $_GET.fonc=="inv3"}}
+ {{:assign var="page" title="Matériel à disposition" path="./equipment.html"}}
+ {{elseif $_GET.fonc=="voir"}}
+ {{:assign var="page" title="Détails du matériel" path="./equipment.html"}}
+ {{:assign var="custom." _title="🔎 Détails" _url="#"}}
+ {{elseif $_GET.fonc=="resa" || $_GET.fonc=="confirm_resa"}}
+ {{:assign var="page" title="Réservation du matériel" path="./equipment.html"}}
+ {{:assign var="custom." _title="🗓️ Réservation" _url="#"}}
+ {{/if}}
+ {{:include file="./_breadcrumbs.html"}} {{* custom au lieu de parent car pas dans la base SQL *}}
+{{/if}}
+{{$page.title|remove_leading_number}}
+Le matériel proposé ici provient soit de dons/achats et appartient à l'association soit de prêts ou mis à disposition pour la communauté.
+Dans le cas de matériel mis à disposition, celui-ci n'appartient pas à l'association : cette dernière va simplement vous mettre en relation avec la personne à qui il appartient.
+
+
+{{if $_GET.ok}}
+ {{if $_GET.fonc == "confirm_resa" && !$_GET.rid }}
+ Réservation effectuée
+ {{/if}}
+{{/if}}
+
+{{if !$_GET.fonc}}
+ {{:include file="/equipment/_web_inventory.html" parent=$page.path}}
+{{*elseif $_GET.fonc=="inv2"}}
+ {{:include file="/equipment/_web_inventory2.html" parent=$page.path*}}
+{{elseif $_GET.fonc=="inv3"}}
+ {{:include file="/equipment/_web_inventory3.html" parent=$page.path}}
+{{elseif $_GET.fonc=="voir"}}
+ {{:include file="/equipment/_web_view.html" parent=$page.path}}
+{{elseif $_GET.fonc=="resa"}}
+ {{:include file="/equipment/_web_resa.html" parent=$page.path}}
+{{elseif $_GET.fonc=="resadel"}}
+ {{:include file="/equipment/_web_resadel.html" keep="_GET.keys" parent=$page.path}}
+{{elseif $_GET.fonc=="confirm_resa"}}
+ {{if $_GET.ok}}
+ {{:include file="/equipment/_web_contract.html" parent=$page.path}}
+ {{/if}}
+{{elseif $_GET.fonc=="save_resa"}}
+ {{:include file="/equipment/_web_saved_resa.html" parent=$page.path}}
+{{else}}
+ {{:include file="/equipment/_web_inventory.html" parent=$page.path keep="cat_key,prop"}}
+{{/if}}
+
+{{:include file="./_foot.html"}}
\ No newline at end of file
diff --git a/_web_exemple/equipment_no_image.png b/_web_exemple/equipment_no_image.png
new file mode 100644
index 0000000..570bb26
Binary files /dev/null and b/_web_exemple/equipment_no_image.png differ
diff --git a/_web_exemple/equipment_styles.css b/_web_exemple/equipment_styles.css
new file mode 100644
index 0000000..4ec04e7
--- /dev/null
+++ b/_web_exemple/equipment_styles.css
@@ -0,0 +1,377 @@
+:root {
+ --gMainColor: 0, 99, 155;
+ --gSecondColor: 7, 220, 98;
+ --gTextColor: 0, 0, 0;
+}
+
+.informations {
+ margin-top : 0.5em;
+}
+
+.informations dt::after {
+ content: ' :';
+}
+
+.informations dl.describe {
+ border: 0px;
+ padding: 0.5em;
+}
+
+.informations dl.describe > dt {
+ float: left;
+ clear: left;
+ width: 200px;
+ text-align: left;
+ font-weight: bold;
+ color: green;
+}
+
+.informations dl.describe > dd {
+ margin: 0 0 0 110px;
+ padding: 0 0 0.5em 0;;
+}
+
+.nombre {
+ text-align: right !important;
+}
+
+.resaactionbutton {
+ z-index: 1;
+ position: absolute;
+ top: 15%;
+ right: 05%;
+}
+
+/* Image style leboncoin */
+figure.leboncoin {
+ border: thin silver solid;
+ display: flex;
+ flex-flow: column;
+ padding: 5px;
+ max-width: 170px;
+ height: 261px;
+ margin: auto;
+}
+
+img.leboncoin {
+ max-width: 150px;
+ max-height: 150px;
+}
+
+figcaption.leboncoin {
+ background-color: #dce7dc;
+ color: white;
+ font: italic smaller sans-serif;
+ padding: 3px;
+ text-align: center;
+ height: 179px;
+}
+
+/** Image vignette */
+
+/* Styles par défaut (mobile first) */
+.imagemateriel {
+ display: block;
+ object-fit: cover;
+ width: 100%; /* L'image prendra 100% de la largeur de son conteneur */
+ height: auto; /* Maintient le ratio */
+ max-width: 250px;
+ max-height: 150px;
+}
+/* Pour les écrans de plus de 768px (tablettes/ordinateurs) */
+@media (min-width: 768px) {
+ .imagemateriel {
+ display: block;
+ object-fit: cover;
+ width: 100%; /* L'image prendra 100% de la largeur de son conteneur */
+ height: auto; /* Maintient le ratio */
+ max-width: 670px;
+ max-height: 150px;
+ }
+}
+
+/* Datepicker */
+dialog.datepicker {
+ user-select: none;
+ position: absolute;
+ left: 0;
+ padding: .3rem;
+ border: none;
+ box-shadow: 0 0 5px #000;
+ border-radius: .5rem;
+ z-index: 1000;
+}
+
+/* Bouton Link */
+nav.tabs aside {
+ float: right;
+ max-width: 50%;
+ text-align: right;
+ clear: right;
+ margin-top: .5em;
+}
+
+.main.icn-btn {
+ padding: 13px 20px;
+ display: inline-block;
+}
+.icn-btn {
+ appearance: none;
+ backface-visibility: hidden;
+ background-color: rgba(255, 255, 255, 0.7);
+ border-radius: 5px;
+ border-style: none;
+ box-shadow: rgb(0, 148, 133) 0 4px 9px;
+ box-sizing: border-box;
+ color: #4cb130;
+ cursor: pointer;
+ font-family: Inter,-apple-system,system-ui,"Segoe UI",Helvetica,Arial,sans-serif;
+ font-size: 16px;
+ font-weight: 600;
+ letter-spacing: normal;
+ line-height: 1.5;
+ outline: none;
+ overflow: hidden;
+ position: relative;
+ text-align: center;
+ text-decoration: none;
+ transform: translate3d(0, 0, 0);
+ transition: all .3s;
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: manipulation;
+ vertical-align: top;
+ white-space: nowrap;
+
+ a:visited {
+ color: black;
+ }
+}
+
+.icn-btn:hover {
+ background-color: #1e8449;
+ opacity: 1;
+ transform: translateY(0);
+ transition-duration: .35s;
+}
+
+.icn-btn:active {
+ transform: translateY(2px);
+ transition-duration: .35s;
+}
+
+.icn-btn:hover {
+ color: #dfadad;
+ box-shadow: rgba(39, 174, 96, .2) 0 6px 12px;
+}
+
+/* Datepicker button (.main[data-icon]:after, [data-icon].right:after) */
+.datepicker.icn-btn[data-icon]:before {
+ display: inline-block;
+ font-family: "paheko", sans-serif;
+ text-shadow: 1px 1px 1px var(--gLightBorderColor);
+ font-size: 1.2em;
+ line-height: .8em;
+ vertical-align: middle;
+ content: attr(data-icon);
+ font-weight: normal;
+ text-decoration: none;
+}
+
+
+/** Table.list */
+
+.list{width:100%;border-collapse:collapse}
+.list td,.list th {padding:12px 15px;border:1px solid #ddd;text-align:center;font-size:16px}
+.list th {background-color:rgba(var(--gSecondColor), 0.5); color:rgb(var(--gTextColor));}
+.list tbody tr:nth-child(even) {background-color:rgba(var(--gSecondColor), 0.2);}
+@media (max-width:768px){
+ .list thead {display:none}
+ .list,.list tbody,.list tr, .list td {display:block;width:100%}
+ .list tr {margin-bottom:15px}
+ .list td {padding-left:50%;text-align:left; position:relative}
+ .list td::before{content:attr(data-label); position:absolute;left:0;width:50%;padding-left:15px;font-size:15px;font-weight:bold;text-align:left}
+}
+
+/** Table Pagination */
+.pagination {
+ clear: both;
+ list-style-type: none;
+ padding: 0.4em 0;
+ text-align: center;
+}
+
+.pagination li {
+ display: inline-block;
+ margin: 0 0.3em;
+}
+
+.pagination li.current {
+ font-size: 1.3em;
+}
+
+.pagination li a {
+ color: rgb(var(--gTextColor));
+}
+
+.pagination li button {
+ color: rgb(var(--gTextColor));
+ background: transparent;
+ border: none;
+ padding: 0;
+ margin: 0;
+ box-shadow: none;
+ text-decoration: underline;
+}
+
+/** Calendar */
+
+/* Base mobile styles (no media query needed) */
+#calendar {
+ display: none; /* Utiliser !important peut être nécessaire ici */
+}
+#calendar_light {
+ display: block;
+}
+
+/* Tablet and up */
+@media screen and (min-width: 768px) {
+ #calendar {
+ display: block;
+ margin: 0;
+ padding: 0;
+ font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
+ font-size: 14px;
+ max-width: 800px;
+ margin: 10px auto;
+ }
+
+ #calendar_light {
+ display: none;
+ }
+}
+/* Desktop and up
+@media screen and (min-width: 1024px) {
+
+}*/
+
+/** Dropdown NEW */
+/**
+ * Dropdown, eg. interactive
+ */
+nav.dropdown {
+ position: relative;
+ height: 2.3rem;
+ width: 20em;
+ border: 1px solid rgb(var(--gMainColor));
+ border-radius: .25rem;
+ z-index: 100;
+}
+
+nav.dropdown ul {
+ background-color: rgb(var(--gBgColor));
+ top: 0;
+ left: 0;
+ right: 0;
+ position: absolute;
+ border-radius: .5em;
+}
+
+nav.dropdown ul::after {
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.5em;
+ height: 1.5em;
+ font-size: 1.5em;
+ content: "↓";
+ font-family: "paheko";
+ cursor: pointer;
+}
+
+nav.dropdown li {
+ display: none;
+}
+
+html.js nav.dropdown:not(.open) {
+ cursor: pointer;
+}
+
+nav.dropdown li.selected, html.nojs nav.dropdown:hover li, nav.dropdown.open li {
+ display: block;
+}
+
+html.js nav.dropdown:not(.open) li a {
+ pointer-events: none;
+}
+
+nav.dropdown li > a {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding: .4rem .6rem;
+ text-decoration: none;
+ height: 1.5em;
+ color: unset;
+ cursor: pointer;
+}
+
+nav.dropdown li a strong {
+ font-weight: normal;
+ text-align: left;
+ margin-right: auto;
+}
+
+nav.dropdown li a small {
+ color: var(--gBorderColor);
+ text-align: right;
+ white-space: pre;
+}
+
+nav.dropdown li.label {
+ display: none;
+}
+
+nav.dropdown.open {
+ border-color: transparent;
+}
+
+nav.dropdown.open li:nth-child(even) a {
+ background: rgba(var(--gSecondColor), 0.2);
+}
+
+nav.dropdown.open ul {
+ box-shadow: 0 0 5px .2rem rgba(var(--gMainColor), 0.5);
+ border-radius: .25rem;
+}
+
+nav.dropdown.open .selected a {
+ box-shadow: 0 0 5px .2rem rgba(var(--gMainColor), 0.5);
+ color: rgb(var(--gHoverLinkColor));
+}
+
+nav.dropdown.open li a strong {
+ font-weight: bold;
+}
+
+nav.dropdown.open li a:hover {
+ background: rgba(var(--gMainColor), 0.2);
+}
+
+nav.dropdown li a > span {
+ width: 2em;
+ text-align: center;
+}
+
+@media handheld, screen and (max-width:981px) {
+ nav.dropdown.open li.default {
+ display: block;
+ }
+
+ nav.home ul {
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/_web_exemple/global.js b/_web_exemple/global.js
new file mode 100644
index 0000000..f4bec79
--- /dev/null
+++ b/_web_exemple/global.js
@@ -0,0 +1,1203 @@
+(function () {
+ let d = document.documentElement;
+ window.g = window.garradin = {
+ admin_url: d.dataset.url,
+ static_url: d.dataset.url + 'static/',
+ version: d.dataset.version,
+ loaded: {}
+ };
+
+ d.classList.remove('nojs');
+ d.classList.add('js');
+
+ window.$ = function(selector) {
+ if (!selector.match(/^[.#]?[a-z0-9_-]+$/i))
+ {
+ return document.querySelectorAll(selector);
+ }
+ else if (selector.substr(0, 1) == '.')
+ {
+ return document.getElementsByClassName(selector.substr(1));
+ }
+ else if (selector.substr(0, 1) == '#')
+ {
+ return document.getElementById(selector.substr(1));
+ }
+ else
+ {
+ return document.getElementsByTagName(selector);
+ }
+ };
+
+ if (!document.querySelectorAll)
+ {
+ return;
+ }
+
+ g.onload = function(callback, dom)
+ {
+ if (typeof dom === 'undefined') {
+ dom = true;
+ }
+
+ var eventName = dom ? 'DOMContentLoaded' : 'load';
+
+ window.addEventListener(eventName, callback);
+
+ if (!dom && document.readyState === 'complete') {
+ callback();
+ }
+ };
+
+ g.toggle = function(selector, visibility, resize_parent)
+ {
+ if (!('classList' in document.documentElement))
+ return false;
+
+ if (selector instanceof Array)
+ {
+ for (var i = 0; i < selector.length; i++)
+ {
+ g.toggle(selector[i], visibility, false);
+ }
+
+ if (resize_parent !== false) {
+ g.resizeParentDialog();
+ }
+
+ return true;
+ }
+ else if (selector instanceof HTMLElement) {
+ var elements = [selector];
+ }
+ else {
+ var elements = document.querySelectorAll(selector);
+ }
+
+ for (var i = 0; i < elements.length; i++) {
+ elements[i].classList.toggle('hidden', visibility ? false : true);
+
+ elements[i].querySelectorAll('[data-required]').forEach(e => {
+ e.required = parseInt(e.dataset.required, 10);
+ });
+
+ // Make sure hidden elements are not really required
+ // Avoid Chrome bug "An invalid form control with name='' is not focusable."
+ elements[i].querySelectorAll('input[required], textarea[required], select[required], button[required]').forEach((e) => {
+ if (typeof e.dataset.disabled === 'undefined') {
+ e.dataset.disabled = e.hasAttribute('disabled') ? 1 : 0;
+ }
+
+ e.disabled = !visibility ? true : parseInt(e.dataset.disabled, 10);
+ });
+ }
+
+ if (resize_parent !== false) {
+ g.resizeParentDialog();
+ }
+
+ return true;
+ };
+
+ g.script = function (file, callback) {
+ if (file in g.loaded) {
+ callback();
+ return;
+ }
+
+ if (!file.match(/^https?:\/\//)) {
+ file = this.static_url + file + '?' + g.version;
+ }
+
+ var script = g.loaded[file] = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = file;
+ script.onload = callback;
+ document.head.appendChild(script);
+ };
+
+ g.style = function (file) {
+ var link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = this.static_url + file + '?' + g.version;
+ return document.head.appendChild(link);
+ };
+
+ g.normalizeString = function (str) {
+ return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "").toLowerCase();
+ };
+
+ g.dialog = null;
+ g.dialog_title = null;
+ g.focus_before_dialog = null;
+ g.dialog_on_close = false;
+ g.dialog_options = null;
+
+ g.openDialog = function (content, options) {
+ if (null !== g.dialog) {
+ g.closeDialog();
+ }
+ return g.replaceDialog(content, options);
+ };
+
+ g.createDialog = function (content, options) {
+ options = g.getDialogOptions(options);
+ g.dialog_options = options;
+
+ g.focus_before_dialog = document.activeElement;
+
+ g.dialog = document.createElement('dialog');
+ g.dialog.id = 'dialog';
+ g.dialog.className = options.classname || '';
+ g.dialog.dataset.caption = options.caption || '';
+
+ var toolbar = document.createElement('header');
+ toolbar.className = 'toolbar';
+
+ var t = document.createElement('h4');
+ t.className = 'title';
+ toolbar.appendChild(t);
+
+ g.dialog_title = document.title;
+
+ if (options.close) {
+ var btn = document.createElement('button');
+ btn.className = 'icn-btn closeBtn main';
+ btn.setAttribute('data-icon', '✘');
+ btn.type = 'button';
+ btn.innerHTML = 'Fermer';
+ btn.onclick = g.closeDialog;
+ toolbar.appendChild(btn);
+ }
+
+ g.dialog.style.opacity = 0;
+ g.dialog.appendChild(toolbar);
+ document.body.appendChild(g.dialog);
+ g.dialog.showModal();
+
+ // Remove ability to scoll background when dialog is open
+ // Avoid having the page "jumping" because the scrollbar has been removed
+ document.body.style.width = document.body.clientWidth + 'px';
+ document.body.style.overflow = 'hidden';
+ };
+
+ g.getDialogOptions = function (options) {
+ if (typeof options !== 'object' || options === null) {
+ options = {};
+ }
+
+ options.callback = options.callback ?? null;
+ options.classname = options.classname ?? null;
+ options.close = options.close ?? true;
+ options.caption = options.caption ?? null;
+ options.click_to_close = options.click_to_close ?? false;
+ options.escape_to_close = options.escape_to_close ?? true;
+ g.dialog_on_close = options.on_close || false;
+ return options;
+ };
+
+ g.replaceDialog = function (content, options) {
+ if (!g.dialog) {
+ g.createDialog(content, options);
+ }
+ else {
+ g.dialog.style.opacity = 0;
+ g.dialog.classList.remove('loaded');
+ g.dialog.lastElementChild.remove();
+ g.resetDialogEvents();
+ }
+
+ options = g.getDialogOptions(options);
+
+ var t = g.dialog.querySelector('h4.title');
+
+ if (!t) {
+ return;
+ }
+
+ t.innerText = options.caption || '';
+
+ if (options.caption && !document.title.match(options.caption)) {
+ document.title = options.caption + ' — ' + g.dialog_title;
+ }
+
+ g.setDialogKey('Escape', g.closeDialogOnEscape);
+
+ if (typeof content == 'string') {
+ var container = document.createElement('div');
+ container.className = 'content';
+ container.innerHTML = content;
+ content = container;
+ }
+ else if (content instanceof DocumentFragment) {
+ var container = document.createElement('div');
+ container.className = 'content';
+ container.appendChild(content.cloneNode(true));
+ content = container;
+ }
+
+ let tag = content.tagName.toLowerCase();
+
+ if (tag !== 'iframe' && tag !== 'div') {
+ var container = document.createElement('div');
+ container.className = 'preview';
+ container.appendChild(content);
+ g.dialog.appendChild(container);
+ }
+ else {
+ g.dialog.appendChild(content);
+ }
+
+ if (tag == 'img' || tag == 'iframe') {
+ event = 'load';
+ }
+ else if (tag == 'audio' || tag == 'video') {
+ event = 'canplaythrough';
+ }
+
+ if (event) {
+ content.addEventListener(event, () => { if (g.dialog) g.dialog.classList.add('loaded'); });
+
+ if (event && options.callback) {
+ content.addEventListener(event, options.callback);
+ }
+ }
+ else {
+ g.dialog.classList.add('loaded');
+ }
+
+ // Restore CSS defaults
+ window.setTimeout(() => { g.dialog.style.opacity = ''; }, 50);
+
+ if (options.click_to_close) {
+ g.dialog.onclick = (e) => {
+ if (e.target === g.dialog) {
+ g.closeDialog();
+ }
+ };
+ }
+
+ return content;
+ };
+
+ g.openFrameDialog = function (url, options) {
+ options = options ?? {};
+ options.height = options.height || 'auto';
+ options.callback = options.callback || null;
+ options.classname = options.classname || null;
+ options.escape_to_close = options.escape_to_close || true;
+
+ var iframe = document.createElement('iframe');
+ iframe.src = url;
+ iframe.name = 'dialog';
+ iframe.id = 'frameDialog';
+ iframe.frameborder = '0';
+ iframe.scrolling = 'yes';
+ iframe.width = iframe.height = 0;
+ iframe.setAttribute('data-height', options.height);
+
+ iframe.addEventListener('load', () => {
+ iframe.contentWindow.onkeyup = (e) => { if (e.key === 'Escape') g.closeDialogOnEscape(); };
+
+ if (iframe.parentNode.className) {
+ return;
+ }
+
+ // We need to wait a bit for the height to be correct, not sure why
+ window.setTimeout(() => {
+ var height = iframe.dataset.height;
+
+ if (iframe.dataset.height == 'auto'
+ && iframe.contentWindow.document.body
+ && iframe.contentWindow.document.body.offsetHeight < g.dialog.offsetHeight) {
+ height = iframe.contentWindow.document.body.offsetHeight + 'px';
+ }
+ else if (!height || height == 'auto') {
+ height = '90%';
+ }
+
+ iframe.style.height = height;
+ }, 200);
+ });
+
+ g.openDialog(iframe, options);
+ return iframe;
+ };
+
+ g.reloadParentDialog = () => {
+ if (typeof window.parent.g === 'undefined' || !window.parent.g.dialog) {
+ return;
+ }
+
+ location.href = window.parent.g.dialog.querySelector('iframe').getAttribute('src');
+ };
+
+ g.setParentDialogHeight = (height) => {
+ if (typeof window.parent.g === 'undefined' || !window.parent.g.dialog) {
+ return;
+ }
+
+ window.parent.g.dialog.querySelector('iframe').setAttribute('data-height', height);
+ g.resizeParentDialog(height);
+ };
+
+ g.toggleDialogFullscreen = () => {
+ g.dialog.classList.add('fullscreen')
+ g.dialog.childNodes[1].style.height = null;
+ };
+
+ g.resizeParentDialog = (forced_height) => {
+ if (typeof window.parent.g === 'undefined' || !window.parent.g.dialog) {
+ return;
+ }
+
+ var p = window.parent;
+ var dialog = p.g.dialog;
+
+ if (dialog.classList.contains('fullscreen')) {
+ return;
+ }
+
+ if (!dialog.dataset.caption && document.title) {
+ var title = document.title.replace(/^(\s[^—-]+\s).*$/, "$1");
+ dialog.querySelector('.title').innerText = title;
+ p.document.title = document.title + ' — ' + p.g.dialog_title;
+ }
+
+ let height;
+
+ if (forced_height) {
+ height = forced_height;
+ }
+ else {
+ let body_height = document.body.offsetHeight;
+ let parent_height = p.innerHeight;
+
+ if (body_height > parent_height * 0.9) {
+ height = '90%';
+ }
+ else {
+ height = body_height + 'px';
+ }
+ }
+
+ dialog.childNodes[1].style.height = height;
+ };
+
+ g.closeDialogOnEscape = () => {
+ if (!g.dialog_options.escape_to_close) {
+ return true;
+ }
+
+ g.closeDialog();
+ return false;
+ };
+
+ g.closeDialog = function () {
+ for (var i in g.dialog_events) {
+ if (!g.dialog_events.hasOwnProperty(i)) {
+ continue;
+ }
+
+ var evt = g.dialog_events[i];
+ if (evt[0] !== 'close') {
+ continue;
+ }
+
+ evt[1]();
+ }
+
+ g.resetDialogEvents();
+
+ if (null === g.dialog) {
+ return;
+ }
+
+ if (g.dialog.preventClose && g.dialog.preventClose()) {
+ return false;
+ }
+
+ if (g.dialog_on_close) {
+ location.href = g.dialog_on_close == true ? location.href : g.dialog_on_close.replace(/!/, g.admin_url);
+ return;
+ }
+
+ var d = g.dialog;
+ d.style.opacity = 0;
+ g.dialog = null;
+ g.dialog_options = null;
+
+ window.setTimeout(() => { d.parentNode.removeChild(d); }, 500);
+
+ if (g.focus_before_dialog) {
+ g.focus_before_dialog.focus();
+ }
+
+ if (g.dialog_title !== null) {
+ document.title = g.dialog_title;
+ g.dialog_title = null;
+ }
+
+ document.body.style.overflow = null;
+ document.body.style.width = null;
+ };
+
+ g.openFormInDialog = (form) => {
+ if (form.target != '_dialog' && form.target != 'dialog') {
+ return;
+ }
+
+ let url = form.getAttribute('action');
+ url = url + (url.indexOf('?') > 0 ? '&' : '?') + '_dialog';
+ form.setAttribute('action', url);
+ form.target = 'dialog';
+
+ g.openFrameDialog('about:blank', {'height': form.getAttribute('data-dialog-height') ? 90 : 'auto'});
+ form.submit();
+ return false;
+ };
+
+ g.checkUncheck = function()
+ {
+ var checked = this.checked;
+ this.form.querySelectorAll('tbody input[type=checkbox]').forEach((elm) => {
+ elm.checked = checked;
+ elm.dispatchEvent(new Event("change"));
+ });
+
+ this.form.querySelectorAll('thead input[type=checkbox], tfoot input[type=checkbox]').forEach((elm) => {
+ elm.checked = checked;
+ });
+
+ return true;
+ };
+
+ g.togglePasswordVisibility = (field, repeat, show) => {
+ if (typeof show == 'undefined') {
+ show = field.type.toLowerCase() == 'password';
+ }
+
+ var btn = field.nextSibling;
+
+ if (!btn) {
+ throw Error('button not found');
+ }
+
+ field.type = show ? 'text' : 'password';
+ btn.dataset.icon = !show ? '👁' : '⤫';
+ btn.innerHTML = !show ? 'Voir le mot de passe' : 'Cacher le mot de passe';
+ field.classList.toggle('clearTextPassword', !show);
+
+ if (repeat) {
+ repeat.type = field.type;
+ repeat.classList.toggle('clearTextPassword', !show);
+ }
+ };
+
+ /**
+ * Adds a "show password" button next to password inputs
+ */
+ g.enhancePasswordField = function (field)
+ {
+ if (field.id.indexOf('_confirmed') != -1) {
+ return;
+ }
+
+ var show_password = document.createElement('button');
+ show_password.type = 'button';
+ show_password.className = 'icn-btn';
+
+ field.parentNode.insertBefore(show_password, field.nextSibling);
+
+ let repeat_field = document.getElementById(field.id + '_confirmed');
+
+ g.togglePasswordVisibility(field, repeat_field, false);
+
+ show_password.onclick = function (e) {
+ var pos = field.selectionStart;
+
+ g.togglePasswordVisibility(field, repeat_field);
+
+ // Remettre le focus sur le champ mot de passe
+ // on ne peut pas vraiment remettre le focus sur le champ
+ // précis qui était utilisé avant de cliquer sur le bouton
+ // car il faudrait enregistrer les actions onfocus de tous
+ // les champs de la page
+ field.focus();
+ field.selectionStart = field.selectionEnd = pos;
+ };
+ };
+
+ g.enhanceHueField = (input) => {
+ var container = document.createElement('span');
+ container.className = 'hue-selector';
+
+ var c = input.cloneNode(true);
+ var gradient = document.createElement('span');
+ gradient.className = 'gradient';
+
+ if (c.dataset.grey) {
+ var grey = document.createElement('span');
+ grey.className = 'grey';
+ gradient.appendChild(grey);
+ }
+
+ var color = document.createElement('span');
+ color.className = 'color';
+ gradient.appendChild(color);
+
+ var handle = document.createElement('span');
+ handle.className = 'handle';
+
+ container.appendChild(gradient);
+ container.appendChild(handle);
+ container.appendChild(c);
+
+ var s = c.dataset.saturation ?? '100%';
+ var l = c.dataset.lightness ?? '50%';
+
+ container.style = '--sl: ' + s + ', ' + l;
+
+ var range = 360;
+
+ // Add 60 shades of grey
+ if (c.dataset.grey) {
+ container.className += ' hue-grey';
+ c.min = '-60';
+ range += 60;
+ }
+
+ var updateHandle = () => {
+ var v = parseInt(c.value, 10) || 0;
+
+ if (v > -10 && v < 0) {
+ v = -1; // White
+ }
+ else if (v < -50) {
+ v = -60; // Black
+ }
+
+ if (v >= 0) {
+ var cs = s,
+ cl = l,
+ h = c.value,
+ pos = (c.dataset.grey ? 60 + v : v) / range;
+ }
+ // For grey colors
+ else {
+ var cs = '0%',
+ cl = (Math.abs(v) / 60) * 100 + '%',
+ h = 0,
+ pos = (v + 60) / range;
+ }
+
+ c.value = v;
+ pos *= 100;
+ var hsl = h + ', ' + cs + ', ' + cl;
+
+ handle.style = '--position: ' + pos + '%; --hsl: hsl(' + hsl + ')';
+ };
+
+ c.addEventListener('input', updateHandle);
+ updateHandle();
+
+ c.addEventListener('focus', () => container.classList.add('focus'));
+ c.addEventListener('blur', () => container.classList.remove('focus'));
+
+ input.replaceWith(container);
+ };
+
+ g.enhanceDateField = (input) => {
+ var span = document.createElement('span');
+ span.className = 'datepicker-parent';
+ var btn = document.createElement('button');
+ var cal = null;
+ btn.className = 'datepicker icn-btn';
+ btn.title = 'Cliquer pour ouvrir le calendrier. Utiliser les flèches du clavier pour sélectionner une date, et page précédente suivante pour changer de mois.';
+ btn.setAttribute('data-icon', '📅');
+ btn.type = 'button';
+ btn.onclick = () => {
+ g.script('scripts/lib/datepicker2.min.js', () => {
+ if (null == cal) {
+ btn.onclick = null;
+ cal = new DatePicker(btn, input, {lang: 'fr', format: 1});
+ cal.open();
+ }
+ });
+ };
+ span.appendChild(btn);
+ input.parentNode.insertBefore(span, input.nextSibling);
+
+ const getCaretPosition = e => e && e.selectionStart || -1;
+
+ const inputKeyEvent = (e) => {
+ if (input.value.match(/^\d$|^\d\d?\/\d$/) && e.key.match(/^[0-9]$/)) {
+ input.value += e.key + '/';
+ e.preventDefault();
+ return false;
+ }
+
+ if (e.key == '/' && input.value.slice(-1) == '/') {
+ e.preventDefault();
+ return false;
+ }
+
+ };
+ input.addEventListener('keydown', inputKeyEvent, true);
+ };
+
+ g.current_list_input = null;
+
+ g.inputListSelected = function(value, label) {
+ var i = g.current_list_input;
+
+ if (!i) {
+ throw Error('Parent input list not found');
+ }
+
+ var can_delete = i.firstChild.getAttribute('data-can-delete') == 1;
+ var multiple = i.firstChild.getAttribute('data-multiple') == 1;
+ var name = i.firstChild.getAttribute('data-name');
+
+ var span = document.createElement('span');
+ span.className = 'label';
+
+ var input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = name + '[' + value + ']';
+ input.value = label;
+
+ span.appendChild(input);
+ span.appendChild(document.createTextNode(label));
+
+ // Add delete button
+ if (can_delete == 1) {
+ var btn = document.createElement('button');
+ btn.className = 'icn-btn';
+ btn.type = 'button';
+ btn.setAttribute('data-icon', '✘');
+ btn.onclick = () => span.parentNode.removeChild(span);
+ span.appendChild(btn);
+ }
+
+ if (!multiple && (old = i.querySelector('span'))) {
+ i.removeChild(old);
+ }
+
+ i.appendChild(span);
+ g.closeDialog();
+ i.firstChild.focus();
+ };
+
+ g.formatMoney = (v, html) => {
+ if (!v) {
+ return '0,00';
+ }
+
+ var s = v < 0 ? '-' : '';
+ v = '' + Math.abs(v);
+
+ var units = v.substr(0, v.length-2) || '0';
+
+ if (html) {
+ // Add spacer
+ units = units.split("").reverse().join("");
+ units = units.replace(/(\d{3})/g, "$1\xa0");
+ units = units.split("").reverse().join("").trim("\xa0");
+ }
+
+ return s + units + ',' + ('00' + v).substr(-2);
+ };
+
+ g.getMoneyAsInt = (v) => {
+ v = v.replace(/[^0-9.,-]/, '');
+ if (v.length == 0) return;
+ var m = 1;
+
+ if (v.match(/^-/)) {
+ m = -1;
+ v = v.substr(1);
+ }
+
+ v = v.split(/[,.]/);
+ var d = v.length == 2 ? v[1] : '0';
+ v = v[0] + (d + '00').substr(0, 2);
+ v = parseInt(v, 10) * m;
+ return v;
+ };
+
+ // Focus on first form input when loading the page
+ g.onload(() => {
+ if (!document.activeElement || document.activeElement.tagName.toLowerCase() == 'body') {
+ let form = document.querySelector('form[data-focus]');
+
+ if (!form) {
+ return;
+ }
+
+ let f = form.dataset.focus;
+ let n = f.match(/^\d+$/) ? (parseInt(f, 10) - 1) : null;
+ let i = form.querySelectorAll(n !== null ? '[name]:not([type="hidden"]):not([readonly]):not([type=button])' : f);
+ var element;
+
+ if (n !== null && i[n]) {
+ element = i[n];
+ }
+ else if (n === null && i[0]) {
+ element = i[0];
+ }
+
+ // Don't focus inputs on touch screens as this opens the keyboard
+ if (!element || (element.tagName.toLowerCase() === 'input' && ('ontouchstart' in window))) {
+ return;
+ }
+
+ element.focus();
+ }
+ });
+
+ g.listSelectorButtonClicked = (e) => {
+ var i = e.target;
+ i.setCustomValidity('');
+ g.current_list_input = i.parentNode;
+ var max = i.getAttribute('data-max');
+
+ if (max > 0 && max <= i.parentNode.querySelectorAll('span').length) {
+ alert('Il n\'est pas possible de faire plus de ' + max + ' choix.');
+ return false;
+ }
+
+ let url = i.value + (i.value.indexOf('?') > 0 ? '&' : '?') + '_dialog';
+ var caption = i.dataset.caption || null;
+ g.openFrameDialog(url, {caption});
+ return false;
+ };
+
+ // List selectors, using an iframe for list
+ g.onload(() => {
+ var inputs = $('form .input-list > button');
+
+ inputs.forEach((i) => {
+ i.onclick = g.listSelectorButtonClicked;
+ });
+
+ // Set custom error message if required list is not selected
+ document.querySelectorAll('form').forEach((form) => {
+ let elements = form.elements;
+
+ // Make sure hidden or disabled form elements are not required
+ for (var j = 0; j < elements.length; j++) {
+ var element = elements[j];
+
+ if (element.required && (element.disabled || !element.offsetParent)) {
+ element.dataset.required = element.hasAttribute('required') ? 1 : 0;
+ element.required = false;
+ }
+ }
+
+ form.addEventListener('submit', (e) => {
+ let elements = form.elements;
+
+ // Make sure hidden or disabled form elements are not required
+ for (var j = 0; j < elements.length; j++) {
+ var element = elements[j];
+
+ if (element.disabled || !element.offsetParent) {
+ element.required = false;
+ }
+ }
+
+ let inputs = form.querySelectorAll('.input-list > button[required]');
+
+ for (var k = 0; k < inputs.length; k++) {
+ var i2 = inputs[k];
+
+ // Ignore hidden / disabled form elements
+ if (i2.disabled || !i2.offsetParent) {
+ continue;
+ }
+
+ let v = i2.parentNode.querySelector('input[type="hidden"]:nth-child(1)');
+
+ if (!v || !v.value) {
+ // Force button to have error message, cannot show validity message
+ i2.type = 'submit';
+ i2.setCustomValidity('Merci de faire une sélection.');
+ i2.reportValidity();
+ e.preventDefault();
+ return false;
+ }
+ }
+
+ return true;
+ });
+ });
+
+ var multiples = $('form .input-list span button');
+
+ multiples.forEach((btn) => {
+ btn.onclick = () => btn.parentNode.parentNode.removeChild(btn.parentNode);
+ });
+
+ // Open links in dialog
+ $('a[target*="_dialog"]').forEach((e) => {
+ e.onclick = () => g.openPreview(e);
+ });
+
+ $('form[target="_dialog"]').forEach((e) => {
+ e.addEventListener('submit', () => g.openFormInDialog(e));
+ });
+ });
+
+ g.dialog_events = [];
+ g.setDialogKey = function (key, callback) {
+ g.addDialogEvent('keyup', (e) => {
+ if (e.key !== key) {
+ return;
+ }
+
+ e.preventDefault();
+ callback();
+ return false;
+ });
+
+ if (key === 'ArrowLeft') {
+ g.addDialogEvent('swipeleft', callback);
+ }
+ else if (key === 'ArrowRight') {
+ g.addDialogEvent('swiperight', callback);
+ }
+ };
+
+ g.addDialogEvent = function (event, callback) {
+ if (event !== 'close') {
+ window.addEventListener(event, callback, true);
+ }
+ g.dialog_events.push([event, callback]);
+ };
+
+ g.resetDialogEvents = function () {
+ var e;
+
+ while (e = g.dialog_events.pop()) {
+ if (e[0] === 'close') {
+ continue;
+ }
+
+ window.removeEventListener(e[0], e[1], true);
+ }
+ };
+
+ /**
+ * Open file preview
+ */
+ g.openPreview = function (e) {
+ let type = e.getAttribute('data-mime');
+ let caption = e.getAttribute('data-caption');
+
+ if (!type) {
+ let url = e.href + (e.href.indexOf('?') > 0 ? '&' : '?') + '_dialog';
+
+ if (m = e.getAttribute('target').match(/_dialog=(.*)/)) {
+ url += '=' + m[1];
+ }
+
+ if (location.href.match(/_dialog/)) {
+ location.href = url;
+ return false;
+ }
+
+ g.openFrameDialog(url, {
+ 'height': e.getAttribute('data-dialog-height') || 'auto',
+ 'classname': e.getAttribute('data-dialog-class'),
+ 'on_close': e.getAttribute('data-dialog-on-close') == 1,
+ caption
+ });
+ return false;
+ }
+
+ if (type.match(/^image\//)) {
+ var i = document.createElement('img');
+ i.src = e.href;
+ i.draggable = false;
+ i.onclick = () => g.navigateToPreview(e);
+ }
+ else if (type.match(/^audio\//)) {
+ var i = document.createElement('audio');
+ i.autoplay = true;
+ i.controls = true;
+ i.src = e.href;
+ i.draggable = false;
+ }
+ else if (type.match(/^video\/|^application\/ogg$/)) {
+ var i = document.createElement('video');
+ i.autoplay = true;
+ i.controls = true;
+ i.src = e.href;
+ i.draggable = false;
+ }
+ else {
+ let url = e.href + (e.href.indexOf('?') > 0 ? '&' : '?') + '_dialog';
+ g.openFrameDialog(url, {height: '90%', caption});
+ return false;
+ }
+
+ g.replaceDialog(i, {caption, 'click_to_close': true});
+
+ g.setDialogKey('ArrowLeft', () => g.navigateToPreview(e, true));
+ g.setDialogKey('ArrowRight', () => g.navigateToPreview(e));
+
+ var evt = e => { e.stopPropagation(); e.preventDefault(); g.closeDialog(); };
+ g.addDialogEvent('swipeup', evt);
+ i.addEventListener('swipeup', evt);
+
+ return false;
+ };
+
+ /**
+ * Navigate between elements to preview (eg. images)
+ */
+ g.navigateToPreview = function (element, to_prev) {
+ var preview_items = document.querySelectorAll('a[target="_dialog"][data-mime]');
+ preview_items = Array.from(preview_items).filter((e) => e.dataset.mime.match(/^(audio|video|image)\//));
+ var next = null;
+ var prev = null;
+
+ for (var i = 0; i < preview_items.length; i++) {
+ var item = preview_items[i];
+
+ if (item.href === element.href) {
+ if (to_prev) {
+ next = prev;
+ break;
+ }
+
+ next = false;
+ continue;
+ }
+
+ if (next === false) {
+ next = item;
+ break;
+ }
+
+ prev = item;
+ }
+
+ if (!next) {
+ g.closeDialog();
+ return;
+ }
+
+ g.openPreview(next);
+ return false;
+ };
+
+ g.onload(() => {
+ document.querySelectorAll('input[data-input="date"]').forEach((e) => {
+ g.enhanceDateField(e);
+ });
+
+ document.querySelectorAll('input[data-input="hue"]').forEach((e) => {
+ g.enhanceHueField(e);
+ });
+
+ document.querySelectorAll('input[type="password"]:not([readonly]):not([disabled]):not(.hidden)').forEach((e) => {
+ g.enhancePasswordField(e);
+ });
+
+ // Enhance file inputs to add image preview, paste support, etc.
+ if (document.querySelector('input[type="file"][data-enhanced]')) {
+ g.script('scripts/inputs/file.js');
+ }
+
+ if (document.querySelector('input[list], textarea[list]')) {
+ g.script('scripts/inputs/datalist.js');
+ }
+
+ var dropdown;
+
+ var closeDropdownEvent = (evt) => {
+ if ((evt.type === 'keydown' && evt.key === 'Escape')
+ || (evt.type === 'click' && !dropdown.contains(evt.target))) {
+ closeDropdown();
+ evt.preventDefault();
+ return false;
+ }
+ };
+
+ var closeDropdown = () => {
+ dropdown.classList.remove('open');
+ dropdown.setAttribute('aria-expanded', 'false');
+ dropdown = null;
+ window.removeEventListener('keydown', closeDropdownEvent, {'capture': true});
+ window.removeEventListener('click', closeDropdownEvent, {'capture': true});
+ };
+
+ var openDropdown = (e) => {
+ if (e.classList.contains('open')) {
+ return true;
+ }
+
+ dropdown = e;
+ e.classList.add('open');
+ e.setAttribute('aria-expanded', 'true');
+ window.addEventListener('keydown', closeDropdownEvent, {'capture': true});
+ window.addEventListener('click', closeDropdownEvent, {'capture': true});
+ return false;
+ }
+
+ document.querySelectorAll('nav.dropdown').forEach(e => {
+ e.onclick = () => openDropdown(e);
+ });
+ });
+
+ g.onload(() => {
+ let forms = document.forms;
+
+ if (forms.length != 1) return;
+
+ // Disable progress on search or the form will stay blurred when clicking export buttons
+ if (forms[0].hasAttribute('data-disable-progress')) return;
+
+ forms[0].addEventListener('submit', (e) => {
+ if (e.defaultPrevented) return;
+ forms[0].classList.add('progressing');
+ });
+ });
+
+ // To be able to select a whole table line just by clicking the row
+ g.onload(function () {
+ var tableActions = document.querySelectorAll('form table tfoot .actions select');
+
+ for (var i = 0; i < tableActions.length; i++)
+ {
+ tableActions[i].onchange = function () {
+ if (!this.value) {
+ return;
+ }
+
+ if (!this.form.querySelector('table tbody input[type=checkbox]:checked'))
+ {
+ this.selectedIndex = 0;
+ return !window.alert("Aucune ligne sélectionnée !");
+ }
+
+ var action = this.form.getAttribute('action');
+ var target = this.form.getAttribute('target');
+
+ if (this.hasAttribute('data-form-action')) {
+ this.form.action = this.dataset.formAction;
+ }
+
+ if (this.getAttribute('data-form-target') === '_dialog') {
+ this.form.target = '_dialog'
+ }
+
+ if (this.options[this.selectedIndex].hasAttribute('data-no-dialog')) {
+ this.form.target = '';
+ }
+
+ if (this.form.target === '_dialog') {
+ g.openFormInDialog(this.form);
+ }
+ else {
+ // Not sure if this is required?
+ this.form.dispatchEvent(new Event('submit'));
+ this.form.submit();
+ }
+
+ this.form.action = action;
+ this.form.target = target || '';
+ this.selectedIndex = 0;
+ };
+ }
+
+ // Ajouter action check/uncheck sur les checkbox de raccourci dans les tableaux
+ document.querySelectorAll('table thead input[type=checkbox], table tfoot input[type=checkbox]').forEach((elm) => {
+ elm.addEventListener('change', g.checkUncheck);
+ });
+
+ document.querySelectorAll('table tbody td.check input[type=checkbox]').forEach((elm) => {
+ elm.addEventListener('change', () => {
+ elm.parentNode.parentNode.classList.toggle('checked', elm.checked);
+ });
+ });
+ });
+
+ g.onload(() => g.resizeParentDialog(), false);
+
+ g.onload(() => {
+ // File drag and drop support
+ if ($('[data-upload-url]').length) {
+ g.script('scripts/file_drag.js');
+ }
+ });
+
+ // Add support for accesskeys, even if web browser doesn't support it, or has a weird keys combination
+ window.addEventListener('keydown', (e) => {
+ if (!e.altKey || !e.shiftKey) {
+ return true;
+ }
+
+ if (e.key.length === 1 && (el = document.querySelector('[accesskey="' + e.key + '"]'))) {
+ el.click();
+ return true;
+ }
+
+ // Highlight accesskeys
+ document.body.classList.add('accesskeys');
+ });
+ window.addEventListener('keyup', () => { document.body.classList.remove('accesskeys'); });
+
+ // Implement swipeleft/swiperight events
+ let touch_start_x = 0;
+ let touch_start_y = 0;
+ let touch_start_el = null;
+
+ window.addEventListener('touchstart', e => {
+ touch_start_el = e.target;
+ touch_start_x = e.changedTouches[0].screenX;
+ touch_start_y = e.changedTouches[0].screenY;
+ });
+
+ window.addEventListener('touchend', e => {
+ if (touch_start_el !== e.target) {
+ return;
+ }
+
+ var touch_end_x = e.changedTouches[0].screenX;
+ var touch_end_y = e.changedTouches[0].screenY;
+ var distance_x = touch_end_x - touch_start_x;
+ var distance_y = touch_end_y - touch_start_y;
+ var direction = null;
+
+ if (Math.abs(distance_x) > Math.abs(distance_y)) {
+ if (distance_x < -20) {
+ direction = 'left';
+ }
+ else if (distance_x > 20) {
+ direction = 'right';
+ }
+ }
+ else {
+ if (distance_y < -20) {
+ direction = 'up';
+ }
+ else if (distance_y > 20) {
+ direction = 'down';
+ }
+ }
+
+ if (!direction) {
+ return;
+ }
+
+ touch_start_el.dispatchEvent(new CustomEvent('swipe' + direction, {
+ bubbles: true,
+ cancelable: true,
+ detail: {direction, distance_x, distance_y}
+ }));
+ });
+})();
\ No newline at end of file
diff --git a/index.html b/index.html
index 432d987..fb3d5ed 100644
--- a/index.html
+++ b/index.html
@@ -15,7 +15,7 @@
{{* Reservations *}}
{{if $module.config.resa_active == true}}
{{if !$_GET.fonc}}
- {{:include file="./_inventory.html"}}
+ {{:include file="./_resa_inventory.html" keep="_GET.prop"}}
{{elseif $_GET.fonc=="voir"}}
{{:include file="./_web_calendar.html" title=$page.title|remove_leading_number}}
{{:include file="./_web_view.html" parent=$page.path}}
@@ -27,8 +27,8 @@
{{/if}}
{{elseif $_GET.fonc=="save_resa"}}
{{:include file="./_web_saved_resa.html" parent=$page.path}}
- {{else}}
- {{:include file="./_inventory.html" parent=$page.path keep="cat_key,prop"}}
+ {{*else}}
+ {{:include file="./_inventory.html" parent=$page.path keep="cat_key,prop"*}}
{{/if}}
{{/if}}
{{else}}