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