UNPKG

cahier-de-bridge

Version:

Gestion d'un éditeur de mains de bridge

1,583 lines (1,477 loc) 55.5 kB
<!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Cahier de Bridge</title> <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" /> <link rel="manifest" href="/images/site.webmanifest" /> <link rel="stylesheet" href="/css/style.css" /> <link rel="stylesheet" href="/css/status.css" /> <link rel="stylesheet" href="/css/tree.css" /> <link rel="stylesheet" href="/css/menu.css" /> <link rel="stylesheet" href="/css/modal.css" /> <link rel="stylesheet" href="/css/hamburger.css" /> <style type="text/css"> body { box-sizing: border-box; margin: 0; padding: 0; } #container { position: absolute; display: flex; flex-direction: column; width: 100%; height: 100%; box-sizing: border-box; } #volets { position: relative; width: 100%; height: 100%; display: flex; flex-direction: row; justify-content: flex-start; box-sizing: border-box; } #volet_centre { position: relative; box-sizing: border-box; display: flex; flex-direction: column; background-color: lightgray; background: linear-gradient(rgba(211, 211, 211, 0.5), rgba(211, 211, 211, 0.5)), url("/images/Cards.png"); } .donne { display: flex; flex-direction: row; background-color: white; } #jtxt { min-height: 50px; width: 100%; padding: 0.5em; background-color: white; border: 1px solid black; border-top: none; } .donne > .cartes { flex-grow: 2; display: grid; grid-template-areas: ". nord . " "ouest centre est" ". sud . "; } .donne > .encheres { display: flex; flex-direction: column; padding: 0.5em; align-items: center; } .NS { display: flex; flex-direction: column; align-items: center; } .nord { justify-content: flex-end; grid-area: nord; margin-bottom: 8px; } .sud { margin-top: 8px; grid-area: sud; } .centre { grid-area: centre; min-width: 60px; min-height: 60px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .OE { display: flex; flex-direction: column; align-self: center; } .est { grid-area: est; align-items: flex-start; } .ouest { grid-area: ouest; align-items: flex-end; } .donne > .encheres table { border: 1px solid black; table-layout: fixed; width: 300px; text-align: center; } .donne > .encheres table th { border-bottom: 1px solid black; } .donne > .encheres table tr { height: 1.2em; } .donne > .encheres table td { vertical-align: middle; } .donne > .encheres table td img { vertical-align: middle; } .donne > .encheres #etxt { width: 100%; height: 100%; border: 1px black solid; padding: 0.5em; } .main { display: flex; flex-direction: row; align-items: center; font-size: 1.5rem; } .main > img { margin-right: 8px; } .vulnerable { border: green 10px solid; width: 50px; height: 50px; } .vulnerable.NSred { border-top-color: red; border-bottom-color: red; } .vulnerable.EWred { border-left-color: red; border-right-color: red; } #sw_edit { appearance: none; } #sw_edit + label { border: 1px darkgray solid; border-radius: 5px; margin-right: 8px; padding-top: 4px; } #sw_edit + label:hover { cursor: pointer; background-color: rgb(190, 190, 190); } #sw_edit:not(:checked) + label img.si_chk { display: none; } #sw_edit:checked + label img.si_unchk { display: none; } [contenteditable="false"] { cursor: default; } [contenteditable="true"] { cursor: text; border: lightgray 1px solid; } h3 { text-align: center; } .d_nord::before { position: relative; content: "▲"; top: -100%; left: 30%; } .d_ouest::before { position: relative; content: "◀"; top: 4px; left: -100%; } .d_est::after { position: relative; content: "▶"; top: 4px; left: 140%; } .d_sud::after { position: relative; content: "▼"; top: 130%; left: 30%; } .titre { align-self: center; padding: 0px 8px; background-color: white; border-top-left-radius: 10px; border-top-right-radius: 10px; border: 1px solid black; border-bottom: none; } .bas { flex-grow: 2; border-bottom: 1px solid black; height: 100%; } #volet_tree { display: flex; flex-direction: column; position: relative; box-sizing: border-box; min-width: 220px; background-color: whitesmoke; z-index: 2; } #volet_tree.hide_me { display: none; } #volet_tree .find { width: 100%; display: flex; flex-direction: row; align-items: center; } #volet_tree .find > input { width: 100%; height: 100%; } #volet_tree .find > img { padding: 4px; } #del_rch { cursor: pointer; } #volet_tree .icones { margin-top: 0.5em; display: flex; flex-direction: row; justify-content: space-between; width: 100%; align-items: center; } #volet_tree #arbre { position: relative; margin-top: 1em; box-sizing: border-box; overflow-y: scroll; height: 100%; } .ed_div { width: 100%; position: relative; display: flex; align-items: center; flex-direction: row; justify-content: space-around; } .cp_btn { margin: 4px; font-weight: bold; font-size: 1.2em; min-width: 24px; } .rch_txt { font-weight: bolder; color: blue; background-color: whitesmoke; border: blue 1px dashed; } #bkp_where div { display: flex; flex-direction: column; } #bkp_where > div > div span { margin: 8px; } #print_this { display: none; } .icn_wrap { position: absolute; display: none; justify-self: flex-start; align-self: flex-start; flex-direction: row; justify-content: flex-start; align-items: center; } .icn_wrap img { margin: 4px; cursor: pointer; } .icn_wrap img:hover { background-color: lightgray; } .cartes:hover .icn_wrap, .encheres:hover .icn_wrap { display: flex; } @media (width <= 1300px) { .main { font-size: 1.25rem; } } @media (width <= 1100px) { .main { font-size: 1rem; } } @media (width <= 890px) { .donne { flex-direction: column-reverse; } .main { font-size: 1rem; } } @media print { body { position: relative; padding: 0; margin: 0; float: none; background-color: white; background-image: none; overflow: visible; } #container { display: none; } #print_this { display: block; } h1 { padding: 0.2em; border-radius: 10px; border: 3px solid black; text-align: center; width: fit-content; align-self: center; margin-bottom: 0.5em; } h3 { border-bottom: 2px dashed blue; text-align: center; width: fit-content; align-self: center; margin-bottom: 0.5em; } .pagebreak { display: flex; flex-direction: column; position: relative; z-index: 2; box-sizing: inherit; clear: both; page-break-after: always; } .donne { flex-direction: column; } } </style> </head> <body> <span id="print_this"></span> <div class="modalBackground" id="sel_donne"> <div class="modalWnd"> <span class="puclose">&times;</span> <!-- Modal Content --> <h1>Sélectionner une donne</h1> <div> <label for="nom_jeu">Nom du modèle: </label> <select id="nom_jeu"></select> </div> <button onclick="CloseModalWnd(); OpenID(nom_jeu.value)">Ouvrir</button> </div> </div> <div class="modalBackground" id="bkp_where"> <div class="modalWnd"> <span class="puclose">&times;</span> <!-- Modal Content --> <h1>Quelle type de restauration ?</h1> <div> <span hlp="ATTENTION ! Tous les utilisateurs seront impactés"> <input type="radio" name="rbt_bkp" value="0" /> Écraser la base existante </span><br /> <span hlp="Préserve la base actuelle"> <input type="radio" name="rbt_bkp" value="1" /> Créer une copie et l'ouvrir </span> <br /> <span id="nom_base" hlp="Saisir le nom de la copie">Nom de la copie: <input type="text" id="ed_nom_bkp" /></span> </div> <button onclick="CloseModalWnd(); OpenBKP()">Restaurer</button> </div> </div> <div class="modalBackground" id="sel_db"> <div class="modalWnd"> <span class="puclose">&times;</span> <!-- Modal Content --> <h1>Sélectionnez la base de donnée à ouvrir</h1> <select id="cbx_db"></select> <button onclick="CloseModalWnd(); openDB()">Ouvrir</button> </div> </div> <div id="container" onclick="closeMenu()"> <div id="volets"> <div id="volet_tree" class="hide_me"> <div class="find"> <img src="/images/Search.png" /> <input type="search" id="ed_find" hlp="Rechercher une donne, un mot dans les textes" /> <img src="/images/cancel_16px.png" id="del_rch" hlp="Effacer le champ de recherche" /> </div> <div class="ed_div"> <input type="checkbox" id="chk_find" /><label for="chk_find">Rechercher seulement<br />dans les noms des jeux</label> </div> <div class="icones si_edit"> <img src="/images/subtree.png" alt="Déplacer cette icone sur l'arbre pour créer des nouveaux dossiers ou sous-dossiers" ondragstart="onSubnodeDrag(event)" /> <img id="node_trash" src="/images/trash_40px.png" draggable="false" alt="Faire glisser ici les dossiers ou jeux à effacer" /> </div> <div id="arbre"></div> </div> <div id="volet_centre"> <div class="menu"> <input type="checkbox" id="tree_sw" /> <label for="tree_sw"> <img src="/images/arrow_d.png" alt="Afficher/Cacher l'arbre de situation" /> </label> <button class="hamburger" type="button" aria-label="Toggle navigation" aria-expanded="false" onclick="toggleNav()"> <span></span> <span></span> <span></span> </button> <div class="menu2"> <div class="level0" style="background-image: url('/images/dossier.png')" hlp="Actions génériques sur une donne (ouvrir, sauver, copier...)"> <div class="level1"> <button class="itmh" id="itm_new"><img src="/images/create_30px.png" alt="Créer une nouvelle donne" />Créer une donne</button> <button class="itmh si_un" alt="Sélectionner une donne existante" id="itm_open"><img src="/images/dossier.png" />Ouvrir une donne</button> <button class="itmh si_dirty" onclick="onSave()" alt="Enregistrer les modifications"><img src="/images/save_30px.png" />Sauver les changements</button> <details hlp="Gestion des données: sauver, restaurer, ajouter, choisir base..."> <summary><img src="/images/database_30px.png" />Base de donnée</summary> <input type="file" style="display: none" id="btn_bkp" /> <a class="hide_me" id="bkp_lnk" href="/public/bridge.db" download>Télécharger</a> <button class="itmh si_admin" onclick="onBkp()" hlp="Sauvegarder la base de donnée dans un backup"><img src="/images/database_export_24px.png" />Sauvegarde</button> <input type="file" class="hide_me" id="get_bkp" /> <button class="itmh si_admin" onclick="onRestore()" hlp="Restaurer la base de donnée depuis une sauvegarde"><img src="/images/database_restore_24px.png" />Restauration</button> <button class="itmh si_admin si_dbs" onclick="OpenModalWnd('sel_db')" hlp="Choisir la base de donnée à ouvrir"><img src="/images/database_view_24px.png" />Sélectionner une base de donnée</button> <input type="file" class="hide_me" id="get_export" /> <button class="itmh" hlp="Exporter donne(s) au format SQL. ctrl-clic pour sélectionner plusieurs" id="itm_export"><img src="/images/export_24px.png" />Exporter donne(s)</button> <input type="file" class="hide_me" id="get_import" /> <label for="get_import" class="itmh si_admin" hlp="Importer des donnes depuis un fichier SQL"> <img src="/images/import_24px.png" />Importer donne(s) </label> </details> <button class="itmh si_open" onclick="PrintThis()" alt="Imprimer"><img src="/images/print_30px.png" />Imprimer cette donne</button> </div> </div> <div class="level0" style="background-image: url('/images/link_30px.png')" hlp="Liens en relations avec ce logiciel (aide, mise à jour..)"> <div class="level1"> <a href="https://github.com/cledou/Cahier-de-Bridge/blob/main/interface.md">Aide à la saisie</a> <a href="https://github.com/cledou/Cahier-de-Bridge/blob/main/install.md">Installation</a> <a href="https://github.com/cledou/Cahier-de-Bridge/discussions">Forum</a> <a href="https://github.com/cledou/Cahier-de-Bridge/issues">Rapport de bugs</a> <a href="https://github.com/cledou/Cahier-de-Bridge/blob/main/assistance.md">Assistance</a> </div> </div> </div> <div style="width: 90%"></div> <!-- repousser menuD à droite--> <div class="flexV" style="margin-right: 10px"> <input id="ed_masquer" type="range" min="1" max="4" hlp="Montrer/cacher commentaires" value="4" style="width: 100px" /> <label style="font-size: 0.7em">Masquer affichage</label> </div> <input type="checkbox" id="sw_edit" /><label for="sw_edit"> <img src="/images/Lock.png" class="si_unchk" hlp="Autoriser les modifications" /> <img src="/images/Unlock.png" class="si_chk" hlp="Interdir les modifications" /></label> <button onclick="PrintThis()"><img src="/images/print_30px.png" alt="Imprimer la donne" /></button> <button class="to_do undo" onclick="Undo()" disabled><img src="/images/undo_30px.png" alt="Revenir en arrière (ctrl-Z)" /></button> <button class="to_do redo" onclick="Redo()" disabled><img src="/images/redo_30px.png" alt="Annuler retour en arrière (shift-ctrl-Z)" /></button> <button class="si_dirty" onclick="onSave()"><img src="/images/save_30px.png" alt="Sauver les modifications" /></button> <button class="si_login hide_me" onclick="window.location.href='/user'"> <img src="/images/add_user_30px.png" alt="Créer un compte utilisateur" /> </button> <button class="si_login hide_me" onclick="window.location.href='/login'"> <img src="/images/close_window_30px.png" hlp="Revenir à l'écran de connexion" /> </button> </div> <h1 class="titre" id="enr_nom" contenteditable="true" style="min-width: 3em; min-height: 1em"></h1> <div class="donne"> <div class="cartes"> <div class="icn_wrap"><img src="images/clipboard_18px.png" hlp="Copier la donne comme image dans le presse-papier" onclick="makeImage('.cartes')" /></div> <div class="NS nord"></div> <div class="OE ouest"></div> <div class="centre"> <div class="vulnerable"></div> </div> <div class="OE est"></div> <div class="NS sud"></div> </div> <div class="encheres"> <div class="icn_wrap"><img src="images/clipboard_18px.png" hlp="Copier l'enchère' comme image dans le presse-papier" onclick="makeImage('.encheres')" /></div> <h3>Enchères</h3> <div class="flexV"> <table id="tbe"></table> <div class="si_edit ed_div"> <div><b>-</b>:Passe</div> <div><b>X</b>:Contre</div> <div><b>XX</b>:Surcontre</div> </div> </div> <div class="si_mask2" id="etxt" contenteditable="true"></div> <div class="ed_div si_mask2"> <div class="flexH"> Entame: <label class="flexH" id="entame" contenteditable="true" style="min-width: 3em; min-height: 1em"></label> </div> <div class="flexH"> Résultat: <label class="flexH" id="score" contenteditable="true" style="min-width: 10em; min-height: 1em"></label> </div> </div> <div class="si_edit ed_div si_mask2"> <div class="flexH"> Vulnérable: <select id="ed_vul"> <option value="-">Aucun</option> <option value="NSEW">Tous</option> <option value="NS">Nord Sud</option> <option value="EW">Est Ouest</option> </select> </div> <div class="flexH"> Copier <button class="cp_btn" onclick="navigator.clipboard.writeText('♠')"><img src="/images/Spades.png" /></button> <button class="cp_btn" onclick="navigator.clipboard.writeText('♥')"><img src="/images/coeur.png" /></button> <button class="cp_btn" onclick="navigator.clipboard.writeText('♦')"><img src="/images/Diamonds.png" /></button> <button class="cp_btn" onclick="navigator.clipboard.writeText('♣')"><img src="/images/Clubs.png" /></button> <button class="cp_btn" onclick="navigator.clipboard.writeText('♠♥♦♣')"><img src="/images/Spades.png" /><img src="/images/coeur.png" /><img src="/images/Diamonds.png" /><img src="/images/Clubs.png" /></button> </div> <div class="flexH"> Donneur: <select id="ed_donneur"> <option value="N">Nord</option> <option value="E">Est</option> <option value="S">Sud</option> <option value="W">Ouest</option> </select> </div> </div> </div> </div> <br /> <div class="flexH si_mask3"> <div class="bas"></div> <h3 class="titre">Jeu de la carte</h3> <div class="bas"></div> </div> <div class="si_mask3" id="jtxt" contenteditable="true"></div> </div> </div> <div id="status"></div> </div> </body> </html> <script src="/socket.io/socket.io.min.js"></script> <script src="/node/html2canvas/dist/html2canvas.min.js"></script> <script src="/js/status.js"></script> <script> /* TODO: voir traduction avec gettext https://github.com/guillaumepotier/gettext.js/ TODO: Undo/Redo */ //********************* // Variables globales //********************* var io = io.connect(location.host); var session = { choix: {} }; var mesChoix; const volet_tree = document.getElementById("volet_tree"); // lié à mesChoix.show_tree const arbre = document.getElementById("arbre"); const node_trash = document.getElementById("node_trash"); const tree_sw = document.getElementById("tree_sw"); const sw_edit = document.getElementById("sw_edit"); const etxt = document.getElementById("etxt"); const jtxt = document.getElementById("jtxt"); const entame = document.getElementById("entame"); const score = document.getElementById("score"); const itm_new = document.getElementById("itm_new"); const enr_nom = document.getElementById("enr_nom"); const print_this = document.getElementById("print_this"); const cbx_db = document.getElementById("cbx_db"); const ed_masquer = document.getElementById("ed_masquer"); const ShowHide = (id, b) => { document.getElementById(id).style.opacity = b ? "1.0" : "0"; }; const BlockIf = (id, b) => { document.getElementById(id).style.display = b ? "block" : "none"; }; const ShowSelector = (sel, b) => { document.querySelectorAll(sel).forEach((el) => (el.style.opacity = b ? "1" : "0")); }; const DisableSelector = (classe, val) => { document.querySelectorAll(classe).forEach((el) => (el.disabled = val)); }; const SetSelectorDisplay = (classe, val) => { document.querySelectorAll(classe).forEach((el) => (el.style.display = val)); }; const SetRbtValue = (name, idx) => { document.querySelectorAll('input[name="' + name + '"]')[idx].checked = true; }; let enr = {}; //********************* // SESSION socket I/O //********************* function SetSessionFlag(f, b) { const foo = b ? mesChoix.flags | f : mesChoix.flags & ~f; if (foo != mesChoix.flags) SetChoix("flags", foo); } // connect -> session -> get_dims -> liste_donnes -> loadDonne OU nouvelle donne io.on("connect", function () { io.emit("session", (data) => { session = data; if (data.erreur != undefined) { Erreur(data.erreur); } if (data.info != undefined) { Info(data.info); } if (!session.user.admin) for (let el of document.getElementsByClassName("si_admin")) el.remove(); mesChoix = session.user.choix; if (mesChoix.flags == undefined) mesChoix.flags = 0; Object.defineProperties(mesChoix, { show_tree: { get: function () { return Boolean(this.flags & 1); }, set: (b) => SetSessionFlag(1, b), }, edit: { get: function () { return Boolean(this.flags & 2); }, set: (b) => SetSessionFlag(2, b), }, chk_find: { get: function () { return Boolean(this.flags & 4); }, set: (b) => SetSessionFlag(4, b), }, rbt_bkp: { get: () => { return Boolean(this.flags & 8); }, set: (b) => SetSessionFlag(8, b), }, }); if (mesChoix.carets == undefined) mesChoix.carets = "XXXXXXXXXXXXX"; document.querySelectorAll(".level1 summary").forEach((caret, idx) => { while (mesChoix.carets.length <= idx) mesChoix.carets += "X"; console.assert(idx < mesChoix.carets.length); caret.addEventListener("click", function () { // En js, string est inmutable let ar = mesChoix.carets.split(""); ar[idx] = ar[idx] == "X" ? "-" : "X"; SetChoix("carets", ar.join("")); }); if (mesChoix.carets[idx] == "X") caret.parentElement.setAttribute("open", true); else caret.parentElement.removeAttribute("open"); }); tree_sw.checked = mesChoix.show_tree; HideVoletGauche(mesChoix.show_tree); chk_find.checked = mesChoix.chk_find; ed_find.value = mesChoix.find || ""; ed_masquer.value = mesChoix.masquer || 4; for (let el of document.getElementsByClassName("si_login")) { if (session.need_login) el.classList.remove("hide_me"); else el.classList.add("hide_me"); } OpenOrNew(); // asynchrone.. }); io.on("info", function (msg) { Info(msg); }); io.on("alert", function (msg) { Erreur(msg); }); io.on("warning", function (msg) { Warning(msg); }); io.onAny((event, p1, p2, p3, p4, p5) => { return; if (p5 != undefined) console.log("IO WEB->", event, p1, p2, p3, p4, p5); else if (p4 != undefined) console.log("IO WEB->", event, p1, p2, p3, p4); else if (p3 != undefined) console.log("IO WEB->", event, p1, p2, p3.toString().substring(0, 20)); else if (p2 != undefined) console.log("IO WEB->", event, p1, p2); else if (p1 != undefined) console.log("IO WEB->", event, p1); else console.log("IO WEB->", event); }); io.on("db_list", (ar) => { SetSelectorDisplay(".si_dbs", ar.length > 1 ? "block" : "none"); let foo = ""; ar.forEach((db) => (foo += "<option>" + db + "</option>")); cbx_db.innerHTML = foo; //cbx_db.value = mesChoix.db }); io.on("reload", () => { window.location.reload(); }); }); // connect //********************* // Let's GO //********************* async function OpenOrNew() { if (!(await OpenID(mesChoix.id_donne)) && !(await OpenOneDonne())) itm_new.click(); } async function OpenOneDonne() { try { const r = await SQL_get("SELECT id FROM donnes LIMIT 1"); DisableSelector(".si_un", r == undefined); return r && OpenID(r.id); } catch (e) { Erreur(e); return false; } } async function OpenID(id) { if (id == undefined) return false; try { const row = await SQL_get("SELECT * FROM donnes WHERE id=" + id); if (row == undefined) return false; const foo = JSON.parse(row.data); VerifierDonne(foo.donne); enr.jeu = foo; enr.nom = row.nom || "Donne n°" + row.id; enr.id = row.id; enr_nom.innerText = enr.nom; AfficheDonne(); SetDirty(false); if (mesChoix.id_donne != row.id) SetChoix("id_donne", row.id); MakeTree(); DisableSelector(".si_un", false); return true; } catch (e) { Erreur(e); return false; } } /****************************/ /* STUFF */ /****************************/ function getEnchereSt(v) { let st = ""; if (v != "") { if (v == "-") st += "Passe"; else if (v == "X") st += "Contre"; else if (v == "XX") st += "Surcontre"; else if (v[0] >= "1" && v[0] <= "7") { st += v[0]; if (v[1] == "P" || v[1] == "♠") st += '<img src="/images/Spades.png" />'; else if (v[1] == "C" || v[1] == "♥") st += '<img src="/images/coeur.png" />'; else if (v[1] == "K" || v[1] == "♦") st += '<img src="/images/Diamonds.png" />'; else if (v[1] == "T" || v[1] == "♣") st += '<img src="/images/Clubs.png" />'; else st += v.substr(1); } else st += v; } return st; } function MakeEncheres(enchere) { let st = "<tr><th>Sud</th><th>Ouest</th><th>Nord</th><th>Est</th></tr>"; for (let i = 0; i < enchere.length; i++) { if (i % 4 == 0) st += "</tr><tr>"; const v = enchere[i]; st += '<td id="enc_' + i + '" onfocus="this.innerText =\'' + v + '\'" contenteditable="' + (mesChoix.edit ? "true" : "false") + '" onblur="blurEnchere(this.innerText,' + i + ',event.relatedTarget.id)" >' + getEnchereSt(v) + "</td>"; } st += "</tr>"; return st; } function blurEnchere(val, idx, next_id) { const foo = val.toUpperCase().trim(); if (foo != enr.jeu.enchere[idx]) { enr.jeu.enchere[idx] = foo; SetDirty(true); } document.getElementById("enc_" + idx).innerHTML = getEnchereSt(foo); if (next_id == "etxt" && val.length > 0) { enr.jeu.enchere.push(" "); document.getElementById("tbe").innerHTML = MakeEncheres(enr.jeu.enchere); document.getElementById("enc_" + (idx + 1)).focus(); } } function GetHTMLmain(img, ar, idx) { return '<div class="main" ><img src="/images/' + img + '.png" /><span style="min-width: 6em" contenteditable="true" oninput="Info(\'\')" onblur="blurMain(' + idx + ',this)">' + AjouteBlancs(ar[idx]) + "</span></div>"; } function MakeDonne(ar, idx) { return GetHTMLmain("Spades", ar, idx++) + GetHTMLmain("coeur", ar, idx++) + GetHTMLmain("Diamonds", ar, idx++) + GetHTMLmain("Clubs", ar, idx++); } function ARDV2N(v) { if (v == "A" || v == "1") return 12; else if (v == "R") return 11; else if (v == "D") return 10; else if (v == "V") return 9; else return Number(v) - 2; } function N2ARDV(n) { if (n == 12) return "A"; if (n == 11) return "R"; if (n == 10) return "D"; if (n == 9) return "V"; return (n + 2).toString(); } var not_done; function VerifierDonne(donne) { // retourne -1 si erreur de distribution, ou nbr cartes manquantes si distribution ok let compte = new Array(52).fill(0); let cnt_NOES = new Array(4).fill(0); not_done = 52; for (let i = 0; i < donne.length; i++) { const NOES = Math.floor(i / 4); const ar = AjouteBlancs(donne[i]).split(" "); for (let j = 0; j < ar.length; j++) if (ar[j].trim().length > 0) { const idx = 13 * (i % 4) + ARDV2N(ar[j]); if (compte[idx] != 0) { Erreur("Doublon: " + ar[j] + "♠♥♦♣"[i % 4]); return false; } compte[idx]++; if (cnt_NOES[NOES] == 13) { const foo = ["Nord", "Ouest", "Est", "Sud"]; Erreur(foo[NOES] + " comporte déjà 13 cartes"); return false; } cnt_NOES[NOES]++; not_done--; } } // distri OK. Complétable ? if (not_done == 13) for (let NOES = 0; NOES < 4; NOES++) if (cnt_NOES[NOES] == 0) { for (let i = 0; i < 4; i++) { let foo = ""; const base = 13 * i; for (let j = 0; j < 13; j++) if (compte[base + j] == 0) foo = N2ARDV(j) + foo; donne[(NOES << 2) + i] = foo; } return true; } return true; } function SetChoix(nom, val) { mesChoix[nom] = val; io.emit("upducfg", nom, val); } function SQL(id, stm, p) { return new Promise((resolve, reject) => { io.emit(id, stm, p, (r) => { // ATTENTION: S'assurer que le serveur retourne bien un objet err si erreur //console.log("SQL", r); if (r != undefined && r.err != undefined) { reject(r.err); } else resolve(r); }); }); } function SQL_all(stm) { return SQL("cb_all", stm); } function SQL_get(stm) { return SQL("cb_get", stm); } function SQL_run(stm, p) { return SQL("cb_run", stm, p); } /****************************/ /* MENUS */ /****************************/ const menu2 = document.querySelector(".menu2"); const level0 = document.querySelectorAll(".level0"); function closeMenu() { level0.forEach((itm) => { itm.classList.remove("open"); }); } level0.forEach((el, idx) => { el.addEventListener("click", (e) => { e.stopPropagation(); // différencier le bouton du menu des sous-menus if (e.target.classList.contains("level0")) { let foo = e.target.classList.contains("open"); level0.forEach((el1) => { el1.classList.remove("open"); }); if (!foo) e.target.classList.add("open"); } }); }); function SetDirty(b) { DisableSelector(".si_dirty", !b); } function HideVoletGauche(b) { if (b) volet_tree.classList.remove("hide_me"); else volet_tree.classList.add("hide_me"); } tree_sw.addEventListener("change", function () { HideVoletGauche(this.checked); mesChoix.show_tree = this.checked; }); function SetElvisArbre(b) { // tree: On ne peut déplacer les noeuds et jeux qu'en mode édition if (!b) removeSelection(); // seuls les <span> jeux ont l'attribut 'draggable'. Les img sont draggable par nature arbre.querySelectorAll("[draggable]").forEach((el) => { el.setAttribute("draggable", b); el.style.cursor = b ? "grab" : "pointer"; }); arbre.querySelectorAll("[contenteditable]").forEach((el) => { el.setAttribute("contenteditable", b); }); SetSelectorDisplay("#arbre .si_edit", b ? "flex" : "none"); } function SetElvisEdit(b) { if (mesChoix.edit != b) SetChoix("edit", b); if (b != sw_edit.checked) sw_edit.checked = b; document.querySelectorAll('[contenteditable="' + (b ? "false" : "true") + '"]').forEach((el) => { el.setAttribute("contenteditable", b); }); SetSelectorDisplay(".si_edit", b ? "flex" : "none"); document.getElementById("tbe").innerHTML = MakeEncheres(enr.jeu.enchere); SetElvisArbre(b); } sw_edit.addEventListener("change", function () { SetElvisEdit(this.checked); }); const itm_open = document.getElementById("itm_open"); nom_jeu = document.getElementById("nom_jeu"); itm_open.addEventListener("click", () => { closeMenu(); GetCbxDonnes() .then((opt) => { nom_jeu.innerHTML = opt; OpenModalWnd("sel_donne"); }) .catch((e) => Erreur(e)); }); function GetSelJeu() { let ar = [enr.id]; if (mesChoix.show_tree) for (let el of all_sel) { const id = Number(el.id.substr(2)); if (id != enr.id && ar.indexOf(id) == -1) ar.push(id); // pour éviter les doublons } return ar; } const bkp_lnk = document.getElementById("bkp_lnk"); const itm_export = document.getElementById("itm_export"); itm_export.addEventListener("click", () => { closeMenu(); io.emit("export", GetSelJeu(), (r) => { if (r.err) Erreur(r.err); else { bkp_lnk.setAttribute("href", r.fn); bkp_lnk.click(); } }); }); const get_import = document.getElementById("get_import"); get_import.addEventListener("change", () => { closeMenu(); Info("Importation de " + get_import.files[0].name + " en cours..."); SendFile(get_import.files[0], "upload", get_import.files[0].name) .then((r) => { io.emit("import", r.fn, (foo) => { if (foo == "OK") { MakeTree(); OK("L'importation s'est bien passée"); } else Erreur(foo); }); }) .catch((e) => Erreur(e)); }); function GetCbxDonnes() { return new Promise((resolve, reject) => { SQL_all("SELECT id,nom FROM donnes ORDER BY nom") .then((rows) => { let opt = ""; rows.forEach((el) => { opt += '<option value="' + el.id + '"'; if (mesChoix.id_donne === el.id) opt += " selected"; opt += ">" + el.nom + "</option>"; }); resolve(opt); }) .catch((e) => reject(e)); }); } nom_jeu.addEventListener("change", function () { CloseModalWnd(); OpenID(this.value); }); function onSave() { io.emit("save_donne", enr, (r) => { if (r.err) Erreur(r); else SetDirty(false); }); } itm_new.addEventListener("click", () => { closeMenu(); io.emit( "save_donne", { jeu: { donneur: "", vul: "", txt1: "", txt2: "", donne: ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], enchere: [" ", " ", " ", " "], entame: " ", score: " ", }, }, (r) => { if (r.err) Erreur(r); else if (r.changes == 1 && OpenID(r.lastInsertRowid)) { SetElvisEdit(true); if ("showPicker" in HTMLSelectElement.prototype) ed_donneur.showPicker(); else ed_donneur.focus(); } } ); }); /****************************/ /* EDITION DE LA DONNE */ /****************************/ const vulnerable = document.querySelector(".cartes .centre .vulnerable"); const classIf = (el, c, b) => { if (b) el.classList.add(c); else el.classList.remove(c); }; function SetVulnerable(val) { classIf(vulnerable, "NSred", val.startsWith("NS")); classIf(vulnerable, "EWred", val.endsWith("EW")); } const ed_vul = document.getElementById("ed_vul"); ed_vul.addEventListener("change", function () { enr.jeu.vul = this.value; SetVulnerable(this.value); SetDirty(true); }); function SetElvisDonneur(val) { classIf(vulnerable, "d_nord", val == "N"); classIf(vulnerable, "d_sud", val == "S"); classIf(vulnerable, "d_est", val == "E"); classIf(vulnerable, "d_ouest", val == "W"); } const ed_donneur = document.getElementById("ed_donneur"); ed_donneur.addEventListener("change", function () { enr.jeu.donneur = this.value; SetElvisDonneur(this.value); SetDirty(true); }); enr_nom.addEventListener("input", function () { enr.nom = this.innerText; SetDirty(true); }); function img2PCKT(txt) { return txt.replace(/<img src="\/images\/Diamonds.png" \/>/g, "&diamondsuit;").replace(/<img src="\/images\/Clubs.png" \/>/g, "&clubs;"); } etxt.addEventListener("focus", function () { this.innerText = enr.jeu.txt1; }); etxt.addEventListener("blur", function () { if (enr.jeu.txt1 != this.innerText) { enr.jeu.txt1 = this.innerText.trim(); SetDirty(true); } etxt.innerHTML = PCKT2img(enr.jeu.txt1); }); jtxt.addEventListener("focus", function () { this.innerText = enr.jeu.txt2; }); jtxt.addEventListener("blur", function () { if (enr.jeu.txt2 != this.innerText) { enr.jeu.txt2 = this.innerText.trim(); SetDirty(true); } jtxt.innerHTML = PCKT2img(enr.jeu.txt2); }); entame.addEventListener("focus", function () { this.innerText = enr.jeu.entame || ""; }); entame.addEventListener("blur", function () { if (enr.jeu.entame != this.innerText) { enr.jeu.entame = this.innerText.trim(); SetDirty(true); } entame.innerHTML = PCKT2img(enr.jeu.entame); }); score.addEventListener("focus", function () { this.innerText = enr.jeu.score || ""; }); score.addEventListener("blur", function () { if (enr.jeu.score != this.innerText) { enr.jeu.score = this.innerText.trim(); SetDirty(true); } score.innerHTML = PCKT2img(enr.jeu.score); }); function AjouteBlancs(val) { // ajouter les espaces let st = ""; if (val != undefined && val.length) { let idx = 0; while (idx < val.length) { const c = val[idx++]; if (c != " ") { if (st != "") st += " "; if (c == "1" && idx < val.length && val[idx] == "0") { st += "10"; idx++; } else st += c; } } } return st; } function RetireBlancs(val) { // retirer les espaces let st = ""; let idx = 0; while (idx < val.length) { const c = val[idx++]; if (c != " ") st += c; } return st; } function blurMain(idx, el) { const st = RetireBlancs(el.innerText.toUpperCase()); if (enr.jeu.donne[idx] != st) { enr.jeu.donne[idx] = st; VerifierDonne(enr.jeu.donne); SetDirty(true); if (not_done == 13) { AfficheDonne(); document.getElementById("enc_0").focus(); return; } } el.innerText = AjouteBlancs(st); } function makeImage(sel) { const foo = document.querySelector(sel); html2canvas(foo, { scrollX: 0, scrollY: -window.scrollY }).then((canvas) => { canvas.toBlob((blob) => { navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]).then(() => OK("Image copiée dans le presse-papier")); }); }); } /****************************/ /* FENETRES POP-UP */ /****************************/ function OpenModalWnd(id) { document.getElementById(id).style.display = "flex"; } function CloseModalWnd() { document.querySelectorAll(".modalBackground").forEach((el) => { el.style.display = "none"; }); } document.querySelectorAll(".modalBackground, .modalWnd > .puclose").forEach((el) => { el.addEventListener("click", (e) => CloseModalWnd()); }); document.querySelectorAll(".modalWnd").forEach((el) => { el.addEventListener("click", (e) => e.stopPropagation()); }); /****************************/ /* ARBRE DE SELECTION */ /****************************/ /* Crédit: https://iamkate.com/code/tree-views/ structure de base d'un noeud simple: <li>noeud</li> structure de base d'un noeud avec enfants: <li> <details> <summary>Nom du noeud</summary> <ul> <li>Enfant 1</li> <li>Enfant 2</li> </ul> </details> </li> les enfants peuvent aussi imbriquer cette structure // ATTENTION: ontoggle se déclenche si 'open' dans le HTML de départ */ function AddJeux(ar, id_node) { let st = ""; ar.forEach((el) => { st += '<li id="j_' + el.id + '" node="' + id_node + '" onclick="onJeuClick(event)" draggable="true" ondragstart="onJeuDrag(event)" hlp="' + (mesChoix.edit ? "Drag: Déplacer. Shift-drag: dupliquer le jeu dans un autre dossier. " : "") + (el.id == enr.id ? "ID:" + el.id + '" class="tree_sel' : "Clic: Ouvrir (ID: " + el.id + ")") + '" >' + (el.nom || "Donne n°" + el.id); //st += '<img class="si_edit" src="/images/Hand_25px.png" id="jeux' + el.id + '" hlp="Sert à faire glisser le jeu vers un autre dossier ou la corbeille"/>'; st += "</li>"; }); return st; } async function renumerote(id, pos) { try { await SQL_run("UPDATE arbre SET pos=? WHERE id=?", [pos, id]); } catch (e) { Erreur(e); } } function AddChilds(ar) { if (ar == undefined) return ""; let st = ""; const ar_max = ar.length - 1; ar.forEach((el, idx) => { if (idx != el.pos) renumerote(el.id, idx); st += '<li><details open><summary ondragstart="onNodeDrag(event)" ondragenter="onDragEnter(this)" ondragleave="onDragLeave(this)" onDrop="DropOnNode(event)" >'; st += '<span contenteditable node="' + el.id + '" onblur="onBlurNode(' + el.id + ',this.innerText)">' + el.itm + "</span>"; st += '<img class="si_edit" src="/images/Hand_25px.png" id="node' + el.id + '" hlp="Sert à faire glisser le dossier vers un autre dossier ou la corbeille (n\'efface pas les jeux)"/>'; if (idx > 0) st += '<img class="si_edit" src="/images/up_25px.png" onclick="UpNode(event,' + el.id + "," + el.pos + ' )" hlp="Remonter ce dossier dans la liste"/>'; if (idx < ar_max) st += '<img class="si_edit" src="/images/down_25px.png" onclick="DownNode(event,' + el.id + "," + el.pos + ')" hlp="Descendre ce dossier dans la liste"/>'; st += "</summary><ul>" + AddChilds(el.childs) + AddJeux(el.jeux, el.id) + "</ul></details>"; }); return st; } async function UpNode(ev, id, pos) { ev.preventDefault(); ev.stopPropagation(); try { await SQL_run("UPDATE arbre SET pos=? WHERE pos=?", [pos, pos - 1]); await SQL_run("UPDATE arbre SET pos=? WHERE id=?", [pos - 1, id]); MakeTree(); } catch (e) { Erreur(e); } } async function DownNode(ev, id, pos) { ev.preventDefault(); ev.stopPropagation(); try { await SQL_run("UPDATE arbre SET pos=? WHERE pos=?", [pos, pos + 1]); await SQL_run("UPDATE arbre SET pos=? WHERE id=?", [pos + 1, id]); MakeTree(); } catch (e) { Erreur(e); } } function SetEventTree() { SetElvisArbre(mesChoix.edit); document.querySelectorAll(".tree [hlp],.tree [alt]").forEach((itm) => { itm.addEventListener("mouseover", (e) => { e.stopPropagation(); Info(itm.getAttribute("hlp") || itm.getAttribute("alt")); }); itm.addEventListener("mouseout", (e) => { Info(""); }); }); } function MakeTree(selector) { if (ed_find.value.trim() != "") MakeRecherche(); else io.emit("get_tree", (r) => { arbre.innerHTML = '<ul class="tree">' + AddChilds(r.childs) + "<li><details open><summary>Non classés</summary><ul>" + AddJeux(r.jeux, 0) + "</ul></details></li></ul>"; SetEventTree(); if (selector != undefined) { const el = arbre.querySelector(selector); el.focus(); window.getSelection().selectAllChildren(el); } }); } const all_sel = arbre.getElementsByClassName("tree_sel"); // en temps réel const removeSelection = () => { for (const el of all_sel) el.classList.remove("tree_sel"); arbre.querySelectorAll("#j_" + enr.id).forEach((el) => el.classList.add("tree_sel")); }; arbre.addEventListener("dragover", (e) => { if (mesChoix.edit) e.preventDefault(); }); node_trash.addEventListener("dragover", (ev) => { ev.preventDefault(); let id = ev.dataTransfer.getData("application/internal"); if (id.startsWith("j_")) Warning("Attention: Vous allez effacer une donne"); }); node_trash.addEventListener("drop", (ev) => { Info(""); ev.preventDefault(); let id = ev.dataTransfer.getData("application/internal"); if (id.startsWith("node")) { SQL_run("DELETE FROM arbre WHERE id=" + id.substr(4)) .then((r) => { // les triggers effacent automatiquement les data2tree et les enfants MakeTree(); }) .catch((e) => Erreur(e)); } else if (id.startsWith("j_")) { SQL_run("DELETE FROM donnes WHERE id=" + id.substr(2)) .then((r) => { // les triggers effacent automatiquement les data2tree et les enfants if (id.substr(2) != enr.id || OpenOrNew()) MakeTree(); }) .catch((e) => Erreur(e)); } }); function onDragEnter(el) { el.style.color = "blue"; el.style.fontWeight = "bold"; } function onDragLeave(el) { el.style.color = "inherit"; el.style.fontWeight = "inherit"; } function onJeuClick(e) { e.stopPropagation(); e.preventDefault(); if (e.ctrlKey == false) { removeSelection(); OpenID(Number(e.target.id.substr(2))); } else e.target.classList.add("tree_sel"); //SelectObjet(e.target.id, e.shiftKey, e.ctrlKey); } function onJeuDrag(ev) { ev.dataTransfer.setData("application/internal", ev.target.id); ev.dataTransfer.setData("text/plain", ev.target.id); ev.dataTransfer.dropEffect = "move"; } // Rappel: node_org=0 si non-classé (jeu sans entrée dans la table data2tree) async function onJeuDrop(id, node_dest, make_copy) { const node_org = Number(document.getElementById(id).getAttribute("node")); if (node_dest == null) node_dest = 0; try { if (node_dest != 0 && (node_org == 0 || make_copy == true)) await SQL_run("INSERT INTO data2tree (id_donne,id_arbre) VALUES (" + id.substr(2) + "," + node_dest + ")"); else if (node_dest == 0) await SQL_run("DELETE FROM data2tree WHERE id_donne=" + id.substr(2) + " AND id_arbre=" + node_org); else await SQL_run("UPDATE data2tree SET id_arbre=" + node_dest + " WHERE id_donne=" + id.substr(2) + " AND id_arbre=" + node_org); MakeTree(); } catch (e) { Erreur(e); } } function onNodeDrag(ev) { ev.dataTransfer.setData("application/internal", ev.target.id); ev.dataTransfer.setData("text/plain", ev.target.id); ev.dataTransfer.dropEffect = "move"; } arbre.addEventListener("drop", (ev) => { Info(""); ev.preventDefault(); let id = ev.dataTransfer.getData("application/internal"); if (id.startsWith("j_")) onJeuDrop(id, ev.target.getAttribute("node"), ev.shiftKey); else if (id.startsWith("node")) unLinkNode(id.substr(4)); else if (id.startsWith("subnode")) AddNode(); }); async function unLinkNode(id) { try { await SQL_run("DELETE FROM arbre WHERE id=" + id); MakeTree(); } catch (e) { Erreur(e); } } async function AddNode() { try { let pos = await SQL_get("SELECT MAX(pos)+1 as n FROM arbre"); let r = await SQL_run("INSERT INTO arbre (itm,pos) VALUES ('Nouveau dossier',?)", [pos.n || 0]); MakeTree('details [node="' + r.lastInsertRowid + '"]'); } catch (e) { Erreur(e); } } async function DropOnNode(ev) { // id est toujours sous la forme 'nodexxx' const node_dest = ev.target.getAttribute("node"); ev.preventDefault(); ev.stopPropagation(); onDragLeave(ev.target.parentElement); const dt = ev.dataTransfer; let foo = dt.getData("application/internal"); try { if (foo.startsWith("j_")) onJeuDrop(foo, node_dest, ev.shiftKey); else if (foo == "subnode") { await SQL_run("INSERT INTO arbre (id_parent,itm) VALUES (" + node_dest + ",'Nouveau dossier')"); MakeTree(); } else if (foo.startsWith("node")) { await SQL_run("UPDATE arbre SET id_parent=" + node_dest + " WHERE id=" + foo.substr(4)); MakeTree(); } } catch (e) { Erreur(e); } } function onSubnodeDrag(ev) { ev.dataTransfer.setData("application/internal", "subnode"); ev.dataTransfer.setData("text/plain", "subnode"); ev.dataTransfer.dropEffect = "copy"; } async function onBlurNode(id_node, txt) { try { // récupérer le nom const id = " WHERE id=" + id_node; r = await SQL_get("SELECT itm FROM arbre" + id); if (r.itm != txt) { await SQL_run("UPDATE arbre SET itm=?" + id, [txt]); } } catch (e) { Erreur(e); } } function PCKT2img(txt) { if (txt == undefined) return ""; const rch = ed_find.value.trim(); if (rch != "") txt = txt.replace(new RegExp(rch, "ig"), '<span class="rch_txt">' + rch + "</span>"); return txt.replace(/♦/g, '<img src="/images/Diamonds.png" />').replace(/♣/g, '<img src="/images/Clubs.png" />').replace(/♠/g, '<img src="/images/Spades.png" />').replace(/♥/g, '<img src="/images/coeur.png" />').replace(/\n/g, "<br />"); } function Masquer(val) { ShowSelector(".encheres", val > 1); ShowSelector(".si_mask2", val > 2); ShowSelector(".si_mask3", val > 3); } ed_masquer.addEventListener("input", function () { Masquer(this.value); SetChoix("masquer", this.value); }); /****************************/ /* RECHERCHER */ /****************************/ const ed_find = document.getElementById("ed_find"); const chk_find = document.getElementById("chk_find"); chk_find.addEventListener("change", function () { mesChoix.chk_find = this.checked; MakeTree(); }); const onRchChg = (val) => { SetChoix("find", val); MakeTree(); }; ed_find.addEventListener("input", function () { onRchChg(this.value); }); document.getElementById("del_rch").addEventListener("click", function () { ed_find.value = ""; onRchChg(""); AfficheTextes(); }); function MakeRecherche() { const like = " LIKE '%" + ed_find.value.replace(/'/g, "''") + "%'"; let stm = "SELECT id,nom FROM donnes WHERE nom" + like; if (!chk_find.checked) stm += " OR data" + like; SQL_all(stm) .then((r) => { arbre.innerHTML = '<ul class="tree">' + "<li><details open><summary>Résultat</summary><ul>" + AddJeux(r, 0) + "</ul></details></li></ul>"; SetEventTree(); if (r.length == 0) Warning("Pas de résultats"); else if (r.findIndex((el) => el.id == mesChoix.id_donne) == -1) OpenID(r[0].id); else AfficheTextes(); }) .catch((e) => Erreur(e)); } /****************************/ /* START */ /****************************/ function AfficheTextes() { entame.innerHTML = PCKT2img(enr.jeu.entame); score.innerHTML = PCKT2img(enr