www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 }