APE - Layout page de détail et wrapper jQuery
Encore un billet sur le même sujet, mais cette fois ci, on va mettre en place la scène pour pouvoir commencer a jouer.
APE Tools/Check et symfony
Avant de commencer a tout refaire je me suis dit que reproduire le Tools/Check de APE dans symfony pourrait être une bonne idée.
Dans l'ordre (je vais vite parce que c'est simple hein ...):
./symfony generate:module frontend ape
* fichier actions.class.php
class apeActions extends sfActions { public function executeCheck(sfWebRequest $request) { return sfView::SUCCESS; } }
* fichier routing.yml on rajoute :
ape_check: url: /ape/check param: { module: ape, action: check }
* fichier template checkSucess.php:
<h1>Test your APE installation</h1> <div id="content"> This test will help you to test your APE Client and server is correctly configured. Click on "Launch test" button to check your installation. <br/> <br/> <div style="text-align:center"> <input type="button" onClick="javascript:new APETest()" value="Launch test!"/> </div> <h2>Debug output: </h2> <div id="log"> </div> </div>
* Ensuite la faut réfléchir un peu on fait un fichier ape_check.css ou on copie le CSS qu'il y avait avant et pareil pour le JS. Aoui j'ai dit qu'il fallait réfléchir : Les path sont complètement différents si bien que tous les fichiers sont inaccessible avec leur path par défaut. il faut donc modifier le fichier web/js/ape-jsf/Demos/config.js pour changer le baseUrl et surtout loader les resources à la main. Donc faire un fichier view.yml dans le module ape qui contient :
checkSuccess: javascripts: [ape-jsf/Clients/mootools-core.js, ape-jsf/Clients/MooTools.js, ape-jsf/Demos/config.js, ape_check] stylesheets: [ape_check]
Il faut aussi supprimer les deux premiers tests qui consistent a loader automatiquement les ressources du coup.
Ensuite on démarre le serveur :
./symfony ape:start
On accède a http://mytld.com/web/frontend_dev.php/ape/check et on lance le test qui devrait fonctionner !
Mise en place du décors
Passons a la mise en place d'une pseudo interface html5/css3 (pour la forme) qui nous permettra de faire joujou.
Si on ouvre le fichier layout.php on voit un layout basique xhtml4 transitional, et on va transformer ça en HTML5.
<!DOCTYPE html> <html lang="en"> <head> <?php include_http_metas() ?> <?php include_metas() ?> <?php include_title() ?> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_stylesheets() ?> <?php include_javascripts() ?> </head> <body> <header> <h1>My Project</h1> </header> <section id="content"> <?php echo $sf_content ?> </section> </body> </html>
On voit le nouveau doctype, le nouveau namespace, les balises header et section. Alors je préviens tout de suite il n'y a que Safari qui rendra cette page correctement ... Opera peut être ? Mais firefox aime moins alors comme j'ai personne qui paye pour ça... tant pis ;-)
Voici le fichier detailSuccess.php
<article id="game" class="box"> <div class="boxheader">Zone de jeu</div> <canvas id="map">Vous ne pouvez pas voir les canvas</canvas> </article> <aside id="chat" class="box"> <div class="boxheader">Zone de discussion</div> <dl id="chatzone"> <dt>From: PiTiLeZarD</dt> <dd>Ici un message pour voir</dd> <dt>From: PiTiLeZarD</dt> <dd>Ici un message pour voir</dd> </dl> <input type="text" id="chatinput" /> </aside> <script type="text/javascript"> $(document).ready(function() { var canvas = $('canvas#map').get(0); var context = canvas.getContext('2d'); context.save(); context.fillStyle = '#888'; context.fillRect(0, 0, 400, 400); context.restore(); }); </script>
Là pareil apparition des balises article et aside qui détermine le layout de façon plus sémentique. Pour plus d'info la dessus vous pouvez consulter : http://www.alistapart.com/articles/previewofhtml5 (entre autre)
J'ai mis un javascript pour griser le canvas et voir qu'il est bien pris en compte. C'est pas très utile mais bon.
Pour le layout on va faire du css3 pour s'amuser. NDLR: si c'est moche c'est domage mais je cherchais plus a tester qu'a faire joli ;-)
Le fichier main.css:
header, .box { border:3px solid #395A99; background-color:#728DCC; color:#173877; text-align:center; width:auto; margin:15px 30px; border-radius:10px; -moz-border-radius:10px; -webkit-box-shadow:10px 10px 5px #888; -moz-box-shadow:10px 10px 5px #888; } header h1 { font-family: 'Tangerine', arial, serif; text-shadow:5px 5px 5px #555; font-size:50px; } div.boxheader { width:95%; margin:3px auto 7px auto; border-bottom:1px solid #395A99; font-family: 'Reenie Beanie', arial, serif; font-size:20px; } /* zone de jeu */ article#game { float:left; width:60%; } canvas#map { width:90%; height:400px; margin: 10px auto; } /* zone de chat */ aside#chat { float:right; width:28%; } dl#chatzone { padding:0 0 0 5px; margin:0 0 10px 0; } dl#chatzone dt { font-size:10px; text-align:left; margin-top:5px; } dl#chatzone dd { text-align:left; padding:5px; margin-right:10px; background:white; border:1px solid #395A99; border-radius:10px; -moz-border-radius:10px; } input#chatinput { width:95%; border:3px solid #395A99; padding:3px; color:#173877; font-weight:bold; border-radius:10px; -moz-border-radius:10px; }
Rien de bien grandiose si ce n'est l'utilisation des coins arrondi, des zones d'ombre et de font personalisé ce qui d'ailleurs n'a rien a voir avec CSS3.
Pour ça j'utilise les google fonts (voir ici : http://code.google.com/webfonts). Pour ce faire il faut créer un dossier config dans le module channels et y ajouter un fichier view.yml qui contient :
all: stylesheets: [http://fonts.googleapis.com/css?family=Tangerine, http://fonts.googleapis.com/css?family=Reenie+Beanie] javascripts: [jquery/jquery-1.4.2.min.js, jquery/jquery.class-0.0.2.min.js] detailSuccess: javascripts: [jquery/jquery.cookie.js, ape-jsf/Clients/jQuery, ape-jsf/Demos/config.js, chat]
Ce qui inclura les font google, ainsi que des javascripts que je vais détailler plus tard.
Alors si vous avez tout suivit (ou surtout si j'ai rien oublié) ça devrait ressembler à ça :

Le fameux wrapper jQuery
Etant obstiné j'ai essayé de n'utiliser que jQuery pendant un moment. Mais jQuery c'est un formidable framework qui n'a pas pour vocation de donner une norme concernant la notation objet mais qui a une architecture de plugins orienté manipulation DOM entre autre... Enfin l'idée c'est qu'ils n'utilisent pas de notion de classe mais étendent les capacité des types existant.
J'ai donc eu recours à 2 trucs :
- La classe Classe utilisé par digg (http://code.google.com/p/digg/wiki/Class)
- J'ai récupéré les fonctions bind et create du framework mootools (au départ non mais y'a d'autres trucs qui marchent plus sans ça)
Voici le code du wrapper jQuery.js:
if (Function.prototype.bind == null) { Function.prototype.bind = function(bind, args) { return this.create({'bind': bind, 'arguments': args}); } } if (Function.prototype.create == null) { Function.prototype.create = function(options) { var self = this; options = options || {}; return function(){ var args = options.arguments || arguments; if(args && !args.length){ args = [args]; } var returns = function(){ return self.apply(options.bind || null, args); }; return returns(); }; } } var APE = { Config: { identifier: 'ape', init: true, frequency: 0, maxfreq: 10, scripts: [] }, Client: Class.create({ eventProxy: [], init: function(core) { if (core) this.core = core; }, fireEvent: function(type, args, delay) { this.core.fireEvent(type, args, delay); }, addEvent: function(type, fn, internal) { var newFn = fn.bind(this), ret = this; if (this.core == undefined) { this.eventProxy.push([type, fn, internal]); } else { var ret = this.core.addEvent(type, newFn, internal); this.core.$originalEvents[type] = this.core.$originalEvents[type] || []; this.core.$originalEvents[type][fn] = newFn; } return ret; }, removeEvent: function(type, fn) { return this.core.removeEvent(type, fn); }, onRaw: function(type, fn, internal) { this.addEvent('raw_' + type.toLowerCase(), fn, internal); }, onCmd: function(type, fn, internal) { this.addEvent('cmd_' + type.toLowerCase(), fn, internal); }, onError: function(type, fn, internal) { this.addEvent('error_' + type.toLowerCase(), fn, internal); }, load: function(config) { var self = this; $.extend(config, { 'transport': config.transport || APE.Config.transport || 0, 'frequency': config.frequency || 0, 'domain': config.domain || APE.Config.domain || document.domain, 'scripts': config.scripts || APE.Config.scripts, 'server': config.server || APE.Config.server, 'maxfreq': config.maxfreq || 10 }); config.init = (function(core){ this.core = core; for (var i = 0; i < this.eventProxy.length; i++) { this.addEvent.apply(this, this.eventProxy[i]); } }).bind(this); //set document.domain if (config.transport != 2 && config.domain != 'auto') document.domain = config.domain; if (config.domain == 'auto') document.domain = document.domain; //Get APE cookie var cookie = $.cookie('APE_Cookie'); var tmp = eval('(' + cookie + ')'); if (tmp) { config.frequency = (tmp.frequency+1) % config.maxfreq; } else { cookie = '{"frequency":0}'; } var reg = new RegExp('"frequency":([ 0-9]+)' , "g") cookie = cookie.replace(reg, '"frequency":' + config.frequency); $.cookie('APE_Cookie', cookie); var iframe = document.createElement('iframe'); iframe.setAttribute('id','ape_' + config.identifier); iframe.style.display = 'none'; iframe.style.position = 'absolute'; iframe.style.left = '-300px'; iframe.style.top = '-300px'; document.body.appendChild(iframe); if (config.transport == 2) { var doc = iframe.contentDocument; if (!doc) doc = iframe.contentWindow.document;//For IE //If the content of the iframe is created in DOM, the status bar will always load... //using document.write() is the only way to avoid status bar loading with JSONP doc.open(); var theHtml = '<html><head></head>'; for (var i = 0; i < config.scripts.length; i++) { theHtml += '<script src="' + config.scripts[i] + '"></script>'; } theHtml += '<body></body></html>'; doc.write(theHtml); doc.close(); } else { iframe.setAttribute('src','http://' + config.frequency + '.' + config.server + '/?[{"cmd":"script","params":{"domain":"' + document.domain +'","scripts":["' + config.scripts.join('","') + '"]}}]'); if (navigator.product == 'Gecko') { //Firefox fix, see bug #356558 // https://bugzilla.mozilla.org/show_bug.cgi?id=356558 iframe.contentWindow.location.href = iframe.getAttribute('src'); } } $(iframe).bind('load', function() { if (!iframe.contentWindow.APE) setTimeout(iframe.onload, 100);//Sometimes IE fire the onload event, but the iframe is not loaded -_- else iframe.contentWindow.APE.init(config); }); } }) };
La class web/js/chat.js (appréciez l'originalité par rapport à la shoutbox NDLR: y'en a pas ! ça utilise juste jQuery):
APE.Chat = Class.create(APE.Client.prototype, { init: function(container) { this.els = {}; this.addEvent('load', this.start); this.els.container = $('#container'); this.addEvent('multiPipeCreate', this.createChat); this.onCmd('send',this.cmdSend); this.onRaw('data',this.rawData); }, start: function() { var self = this; if (!this.core.options.restore) { self.nickname = prompt('Your nickname') } else { self.nickname = null; } this.core.start({'name': self.nickname}); }, createChat: function(pipe, options) { var self = this; self.pipe = pipe; $('#chatinput').bind('keypress', {'self': self}, this.postMessage); }, postMessage: function(ev) { var self = ev.data.self; var code = (ev.keyCode ? ev.keyCode : ev.which); if (code == 13) { ev.stopPropagation(); self.pipe.send($('#chatinput').val()); $('#chatinput').val(''); } }, cmdSend: function(param, pipe) { var self = this; var content = '<dt>'+ self.nickname +'</dt>'; content += '<dd>' + param.msg + '</dd>'; $('#chatzone').prepend(content); }, rawData: function(raw, pipe){ var self = this; var content = '<dt>'+ raw.data.from.properties.name +'</dt>'; content += '<dd>' + unescape(raw.data.msg) + '</dd>'; $('#chatzone').prepend(content); } });
Voila donc après ça il suffit simplement de rajouter ce bout de code au $(document).ready déja existant:
var chat = new APE.Chat('chatzone'); chat.load({ 'identifier':'chatzone', 'channel':'global' });
Alors a ce niveau il reste plein de choses a faire, tant d'un point de vue design que fonctionnalités mais on a un chat qui marche avec une zone de jeu !
La suite bientôt (j'espère)
