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