editeur.js (17165B)
1 $(function() { 2 $(".éditeur-semantique").each(function(i,e) { 3 éditeurSémantique(e, schémasTypesNoeud); 4 }); 5 }); 6 7 function inRange(x, min, max) { 8 if (typeof x != "number") { 9 return 0; 10 } else { 11 return Math.min(Math.max(min,x), max); 12 } 13 } 14 15 Object.keys = function(object) { 16 var keys = []; 17 for (var k in object) { 18 keys.push(k); 19 } 20 return keys; 21 } 22 23 /* Renvoie un objet composé de trois fonctions : 24 * 25 * ajouterÉcouteur(déclenchementInitial) 26 * Enregistre un nouvel écouteur, et appelle 27 * déclenchementInitial si c'est une fonction. 28 * Cela permet d'initialiser l'écouteur la 29 * première fois qu'il s'enregistre. 30 * enleverÉcouteur(écouteur) 31 * Permet enlève écouteur de la liste. 32 * déclencherÉcouteurs(param1, ..., paramN) 33 * Appelle tous les écouteurs de la liste avec 34 * param1, ..., paramN comme paramètres. 35 */ 36 function Écoutable(déclenchementInitial) { 37 var écouteurs = []; 38 return { 39 ajouterÉcouteur: function(écouteur) { 40 if (écouteurs.indexOf(écouteur) < 0) { 41 écouteurs.push(écouteur); 42 if (déclenchementInitial) 43 déclenchementInitial(); 44 } 45 }, 46 enleverÉcouteur: function(écouteur) { 47 var i = écouteurs.indexOf(écouteur); 48 if (i >= 0) écouteurs.splice(i,1); 49 }, 50 déclencherÉcouteurs: function() { 51 var args = arguments; 52 $(écouteurs).each(function(i,écouteur){ 53 écouteur.apply({}, args); 54 }); 55 } 56 }; 57 } 58 59 function valeur(v) { 60 var _valeur = v; 61 var écoutable = new Écoutable(function() { 62 // Première fois. 63 écoutable.déclencherÉcouteurs(_valeur, _valeur); 64 }); 65 var f = function(v) { 66 return (typeof v == "undefined") 67 ? f.get() 68 : f.set(v); 69 }; 70 f.get = function() { 71 return _valeur; 72 } 73 f.set = function(v) { 74 if (v != _valeur) { 75 _valeur = v; 76 écoutable.déclencherÉcouteurs(v); 77 } 78 } 79 f.ajouterÉcouteur = écoutable.ajouterÉcouteur; 80 f.enleverÉcouteur = écoutable.enleverÉcouteur; 81 return f; 82 } 83 84 /* ===== Types de noeud ===== */ 85 86 var MULTI_LIGNE = 0; 87 var MONO_LIGNE = 1; 88 var EN_LIGNE = 2; 89 90 $.fn.extend({ 91 appendVuesEnfants: function (modèle, typeVue) { 92 for (var i = 0; i < modèle.nbEnfants(); i++) { 93 this.append(modèle.enfant(i).créerVue(typeVue)); 94 } 95 var html = this; 96 modèle.insérerEnfant.ajouterÉcouteur(function(noeud, position) { 97 var v = noeud.créerVue(typeVue); 98 if (position <= 0) { 99 v.prependTo(html); 100 } else if (position >= html.children().size()) { 101 v.appendTo(html); 102 } else { 103 v.insertBefore(html.children().eq(position)); 104 } 105 }); 106 modèle.supprimerEnfant.ajouterÉcouteur(function(position) { 107 html.children().eq(position).remove(); 108 }); 109 return this; 110 }, 111 bindText: function(valeur) { 112 var that = this; 113 valeur.ajouterÉcouteur(function(val) { 114 that.text(val); 115 }); 116 this.text(valeur.get()); 117 return this; 118 }, 119 bindVal: function(valeur) { 120 var that = this; 121 valeur.ajouterÉcouteur(function(val) { 122 if (that.val() != val) 123 that.val(val); 124 }); 125 this.val(valeur.get()); 126 this.bind("propertychange input cut paste keypress", function() { 127 valeur.set(that.val()); 128 }); 129 return this; 130 } 131 }); 132 133 function squeletteAperçuNoeud(noeud) { 134 var ct = {}; 135 ct[MULTI_LIGNE] = { tag: 'div', tagc: 'div', cat: 'multi-ligne' }; 136 ct[MONO_LIGNE] = { tag: 'div', tagc: 'span', cat: 'mono-ligne' }; 137 ct[EN_LIGNE] = { tag: 'span', tagc: 'span', cat: 'en-ligne' }; 138 ct = ct[noeud.type().catégorie]; 139 140 var html = $('<' + ct.tag + ' class="noeud"/>').addClass(noeud.type().nom).addClass(ct.cat); 141 var étiquette = $('<span class="étiquette"/>').appendTo(html); 142 var contenu = $('<' + ct.tagc + ' class="contenu"/>').appendTo(html); 143 144 html.click(function(){ 145 noeud.document().noeudActif.set(noeud); 146 return false; 147 }); 148 149 return html; 150 } 151 152 // Nettoie typesNoeud 153 function TypesNoeud(schémaDéfaut, schémasTypesNoeud) { 154 for (var i in schémasTypesNoeud) { 155 this[i] = $.extend({}, schémaDéfaut, schémasTypesNoeud[i]); 156 this[i].vues = $.extend({}, schémaDéfaut.vues, schémasTypesNoeud[i].vues); 157 this[i].nom = i; 158 } 159 } 160 161 var schémasTypesNoeud = new TypesNoeud( 162 {// Schéma par défaut 163 catégorie: EN_LIGNE, 164 enfants: ['texte'], 165 vues: { 166 aperçu: function() { 167 return squeletteAperçuNoeud(this) 168 .children(".contenu").appendVuesEnfants(this, 'aperçu').end(); 169 }, 170 édition: function() { // TODO : afficher la même chose que "édition.append(...);" (dans la vue de document). 171 return $('<div class="info">Cliquez sur du texte pour le modifier.</div>'); 172 } 173 }, 174 vue: function (typeVue) { 175 var tv = this.type().vues[typeVue]; 176 return (tv) ? tv.call(this, typeVue) : $("<span/>"); 177 }, 178 propriétés: {} 179 }, 180 {// Schémas des types de noeud 181 document: { 182 catégorie: MULTI_LIGNE, 183 enfants: ['titre', 'paragraphe'], 184 // surcharge de la _fonction_ "vue" (pas le tableau "vues"). 185 vue: function() { 186 var html = $('<div class="conteneur-esem"/>'); 187 188 // Paneau Aperçu. 189 var aperçu = $('<div class="aperçu"/>').appendTo(html).appendVuesEnfants(this, 'aperçu'); 190 191 // Paneau Boutons 192 var boutons = $('<div class="boutons"/>').appendTo(html); 193 194 // Paneau Édition 195 var édition = $('<div class="éditeur"/>').appendTo(html); 196 this.noeudActif.ajouterÉcouteur(function(actif) { 197 édition.empty(); 198 if (actif !== null) { 199 édition.append(actif.créerVue("édition")); 200 } else { 201 édition.append('<div class="info">Cliquez sur du texte pour le modifier.</div>'); 202 } 203 }); 204 205 return html; 206 } 207 }, 208 titre: { 209 catégorie: MONO_LIGNE, 210 enfants: ['important', 'texte'], 211 }, 212 paragraphe: { 213 catégorie: MULTI_LIGNE, 214 enfants: ['important', 'texte'], 215 }, 216 important: { 217 catégorie: EN_LIGNE, 218 enfants: ['texte'], 219 }, 220 lien: { 221 catégorie: EN_LIGNE, 222 enfants: ['texte'], 223 vues: { 224 aperçu: function() { 225 var ret = squeletteAperçuNoeud(this); 226 $('<span class="cible"/>').bindText(this.propriété("cible")).appendTo(ret.children(".contenu")); 227 $('<span class="texte"/>').bindText(this.propriété("texte")).appendTo(ret.children(".contenu")); 228 return ret; 229 }, 230 édition: function() { 231 var html = $('<div/>'); 232 $('<label>Texte du lien : </label>').appendTo(html); 233 $('<input type="text"/>').bindVal(this.propriété("texte")).appendTo(html); 234 $('<br/>').appendTo(html); 235 $('<label>Cible du lien : </label>').appendTo(html); 236 $('<input type="text"/>').bindVal(this.propriété("cible")).appendTo(html); 237 return html; 238 }, 239 }, 240 propriétés: { 241 interne: false, 242 cible: 'http://www.example.com/', 243 texte: 'texte du lien' 244 } 245 }, 246 texte: { 247 catégorie: EN_LIGNE, 248 enfants: [], 249 vues: { 250 aperçu: function() { 251 var noeud = this; 252 return $('<span class="noeud texte en-ligne"/>') 253 .bindText(this.propriété("texte")) 254 .click(function(){ 255 noeud.document().noeudActif.set(noeud); 256 return false; 257 }); 258 }, 259 édition: function() { 260 return $('<textarea rows="10" cols="70"/>').bindVal(this.propriété("texte")); // TODO 261 }, 262 }, 263 propriétés: { 264 texte: '' 265 } 266 } 267 } 268 ); 269 270 /* ===== Manipulation des noeuds ===== */ 271 272 var créerDocument = function(schémasTypesNoeud) { 273 function clôture_référence_document(privé_document) { 274 return { 275 document: function() { 276 return privé_document; 277 } 278 }; 279 }; 280 function clôture_parent() { 281 var privé_parent = null; 282 return { 283 parent: function() { 284 return privé_parent; 285 }, 286 setParent: function(p) { 287 // vérification de la cohérence du modèle 288 if (p !== null && typeof p != "undefined" && p.indexOf(this) >= 0) { 289 // Nouveau parent 290 privé_parent = p; 291 } else if (this.positionDansParent() >= 0) { 292 // Parent existant 293 // Pas de modification 294 } else { 295 // Pas de parent 296 privé_parent = null; 297 } 298 }, 299 positionDansParent: function() { 300 return (privé_parent === null) ? -1 : privé_parent.indexOf(this); 301 } 302 }; 303 }; 304 function clôture_enfants() { 305 var privé_enfants = []; 306 var écoutableInsérer = new Écoutable(); 307 var écoutableSupprimer = new Écoutable(); 308 var ret = { 309 nbEnfants: function() { 310 return privé_enfants.length; 311 }, 312 enfant: function(i) { 313 return privé_enfants[i]; 314 }, 315 indexOf: function(noeud) { 316 return privé_enfants.indexOf(noeud); 317 }, 318 insérerEnfant: function(noeud, position) { 319 if (noeud.parent() !== null) 320 noeud.supprimer(); 321 position = inRange(position, 0, privé_enfants.length); 322 privé_enfants.splice(position, 0, noeud); 323 // noeud.setParent() doit être appellé après l'insertion 324 // car setParent vérifie qu'on est bien le parent. 325 noeud.setParent(this); 326 écoutableInsérer.déclencherÉcouteurs(noeud, position); 327 }, 328 supprimerEnfant: function(position) { 329 position = inRange(position, 0, privé_enfants.length - 1); 330 var e = privé_enfants.splice(position, 1)[0]; 331 e.setParent(null); 332 écoutableSupprimer.déclencherÉcouteurs(position); 333 return e; 334 } 335 }; 336 ret.insérerEnfant.ajouterÉcouteur = écoutableInsérer.ajouterÉcouteur; 337 ret.insérerEnfant.enleverÉcouteur = écoutableInsérer.enleverÉcouteur; 338 ret.supprimerEnfant.ajouterÉcouteur = écoutableSupprimer.ajouterÉcouteur; 339 ret.supprimerEnfant.enleverÉcouteur = écoutableInsérer.enleverÉcouteur; 340 return ret; 341 }; 342 function clôture_type(privé_type) { 343 return { 344 type: function() { 345 return this.document().schémaTypeNoeud(privé_type); 346 }, 347 setType: function(nouveauType) { 348 // TODO : lien -> (texte ou important ou ...) doit préserver le texte du lien. 349 // TODO : vérifier si le parent peut bien contenir ce type. 350 // TODO : vérifier si ce type peut bien contenir les enfants actuels. 351 privé_type = nouveauType; 352 // TODO : modifier la vue 353 } 354 }; 355 }; 356 function clôture_propriétés(propriétésDéfaut) { 357 var privé_propriétés = {}; 358 for (i in propriétésDéfaut) { 359 privé_propriétés[i] = valeur(propriétésDéfaut); 360 } 361 362 return { 363 propriété: function(nom) { 364 return privé_propriétés[nom]; 365 }, 366 listePropriétés: function() { 367 return Object.keys(privé_propriétés); 368 } 369 } 370 }; 371 372 var supplément_manipulation = { 373 supprimer: function() { 374 return this.parent().supprimerEnfant(this.positionDansParent()); 375 }, 376 insérerAvant: function(noeud) { // insère noeud avant this (à l'extérieur). 377 this.parent.insérerEnfant(noeud, this.positionDansParent()); 378 }, 379 insérerAprès: function(noeud) { // insère noeud après this (à l'extérieur). 380 this.parent.insérerEnfant(noeud, this.positionDansParent() + 1); 381 }, 382 insérerDébut: function(noeud) { // insère noeud au début de this (à l'intérieur). 383 this.insérerEnfant(noeud, 0); 384 }, 385 insérerFin: function(noeud) { // insère noeud à la fin de this (à l'intérieur). 386 this.insérerEnfant(noeud, this.nbEnfants()); 387 }, 388 emballer: function(noeud) { // insère noeud à la place de this, et met this dedans. 389 var pos = this.positionDansParent(); 390 var parent = this.parent(); 391 parent.supprimerEnfant(pos); 392 parent.insérerEnfant(noeud, pos); 393 noeud.insérerEnfant(this, 0); 394 }, 395 remplacer: function () { 396 var pos = this.positionDansParent(); 397 var parent = this.parent(); 398 parent.supprimerEnfant(pos); 399 for (var i = 0; i < arguments.length; i++) { 400 parent.insérerEnfant(arguments[i], pos++); 401 } 402 }, 403 déballer: function() { // Contraire de emballer : supprime this, mais garde le contenu. 404 var c = []; 405 for (var i = 0; i < this.nbEnfants(); i++) { 406 c[i] = this.supprimerEnfant(i); // TODO : l'insertion devrait elle-même supprimer le noeud s'il est déjà inséré quelque part. 407 } 408 this.remplacer.apply(this, c); 409 } 410 }; 411 412 var supplément_vue = { 413 créerVue: function(typeVue) { 414 return this.type().vue.call(this, typeVue); 415 } 416 }; 417 418 var supplément_toString = { 419 toString: function() { 420 info = "[" + this.nbEnfants() + "]"; 421 var t = this.propriété("texte"); 422 if (t) info += ' "' + t.get() + '"'; 423 return this.type().nom + info; 424 } 425 }; 426 427 function clôture_document(privé_schémasTypesNoeud) { 428 var privé_pressePapier = null; 429 430 var privé_document = { 431 noeudActif: valeur(null), // TODO : vérifier que ce soit bien null ou un noeud 432 schémaTypeNoeud: function(type) { 433 return privé_schémasTypesNoeud[type]; 434 }, 435 créerNoeud: function(type) { 436 return $.extend( 437 {}, 438 clôture_référence_document(privé_document), 439 clôture_parent(), 440 clôture_enfants(), 441 clôture_type(type), 442 clôture_propriétés(privé_schémasTypesNoeud[type].propriétés), 443 supplément_manipulation, 444 supplément_vue, 445 supplément_toString 446 ); 447 } 448 }; 449 450 return $.extend(privé_document, privé_document.créerNoeud("document")); 451 }; 452 453 return document = clôture_document(schémasTypesNoeud); 454 } 455 456 /* ===== Textarea => éditeur sémantique ===== */ 457 458 function éditeurSémantique(textareaOrigine, schémasTypesNoeud) { 459 // XML -> modèle 460 var textareaOrigine = $(textareaOrigine); 461 // TODO : Est-ce que le parsage de xml par jQuery est portable ? . 462 // TODO : Utiliser .val() ? ou .text() ? 463 var xml = $("<document/>").append(textareaOrigine.val()); 464 var modèle = XMLVersModèle(xml.get(0), schémasTypesNoeud); 465 466 // Vue 467 var vue = modèle.créerVue(null).insertAfter(textareaOrigine); 468 // Il faut garder le textarea d'origine, sinon lors d'un refresh, 469 // le textarea d'origine prend la valeur d'un des textarea affichés 470 // et c'est cette mauvaise valeur qu'on récupère en tant que XML. 471 textareaOrigine.hide(); 472 473 // Debug 474 m = modèle; 475 v = vue; 476 } 477 478 function XMLVersModèle(xml, schémasTypesNoeud, document) { 479 var tag = xml.tagName.toLowerCase(); 480 481 // Création du noeud 482 if (document) { 483 var noeud = document.créerNoeud(tag); 484 } else { 485 var document = créerDocument(schémasTypesNoeud); 486 var noeud = document; 487 } 488 489 // Remplissage des propriétés 490 $.each(noeud.listePropriétés(), function(i,prop) { 491 var propval = $(xml).attr(prop); 492 if (typeof propval != "undefined") { 493 noeud.propriété(prop).set(propval); 494 } 495 }); 496 497 // Remplissage des enfants 498 $(xml).children().each(function (i,e) { 499 var x = XMLVersModèle(e, schémasTypesNoeud, document); 500 noeud.insérerFin(x); 501 }); 502 503 return noeud; 504 } 505 506 /* Modèle : 507 508 { document, noeudActif, pressePapiers } 509 510 // Todo : où indiquer TypeVue (aperçu, édition, boutons(?), arbre) ? 511 512 vue.supprimerVue(); 513 vue.supprimerEnfant(position); 514 noeud.créerVue(typeVue); 515 vue.insérerEnfant(noeud_enfant, position) { insérer_dans_vue_courante_à_position(créerVue(noeud_enfant), position); } 516 vue.setPropriété(propriété, valeur); 517 518 */ 519 520 521 522 523 524 525 526 function éditeurSémantique_(textareaÉditeur) { 527 var conteneur = $('<div class="conteneur-esem"/>'); 528 var éditeur = $(textareaÉditeur).removeClass("éditeur-semantique").addClass("éditeur"); 529 var boutons = $('<div class="boutons"/>'); 530 var aperçu = $('<div class="aperçu"/>'); 531 var elementActif = null; 532 533 éditeur.replaceWith(conteneur); 534 conteneur.append(aperçu); 535 conteneur.append(boutons); 536 conteneur.append(éditeur); 537 538 var xml = $("<document/>").append(éditeur.text()); // Est-ce portable ?. 539 éditeur.text(""); 540 541 function init() { 542 xmlVersDom(xml, aperçu); 543 sélectionElement(aperçu.children().first()); // assertion : type == Document 544 bouton("important", function(debut, fin) { 545 var t = elementActif.text(); 546 var t1 = créerElement("texte").text(t.substring(0,debut)); 547 var t2 = créerElement("texte").text(t.substring(debut,fin)); 548 var t2important = créerElement("important").append(t2); 549 var t3 = créerElement("texte").text(t.substring(fin)); 550 elementActif.replaceWith(t1); 551 t2important.insertAfter(t1); 552 t3.insertAfter(t2important); 553 sélectionElement(t2); 554 }); 555 } 556 557 function bouton(texte, fonction) { 558 boutons.append($('<input type="button" class="bouton"/>').val(texte).click(function(e) { 559 fonction(éditeur.get(0).selectionStart, éditeur.get(0).selectionEnd); 560 })); 561 } 562 563 function xmlVersDom(xml,htmlParent) { 564 var htmlElem = créerElement(xml.get(0).tagName.toLowerCase()).appendTo(htmlParent); 565 566 if (xml.get(0).tagName.toLowerCase() == "texte") { 567 htmlElem.append(xml.text()); 568 } 569 570 xml.children().each(function(i,xmlElem) { 571 xmlVersDom($(xmlElem),htmlElem); 572 }); 573 } 574 575 function créerElement(type) { 576 var el = $('<span class="element"/>') 577 .addClass(type) 578 .data("type", type) 579 .click(function(e) { 580 sélectionElement(el); 581 return false; 582 }); 583 return el; 584 } 585 586 function sélectionElement(e) { 587 elementActif = e; 588 if (e.data("type") == "texte") { 589 éditeurAttacher(e); 590 } else { 591 éditeurDétacher(); 592 } 593 } 594 595 function éditeurAttacher(elem) { 596 éditeurDétacher(); 597 éditeur.val(elem.text()); 598 599 var éditeurValPrécédente = éditeur.val(); 600 function éditeurModif(e) { 601 if (éditeur.val() != éditeurValPrécédente) 602 elem.text(éditeur.val()); 603 } 604 605 éditeur.bind("propertychange input cut paste keypress", éditeurModif); 606 } 607 608 function éditeurDétacher() { 609 éditeur.unbind("propertychange input cut paste keypress"); 610 éditeur.val(""); 611 } 612 613 init(); 614 }