ADD function dans Util permettant de positionner une boite (div), sera utilisé pour...
[euphorik.git] / js / euphorik.js
1 // coding: utf-8
2 // Copyright 2008 Grégory Burri
3 //
4 // This file is part of Euphorik.
5 //
6 // Euphorik is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // Euphorik is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with Euphorik. If not, see <http://www.gnu.org/licenses/>.
18
19 /**
20 * Contient la base javascript pour le site euphorik.ch.
21 * Chaque page possède son propre fichier js nommé "page<nom de la page>.js".
22 * Auteur : GBurri
23 * Date : 6.11.2007
24 */
25
26
27 /**
28 * La configuration.
29 * Normalement 'const' à la place de 'var' mais non supporté par IE7.
30 */
31 var conf = {
32 nickDefaut : "<nick>",
33 nbMessageAffiche : 40, // (par page)
34 pseudoDefaut : "<nick>",
35 tempsAffichageMessageDialogue : 4000, // en ms
36 tempsKick : 15, // en minute
37 tempsBan : 60 * 24 * 3, // en minutes (3jours)
38 smiles : {
39 "smile" : [/:\)/g, /:-\)/g],
40 "bigsmile" : [/:D/g, /:-D/g],
41 "clin" : [/;\)/g, /;-\)/g],
42 "cool" : [/8\)/g, /8-\)/g],
43 "eheheh" : [/:P/g, /:-P/g],
44 "lol" : [/\[-lol\]/g],
45 "spliff" : [/\[-spliff\]/g],
46 "oh" : [/:o/g, /:O/g],
47 "heink" : [/\[-heink\]/g],
48 "hum" : [/\[-hum\]/g],
49 "boh" : [/\[-boh\]/g],
50 "sniff" : [/:\(/g, /:-\(/g],
51 "triste" : [/\[-triste\]/g],
52 "pascontent" : [/>\(/g, /&gt;\(/g],
53 "argn" : [/\[-argn\]/g],
54 "redface" : [/\[-redface\]/g],
55 "bunny" : [/\[-lapin\]/g],
56 "chat" : [/\[-chat\]/g],
57 "renne" : [/\[-renne\]/g],
58 "star" : [/\[-star\]/g],
59 "kirby" : [/\[-kirby\]/g],
60 "slurp" : [/\[-slurp\]/g],
61 "agreed" : [/\[-agreed\]/g],
62 "dodo" : [/\[-dodo\]/g],
63 "bn" : [/\[-bn\]/g]
64 }
65 }
66
67 ///////////////////////////////////////////////////////////////////////////////////////////////////
68
69 String.prototype.trim = function()
70 {
71 return jQuery.trim(this) // anciennement : this.replace(/^\s+|\s+$/g, "");
72 }
73
74 String.prototype.ltrim = function()
75 {
76 return this.replace(/^\s+/, "");
77 }
78
79 String.prototype.rtrim = function()
80 {
81 return this.replace(/\s+$/, "");
82 }
83
84 ///////////////////////////////////////////////////////////////////////////////////////////////////
85
86 /**
87 * Cette classe regroupe des fonctions utilitaires (helpers).
88 * @formateur est permet de formater les messages affichés à l'aide de messageDialogue (facultatif)
89 */
90 function Util(formateur)
91 {
92 $("#info .fermer").click(function(){
93 $("#info").slideUp(50)
94 })
95
96 $("body").append('<div id="flecheBulle"></div>').append('<div id="messageBulle"><p></p></div>')
97
98 this.formateur = formateur
99 this.bulleActive = true
100 }
101
102 var messageType = {informatif: 0, question: 1, erreur: 2}
103
104 /**
105 * Affiche une boite de dialogue avec un message à l'intérieur.
106 * @param message le message (string)
107 * @param type voir 'messageType'. par défaut messageType.informatif
108 * @param les boutons sous la forme d'un objet ou les clefs sont les labels des boutons
109 * et les valeurs les fonctions executées lorsqu'un bouton est activé.
110 * @param formate faut-il formaté le message ? true par défaut
111 */
112 Util.prototype.messageDialogue = function(message, type, boutons, formate)
113 {
114 var thisUtil = this
115
116 if (type == undefined)
117 type = messageType.informatif
118
119 if (formate == undefined)
120 formate = true
121
122 if (this.timeoutMessageDialogue != undefined)
123 clearTimeout(this.timeoutMessageDialogue)
124
125 var fermer = function(){$("#info").slideUp(100)}
126 fermer()
127
128 $("#info .message").html(thisUtil.formateur == undefined || !formate ? message : thisUtil.formateur.traitementComplet(message))
129 switch(type)
130 {
131 case messageType.informatif : $("#info #icone").attr("class", "information"); break
132 case messageType.question : $("#info #icone").attr("class", "interrogation"); break
133 case messageType.erreur : $("#info #icone").attr("class", "exclamation"); break
134 }
135 $("#info .boutons").html("")
136 for (var b in boutons)
137 $("#info .boutons").append("<div>" + b + "</div>").find("div:last").click(boutons[b]).click(fermer)
138
139 $("#info").slideDown(200)
140 this.timeoutMessageDialogue = setTimeout(fermer, conf.tempsAffichageMessageDialogue)
141 }
142
143 var positionTypeX = {gauche: 0, gaucheRecouvrement: 1, centre: 2, droiteRecouvrement: 3, droite: 4}
144 var positionTypeY = {haut: 0, hautRecouvrement: 1, centre: 2, basRecouvrement: 3, bas: 4}
145
146 /**
147 * Afficher une boite flottante (élément) par rapport à une cible.
148 * La boite est affichée de manière à ne pas dépasser de la fenêtre.
149 * @boite l'élément à afficher
150 * @cible l'élément sur lequel est affiché la boite
151 * @positionX de type positionTypeX
152 * @positionY de type positionTypeY
153 */
154 Util.prototype.afficherBoite = function(boite, cible, positionX, positionY)
155 {
156 var positionCible = cible.offset()
157 var positionBoite =
158 {
159 left : positionX == positionTypeX.gauche ? positionCible.left - boite.width() :
160 (positionX == positionTypeX.gaucheRecouvrement ? positionCible.left - boite.width() + cible.width() :
161 (positionX == positionTypeX.droitelsRecouvrement ? positionCible.left :
162 (positionX == positionTypeX.droite ? positionCible.left + cible.width() :
163 positionCible.left + cible.width() / 2 - boite.width() / 2 ))), // centre
164 top : positionY == positionTypeY.haut ? positionCible.top - boite.height() :
165 (positionY == positionTypeY.hautRecouvrement ? positionCible.top - boite.height() + cible.height() :
166 (positionY == positionTypeY.basRecouvrement ? positionCible.top :
167 (positionY == positionTypeY.bas ? positionCible.top + cible.height() :
168 positionCible.top + cible.height() / 2 - boite.height() / 2 ))) // centre
169 }
170
171 // calcul les décalages en x et en y pour éviter que la boite ne sorte de la fenêtre, tient compte de la position des barres de défilement
172 var marge = 10
173 positionBoite.left = positionBoite.left < marge + window.pageXOffset ? marge + window.pageXOffset :
174 (boite.width() - $(window).width() + (positionBoite.left - window.pageXOffset) + marge > 0 ? $(window).width() - boite.width() - marge + window.pageXOffset : positionBoite.left)
175 positionBoite.top = positionBoite.top < marge + window.pageYOffset ? marge + window.pageYOffset :
176 (boite.height() - $(window).height() + (positionBoite.top - window.pageYOffset) + marge > 0 ? $(window).height() - boite.height() - marge + window.pageYOffset : positionBoite.top)
177
178 boite.css("top", positionBoite.top).css("left", positionBoite.left).show()
179 }
180
181 /**
182 * Affiche un info bulle lorsque le curseur survole l'élément donné.
183 * FIXME : le width de element ne tient pas compte du padding !?
184 */
185 Util.prototype.infoBulle = function(message, element)
186 {
187 var thisUtil = this
188
189 var cacherBulle = function()
190 {
191 $("#flecheBulle").hide()
192 $("#messageBulle").hide()
193 }
194
195 element.hover(
196 function(e)
197 {
198 if (!thisUtil.bulleActive)
199 return
200
201 var m = $("#messageBulle")
202 var f = $("#flecheBulle")
203
204 // remplie le paragraphe de la bulle avec le message
205 $("p", m).html(message)
206
207 // réinitialise la position, évite le cas ou la boite est collé à droite et remplie avec un texte la faisant dépassé
208 // dans ce cas la hauteur n'est pas calculé correctement
209 m.css("top", 0).css("left", 0)
210
211 var positionFleche = {
212 left : element.offset().left + element.width() / 2 - f.width() / 2,
213 top : element.offset().top - f.height()
214 }
215 var positionMessage = {
216 left : element.offset().left + element.width() / 2 - m.width() / 2,
217 top : element.offset().top - f.height() - m.height()
218 }
219 var depassementDroit = (positionMessage.left + m.width()) - $("body").width()
220 if (depassementDroit > 0)
221 positionMessage.left -= depassementDroit
222 else
223 {
224 if (positionMessage.left < 0)
225 positionMessage.left = 0
226 }
227
228 m.css("top", positionMessage.top).css("left", positionMessage.left).show()
229 f.css("top", positionFleche.top).css("left", positionFleche.left).show()
230 },
231 cacherBulle
232 ).click(cacherBulle)
233 }
234
235 /**
236 * Utilisé pour l'envoie de donnée avec la méthode ajax de jQuery.
237 */
238 Util.prototype.jsonVersAction = function(json)
239 {
240 return {action : JSON.stringify(json) }
241 }
242
243 Util.prototype.md5 = function(chaine)
244 {
245 return hex_md5(chaine)
246 }
247
248 // pompé de http://www.faqts.com/knowledge_base/view.phtml/aid/13562/fid/130
249 Util.prototype.setSelectionRange = function(input, selectionStart, selectionEnd)
250 {
251 if (input.setSelectionRange)
252 {
253 input.focus()
254 input.setSelectionRange(selectionStart, selectionEnd)
255 }
256 else if (input.createTextRange)
257 {
258 var range = input.createTextRange()
259 range.collapse(true)
260 range.moveEnd('character', selectionEnd)
261 range.moveStart('character', selectionStart)
262 range.select()
263 }
264 }
265
266 Util.prototype.setCaretToEnd = function(input)
267 {
268 this.setSelectionRange(input, input.value.length, input.value.length)
269 }
270 Util.prototype.setCaretToBegin = function(input)
271 {
272 this.setSelectionRange(input, 0, 0)
273 }
274 Util.prototype.setCaretToPos = function(input, pos)
275 {
276 this.setSelectionRange(input, pos, pos)
277 }
278 Util.prototype.selectString = function(input, string)
279 {
280 var match = new RegExp(string, "i").exec(input.value)
281 if (match)
282 {
283 this.setSelectionRange (input, match.index, match.index + match[0].length)
284 }
285 }
286 Util.prototype.replaceSelection = function(input, replaceString) {
287 if (input.setSelectionRange)
288 {
289 var selectionStart = input.selectionStart
290 var selectionEnd = input.selectionEnd
291 input.value = input.value.substring(0, selectionStart) + replaceString + input.value.substring(selectionEnd)
292
293 if (selectionStart != selectionEnd) // has there been a selection
294 this.setSelectionRange(input, selectionStart, selectionStart + replaceString.length)
295 else // set caret
296 this.setCaretToPos(input, selectionStart + replaceString.length)
297 }
298 else if (document.selection)
299 {
300 input.focus()
301 var range = document.selection.createRange()
302 if (range.parentElement() == input)
303 {
304 var isCollapsed = range.text == ''
305 range.text = replaceString
306 if (!isCollapsed)
307 {
308 range.moveStart('character', -replaceString.length);
309 }
310 }
311 }
312 }
313
314 Util.prototype.rot13 = function(chaine)
315 {
316 var ACode = 'A'.charCodeAt(0)
317 var aCode = 'a'.charCodeAt(0)
318 var MCode = 'M'.charCodeAt(0)
319 var mCode = 'm'.charCodeAt(0)
320 var ZCode = 'Z'.charCodeAt(0)
321 var zCode = 'z'.charCodeAt(0)
322
323 var f = function(ch, pos) {
324 if (pos == ch.length)
325 return ""
326
327 var c = ch.charCodeAt(pos);
328 return String.fromCharCode(
329 c +
330 (c >= ACode && c <= MCode || c >= aCode && c <= mCode ? 13 :
331 (c > MCode && c <= ZCode || c > mCode && c <= zCode ? -13 : 0))
332 ) + f(ch, pos + 1)
333 }
334 return f(chaine, 0)
335 }
336
337 ///////////////////////////////////////////////////////////////////////////////////////////////////
338
339 function Pages()
340 {
341 this.pageCourante = null
342 this.pages = {}
343 }
344
345 /**
346 * Accepte soit un objet soit un string.
347 * un string correspond au nom de la page, par exemple : "page" -> "page.html"
348 */
349 Pages.prototype.ajouterPage = function(page)
350 {
351 if (typeof page == "string")
352 {
353 this.pages[page] = page
354 }
355 else
356 {
357 page.pages = this // la magie des langages dynamiques : le foutoire
358 this.pages[page.nom] = page
359 }
360 }
361
362 Pages.prototype.afficherPage = function(nomPage, forcerChargement)
363 {
364 if (forcerChargement == undefined) forcerChargement = false
365
366 var page = this.pages[nomPage]
367 if (page == undefined || (!forcerChargement && page == this.pageCourante)) return
368
369 if (this.pageCourante != null && this.pageCourante.decharger)
370 this.pageCourante.decharger()
371
372 $("#menu li").removeClass("courante")
373 $("#menu li." + nomPage).addClass("courante")
374
375 this.pageCourante = page
376 var contenu = ""
377 if (typeof page == "string")
378 $.ajax({async: false, url: "pages/" + page + ".html", success : function(page) { contenu += page }})
379 else
380 contenu += this.pageCourante.contenu()
381 $("#page").html(contenu).removeClass().addClass(this.pageCourante.nom)
382
383 if (this.pageCourante.charger)
384 this.pageCourante.charger()
385 }
386
387 ///////////////////////////////////////////////////////////////////////////////////////////////////
388
389 /**
390 * Classe permettant de formater du texte par exemple pour la substitution des liens dans les
391 * message par "[url]".
392 * TODO : améliorer l'efficacité des méthods notamment lié au smiles.
393 */
394 function Formateur()
395 {
396 this.smiles = conf.smiles
397 this.protocoles = "http|https|ed2k"
398
399 this.regexUrl = new RegExp("(?:(?:" + this.protocoles + ")://|www\\.)[^ ]*", "gi")
400 this.regexImg = new RegExp("^.*?\\.(gif|jpg|png|jpeg|bmp|tiff)$", "i")
401 this.regexDomaine = new RegExp("^(?:(?:" + this.protocoles + ")://|www\\.).*?([^/.]+\\.[^/.]+)(?:$|/).*$", "i")
402 this.regexTestProtocoleExiste = new RegExp("^(?:" + this.protocoles + ")://.*$", "i")
403 this.regexNomProtocole = new RegExp("^(.*?)://")
404 }
405
406 /**
407 * Formate un pseudo saise par l'utilisateur.
408 * @param pseudo le pseudo brut
409 * @return le pseudo filtré
410 */
411 Formateur.prototype.filtrerInputPseudo = function(pseudo)
412 {
413 return pseudo.replace(/{|}/g, "").trim()
414 }
415
416 Formateur.prototype.getSmilesHTML = function()
417 {
418 var XHTML = ""
419 for (var sNom in this.smiles)
420 {
421 XHTML += "<img class=\"" + sNom + "\" src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />"
422 }
423 return XHTML
424 }
425
426 /**
427 * Formatage complet d'un texte.
428 * @M le message
429 * @pseudo facultatif, permet de contruire le label des images sous la forme : "<Pseudo> : <Message>"
430 */
431 Formateur.prototype.traitementComplet = function(M, pseudo)
432 {
433 return this.traiterLiensConv(this.traiterSmiles(this.traiterURL(this.traiterWikiSyntaxe(this.remplacerBalisesHTML(M)), pseudo)))
434 }
435
436 /**
437 * Transforme les liens en entités clickables.
438 * Un lien vers une conversation permet d'ouvrire celle ci, elle se marque comme ceci dans un message :
439 * "{5F}" ou 5F est la racine de la conversation.
440 * Ce lien sera transformer en <span class="lienConv">{5F}</span> pouvant être clické pour créer la conv 5F.
441 */
442 Formateur.prototype.traiterLiensConv = function(M)
443 {
444 return M.replace(
445 /\{\w+\}/g,
446 function(lien)
447 {
448 return "<span class=\"lienConv\">" + lien + "</span>"
449 }
450 )
451 }
452
453 /**
454 * FIXME : Cette méthode est attrocement lourde ! A optimiser.
455 * moyenne sur échantillon : 234ms
456 */
457 Formateur.prototype.traiterSmiles = function(M)
458 {
459 for (var sNom in this.smiles)
460 {
461 ss = this.smiles[sNom]
462 for (var i = 0; i < ss.length; i++)
463 M = M.replace(ss[i], "<img src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />")
464 }
465 return M
466 }
467
468 Formateur.prototype.remplacerBalisesHTML = function(M)
469 {
470 return M.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
471 }
472
473 Formateur.prototype.traiterURL = function(M, pseudo)
474 {
475 thisFormateur = this
476
477 var traitementUrl = function(url)
478 {
479 // si ya pas de protocole on rajoute "http://"
480 if (!thisFormateur.regexTestProtocoleExiste.test(url))
481 url = "http://" + url
482 var extension = thisFormateur.getShort(url)
483 return "<a " + (extension[1] ? "title=\"" + (pseudo == undefined ? "" : thisFormateur.traiterPourFenetreLightBox(pseudo, url) + ": ") + thisFormateur.traiterPourFenetreLightBox(M, url) + "\"" + " rel=\"lightbox\"" : "") + " href=\"" + url + "\" >[" + extension[0] + "]</a>"
484 }
485 return M.replace(this.regexUrl, traitementUrl)
486 }
487
488 /**
489 * Formatage en utilisant un sous-ensemble des règles de mediwiki.
490 * par exemple ''italic'' devient <i>italic</i>
491 */
492 Formateur.prototype.traiterWikiSyntaxe = function(M)
493 {
494 return M.replace(
495 /'''(.*?)'''/g,
496 function(texte, capture)
497 {
498 return "<b>" + capture + "</b>"
499 }
500 ).replace(
501 /''(.*?)''/g,
502 function(texte, capture)
503 {
504 return "<i>" + capture + "</i>"
505 }
506 )
507 }
508
509 /**
510 * Renvoie une version courte de l'url.
511 * par exemple : http://en.wikipedia.org/wiki/Yakov_Smirnoff devient wikipedia.org
512 */
513 Formateur.prototype.getShort = function(url)
514 {
515 var estUneImage = false
516 var versionShort = null
517 var rechercheImg = this.regexImg.exec(url)
518
519 if (rechercheImg != null)
520 {
521 versionShort = rechercheImg[1].toLowerCase()
522 if (versionShort == "jpeg") versionShort = "jpg" // jpeg -> jpg
523 estUneImage = true
524 }
525 else
526 {
527 var rechercheDomaine = this.regexDomaine.exec(url)
528 if (rechercheDomaine != null && rechercheDomaine.length >= 2)
529 versionShort = rechercheDomaine[1]
530 else
531 {
532 var nomProtocole = this.regexNomProtocole.exec(url)
533 if (nomProtocole != null && nomProtocole.length >= 2)
534 versionShort = nomProtocole[1]
535 }
536 }
537
538 return [versionShort == null ? "url" : versionShort, estUneImage]
539 }
540
541 /**
542 * Traite les pseudo et messages à être affiché dans le titre d'une image visualisé avec lightbox.
543 */
544 Formateur.prototype.traiterPourFenetreLightBox = function(M, urlCourante)
545 {
546 thisFormateur = this
547 var traitementUrl = function(url)
548 {
549 return "[" + thisFormateur.getShort(url)[0] + (urlCourante == url ? "*" : "") + "]"
550 }
551
552 return this.remplacerBalisesHTML(M).replace(this.regexUrl, traitementUrl)
553 }
554
555
556 ///////////////////////////////////////////////////////////////////////////////////////////////////
557
558 // les statuts possibes du client
559 var statutType = {
560 // mode enregistré, peut poster des messages et modifier son profile
561 auth_registered : 0,
562 // mode identifié, peut poster des messages mais n'a pas accès au profile
563 auth_not_registered : 1,
564 // mode déconnecté, ne peut pas poster de message
565 deconnected : 2
566 }
567
568 function Client(util)
569 {
570 this.util = util
571
572 this.cookie = null
573 this.regexCookie = new RegExp("^cookie=([^;]*)")
574
575 // données personnels
576 this.resetDonneesPersonnelles()
577
578 this.setStatut(statutType.deconnected)
579
580 // si true alors chaque modification du client est mémorisé sur le serveur
581 this.autoflush = $.browser["opera"]
582 }
583
584 Client.prototype.resetDonneesPersonnelles = function()
585 {
586 this.id = 0
587 this.pseudo = conf.pseudoDefaut
588 this.login = ""
589 this.password = ""
590 this.email = ""
591 this.css = $("link#cssPrincipale").attr("href")
592 this.nickFormat = "nick"
593 this.viewTimes = true
594 this.viewTooltips = true
595 this.cookie = undefined
596
597 this.pagePrincipale = 1
598 this.ekMaster = false
599
600 // les conversations, une conversation est un objet possédant les attributs suivants :
601 // - root (entier)
602 // - page (entier)
603 this.conversations = new Array()
604 }
605
606 Client.prototype.setCss = function(css)
607 {
608 if (this.css == css || css == "")
609 return
610
611 this.css = css
612 $("link#cssPrincipale").attr("href", this.css)
613 if (this.autoflush) this.flush(true)
614 }
615
616 Client.prototype.pageSuivante = function(numConv)
617 {
618 if (numConv < 0 && this.pagePrincipale > 1)
619 this.pagePrincipale -= 1
620 else if (this.conversations[numConv].page > 1)
621 this.conversations[numConv].page -= 1
622 }
623
624 Client.prototype.pagePrecedente = function(numConv)
625 {
626 if (numConv < 0)
627 this.pagePrincipale += 1
628 else
629 this.conversations[numConv].page += 1
630 }
631
632 /**
633 * Définit la première page pour la conversation donnée.
634 * @return true si la page a changé sinon false
635 */
636 Client.prototype.goPremierePage = function(numConv)
637 {
638 if (numConv < 0)
639 {
640 if (this.pagePrincipale == 1)
641 return false
642 this.pagePrincipale = 1
643 }
644 else
645 {
646 if (this.conversations[numConv].page == 1)
647 return false
648 this.conversations[numConv].page = 1
649 }
650 return true
651 }
652
653 /**
654 * Ajoute une conversation à la vue de l'utilisateur.
655 * Le profile de l'utilisateur est directement sauvegardé sur le serveur.
656 * @param racines la racine de la conversation (integer)
657 * @return true si la conversation a été créée sinon false (par exemple si la conv existe déjà)
658 */
659 Client.prototype.ajouterConversation = function(racine)
660 {
661 // vérification s'il elle n'existe pas déjà
662 for (var i = 0; i < this.conversations.length; i++)
663 if (this.conversations[i].root == racine)
664 return false
665
666 this.conversations.push({root : racine, page : 1})
667 if (this.autoflush) this.flush(true)
668
669 return true
670 }
671
672 Client.prototype.supprimerConversation = function(num)
673 {
674 if (num < 0 || num >= this.conversations.length) return
675
676 // décalage TODO : supprimer le dernier élément
677 for (var i = num; i < this.conversations.length - 1; i++)
678 this.conversations[i] = this.conversations[i+1]
679 this.conversations.pop()
680
681 if (this.autoflush) this.flush(true)
682 }
683
684 Client.prototype.getJSONLogin = function(login, password)
685 {
686 return {
687 "action" : "authentification",
688 "login" : login,
689 "password" : password
690 }
691 }
692
693 Client.prototype.getJSONLoginCookie = function()
694 {
695 return {
696 "action" : "authentification",
697 "cookie" : this.cookie
698 }
699 }
700
701 /**
702 * le couple (login, password) est facultatif. S'il n'est pas fournit alors il ne sera pas possible
703 * de s'autentifier avec (login, password).
704 */
705 Client.prototype.getJSONEnregistrement = function(login, password)
706 {
707 var mess = { "action" : "register" }
708
709 if (login != undefined && password != undefined)
710 {
711 mess["login"] = login
712 mess["password"] = password
713 }
714
715 return mess;
716 }
717
718 Client.prototype.getJSONConversations = function()
719 {
720 var conversations = new Array()
721 for (var i = 0; i < this.conversations.length; i++)
722 conversations.push(this.conversations[i].root)
723 return conversations
724 }
725
726 Client.prototype.getJSONProfile = function()
727 {
728 return {
729 "action" : "set_profile",
730 "cookie" : this.cookie,
731 "login" : this.login,
732 "password" : this.password,
733 "nick" : this.pseudo,
734 "email" : this.email,
735 "css" : this.css,
736 "nick_format" : this.nickFormat,
737 "view_times" : this.viewTimes,
738 "view_tooltips" : this.viewTooltips,
739 "conversations" : this.getJSONConversations()
740 }
741 }
742
743 /**
744 * Renvoie null si pas définit.
745 */
746 Client.prototype.getCookie = function()
747 {
748 var cookie = this.regexCookie.exec(document.cookie)
749 if (cookie == null) this.cookie = null
750 else this.cookie = cookie[1]
751 }
752
753 Client.prototype.delCookie = function()
754 {
755 document.cookie = "cookie=; max-age=0"
756 }
757
758 Client.prototype.setCookie = function()
759 {
760 if (this.cookie == null || this.cookie == undefined)
761 return
762
763 // ne fonctionne pas sous IE....
764 /*document.cookie = "cookie=" + this.cookie + "; max-age=" + (60 * 60 * 24 * 365) */
765
766 document.cookie =
767 "cookie="+this.cookie+"; expires=" + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365).toUTCString()
768 }
769
770 Client.prototype.authentifie = function()
771 {
772 return this.statut == statutType.auth_registered || this.statut == statutType.auth_not_registered
773 }
774
775 Client.prototype.setStatut = function(statut)
776 {
777 // conversation en "enum" si en "string"
778 if (typeof(statut) == "string")
779 {
780 statut =
781 statut == "auth_registered" ?
782 statutType.auth_registered :
783 (statut == "auth_not_registered" ? statutType.auth_not_registered : statutType.deconnected)
784 }
785
786 if (statut == this.statut) return
787
788 this.statut = statut
789 this.majMenu()
790 }
791
792 /**
793 * Effectue la connexion vers le serveur.
794 * Cette fonction est bloquante tant que la connexion n'a pas été établie.
795 * S'il existe un cookie en local on s'authentifie directement avec lui.
796 * Si il n'est pas possible de s'authentifier alors on affiche un captcha anti-bot.
797 */
798 Client.prototype.connexionCookie = function()
799 {
800 this.getCookie()
801 if (this.cookie == null) return false;
802 return this.connexion(this.getJSONLoginCookie())
803 }
804
805 Client.prototype.connexionLogin = function(login, password)
806 {
807 return this.connexion(this.getJSONLogin(login, password))
808 }
809
810 Client.prototype.enregistrement = function(login, password)
811 {
812 if (this.authentifie())
813 {
814 this.login = login
815 this.password = password
816 if(this.flush())
817 {
818 this.setStatut(statutType.auth_registered)
819 return true
820 }
821 return false
822 }
823 else
824 {
825 return this.connexion(this.getJSONEnregistrement(login, password))
826 }
827 }
828
829 Client.prototype.connexion = function(messageJson)
830 {
831 ;; dumpObj(messageJson)
832 thisClient = this
833 jQuery.ajax(
834 {
835 async: false,
836 type: "POST",
837 url: "request",
838 dataType: "json",
839 data: this.util.jsonVersAction(messageJson),
840 success:
841 function(data)
842 {
843 ;; dumpObj(data)
844 if (data["reply"] == "error")
845 thisClient.util.messageDialogue(data["error_message"])
846 else
847 thisClient.chargerDonnees(data)
848 }
849 }
850 )
851 return this.authentifie()
852 }
853
854 Client.prototype.deconnexion = function()
855 {
856 this.flush(true)
857 this.delCookie()
858 this.resetDonneesPersonnelles()
859 this.setStatut(statutType.deconnected) // deconnexion
860 }
861
862 Client.prototype.chargerDonnees = function(data)
863 {
864 // la modification du statut qui suit met à jour le menu, le menu dépend (page admin)
865 // de l'état ekMaster
866 this.ekMaster = data["ek_master"] != undefined ? data["ek_master"] : false
867
868 this.setStatut(data["status"])
869
870 if (this.authentifie())
871 {
872 this.cookie = data["cookie"]
873 this.setCookie()
874
875 this.id = data["id"]
876 this.login = data["login"]
877 this.pseudo = data["nick"]
878 this.email = data["email"]
879 this.setCss(data["css"])
880 this.nickFormat = data["nick_format"]
881 this.viewTimes = data["view_times"]
882 this.viewTooltips = data["view_tooltips"]
883
884 // la page de la conversation principale
885 this.pagePrincipale = 1
886
887 // les conversations
888 this.conversations = data["conversations"]
889 for (var i = 0; i < this.conversations.length; i++)
890 this.conversations[i] = {root : this.conversations[i], page : 1}
891
892 this.majBulle()
893 this.majCssSelectionee()
894 }
895 }
896
897 /**
898 * Met à jour les données personne sur serveur.
899 * @param async de manière asynchrone ? défaut = true
900 * @return false si le flush n'a pas pû se faire sinon true
901 */
902 Client.prototype.flush = function(async)
903 {
904 if (async == undefined)
905 async = false
906
907 if (!this.authentifie())
908 return false
909
910 var thisClient = this
911 var ok = true
912
913 ;; dumpObj(this.getJSONProfile())
914 jQuery.ajax(
915 {
916 async: async,
917 type: "POST",
918 url: "request",
919 dataType: "json",
920 data: this.util.jsonVersAction(this.getJSONProfile()),
921 success:
922 function(data)
923 {
924 ;; dumpObj(data)
925 if (data["reply"] == "error")
926 {
927 thisClient.util.messageDialogue(data["error_message"])
928 ok = false
929 }
930 else
931 {
932 thisClient.majBulle()
933 }
934 }
935 }
936 )
937
938 return ok
939 }
940
941 Client.prototype.majMenu = function()
942 {
943 displayType = "block"
944
945 $("#menu .admin").css("display", this.ekMaster ? displayType : "none")
946
947 // met à jour le menu
948 if (this.statut == statutType.auth_registered)
949 {
950 $("#menu .profile").css("display", displayType).text("profile")
951 $("#menu .logout").css("display", displayType)
952 $("#menu .register").css("display", "none")
953 }
954 else if (this.statut == statutType.auth_not_registered)
955 {
956 $("#menu .profile").css("display", "none")
957 $("#menu .logout").css("display", displayType)
958 $("#menu .register").css("display", displayType)
959 }
960 else
961 {
962 $("#menu .profile").css("display", displayType).text("login")
963 $("#menu .logout").css("display", "none")
964 $("#menu .register").css("display", displayType)
965 }
966 }
967
968 /**
969 * Met à jour l'affichage des infos bulles en fonction du profile.
970 */
971 Client.prototype.majBulle = function()
972 {
973 this.util.bulleActive = this.viewTooltips
974 }
975
976 /**
977 * Met à jour la css sélectionnée, lors du chargement des données.
978 */
979 Client.prototype.majCssSelectionee = function()
980 {
981 // extraction du numéro de la css courante
982 var numCssCourante = this.css.match(/^.*?\/(\d)\/.*$/)
983 if (numCssCourante != null && numCssCourante[1] != undefined)
984 {
985 $("#menuCss option").removeAttr("selected")
986 $("#menuCss option[value=" + numCssCourante[1]+ "]").attr("selected", "selected")
987 }
988 }
989
990 Client.prototype.slap = function(userId, raison)
991 {
992 var thisClient = this
993
994 jQuery.ajax({
995 type: "POST",
996 url: "request",
997 dataType: "json",
998 data: this.util.jsonVersAction(
999 {
1000 "action" : "slap",
1001 "cookie" : thisClient.cookie,
1002 "user_id" : userId,
1003 "reason" : raison
1004 }),
1005 success:
1006 function(data)
1007 {
1008 if (data["reply"] == "error")
1009 thisClient.util.messageDialogue(data["error_message"])
1010 }
1011 })
1012 }
1013
1014 Client.prototype.ban = function(userId, raison, minutes)
1015 {
1016 var thisClient = this
1017
1018 // par défaut un ban correspond à 3 jours
1019 if (typeof(minutes) == "undefined")
1020 minutes = conf.tempsBan;
1021
1022 jQuery.ajax({
1023 type: "POST",
1024 url: "request",
1025 dataType: "json",
1026 data: this.util.jsonVersAction(
1027 {
1028 "action" : "ban",
1029 "cookie" : thisClient.cookie,
1030 "duration" : minutes,
1031 "user_id" : userId,
1032 "reason" : raison
1033 }),
1034 success:
1035 function(data)
1036 {
1037 if (data["reply"] == "error")
1038 thisClient.util.messageDialogue(data["error_message"])
1039 }
1040 })
1041 }
1042
1043 Client.prototype.kick = function(userId, raison)
1044 {
1045 this.ban(userId, raison, conf.tempsKick)
1046 }
1047
1048 ///////////////////////////////////////////////////////////////////////////////////////////////////
1049
1050 /**
1051 * classe permettant de gérer les événements (push serveur).
1052 * l'information envoyé est sous la forme :
1053 * {
1054 * "action" : "wait_event"
1055 * "page" : <page>
1056 * [..]
1057 * }
1058 * l'information reçu est sous la forme :
1059 * {
1060 * "reply" : <reply>
1061 * }
1062 * @page la page
1063 */
1064 function PageEvent(page, util)
1065 {
1066 this.page = page
1067 this.util = util
1068
1069 // l'objet JSONHttpRequest représentant la connexion d'attente
1070 this.attenteCourante = null
1071
1072 // le multhreading du pauvre, merci javascript de m'offrire autant de primitives pour la gestion de la concurrence...
1073 this.stop = false
1074 }
1075
1076 /**
1077 * Arrête l'attente courante s'il y en a une.
1078 */
1079 PageEvent.prototype.stopAttenteCourante = function()
1080 {
1081 this.stop = true
1082
1083 if (this.attenteCourante != null)
1084 {
1085 this.attenteCourante.abort()
1086 }
1087 }
1088
1089 /**
1090 * Attend un événement lié à la page.
1091 * @funSend une fonction renvoyant les données json à envoyer
1092 * @funsReceive est un objet comprenant les fonctions à appeler en fonction du "reply"
1093 * les fonctions acceptent un paramètre correspondant au données reçues.
1094 * exemple : {"new_message" : function(data){ ... }}
1095 */
1096 PageEvent.prototype.waitEvent = function(funSend, funsReceive)
1097 {
1098 this.stopAttenteCourante()
1099
1100 this.stop = false
1101
1102 var thisPageEvent = this
1103
1104 // on doit conserver l'ordre des valeurs de l'objet JSON (le serveur les veut dans l'ordre définit dans le protocole)
1105 // TODO : ya pas mieux ?
1106 var dataToSend =
1107 {
1108 "action" : "wait_event",
1109 "page" : this.page
1110 }
1111 var poulpe = funSend()
1112 for (v in poulpe)
1113 dataToSend[v] = poulpe[v]
1114
1115 ;; dumpObj(dataToSend)
1116
1117 this.attenteCourante = jQuery.ajax({
1118 type: "POST",
1119 url: "request",
1120 dataType: "json",
1121 // TODO : doit disparaitre
1122 timeout: 180000, // timeout de 3min. Gros HACK pas beau. FIXME problème décrit ici : http://groups.google.com/group/jquery-en/browse_thread/thread/8724e64af3333a76
1123 data: this.util.jsonVersAction(dataToSend),
1124 success:
1125 function(data)
1126 {
1127 ;; dumpObj(data)
1128
1129 funsReceive[data["reply"]](data)
1130
1131 // rappel de la fonction dans 100 ms
1132 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 100)
1133 },
1134 error:
1135 function(XMLHttpRequest, textStatus, errorThrown)
1136 {
1137 ;; console.log("Connexion perdue dans waitEvent")
1138 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 1000)
1139 }
1140 })
1141 }
1142
1143 /**
1144 * Si un stopAttenteCourante survient un peu n'importe quand il faut imédiatement arreter de boucler.
1145 */
1146 PageEvent.prototype.waitEvent2 = function(funSend, funsReceive)
1147 {
1148 if (this.stop)
1149 return
1150 this.waitEvent(funSend, funsReceive)
1151 }
1152
1153 ///////////////////////////////////////////////////////////////////////////////////////////////////
1154
1155 function initialiserListeStyles(client)
1156 {
1157 $("#menuCss").change(
1158 function()
1159 {
1160 client.setCss("css/" + $("option:selected", this).attr("value") + "/euphorik.css")
1161 }
1162 )
1163 }
1164
1165 // charge dynamiquement le script de debug
1166 ;; jQuery.ajax({async : false, url : "js/debug.js", dataType : "script"})
1167
1168 // le main
1169 $(document).ready(
1170 function()
1171 {
1172 var formateur = new Formateur()
1173 var util = new Util(formateur)
1174 var client = new Client(util)
1175 var pages = new Pages()
1176
1177 // connexion vers le serveur (utilise un cookie qui traine)
1178 client.connexionCookie()
1179
1180 initialiserListeStyles(client)
1181
1182 // FIXME : ne fonctionne pas sous opera
1183 // voir : http://dev.jquery.com/ticket/2892#preview
1184 $(window).unload(function(){client.flush()})
1185
1186 $("#menu .minichat").click(function(){ pages.afficherPage("minichat") })
1187 $("#menu .admin").click(function(){ pages.afficherPage("admin") })
1188 $("#menu .profile").click(function(){ pages.afficherPage("profile") })
1189 $("#menu .logout").click(function(){
1190 util.messageDialogue("Êtes-vous sur de vouloir vous délogger ?", messageType.question,
1191 {"Oui" : function()
1192 {
1193 client.deconnexion();
1194 pages.afficherPage("minichat", true)
1195 },
1196 "Non" : function(){}
1197 }
1198 )
1199 })
1200 $("#menu .register").click(function(){ pages.afficherPage("register") })
1201 $("#menu .about").click(function(){ pages.afficherPage("about") })
1202
1203 // TODO : simplifier et pouvoir créer des liens par exemple : <span class="lien" href="conditions">Conditions d'utilisation</span>
1204 $("#footer .conditions").click(function(){ pages.afficherPage("conditions_utilisation") })
1205
1206 pages.ajouterPage(new PageMinichat(client, formateur, util))
1207 pages.ajouterPage(new PageAdmin(client, formateur, util))
1208 pages.ajouterPage(new PageProfile(client, formateur, util))
1209 pages.ajouterPage(new PageRegister(client, formateur, util))
1210 pages.ajouterPage(new PageAbout(client, formateur, util))
1211 pages.ajouterPage("conditions_utilisation")
1212
1213 pages.afficherPage("minichat")
1214 }
1215 )