[SPIP] +2.1.12
[velocampus/web/www.git] / www / extensions / porte_plume / javascript / jquery.markitup_pour_spip.js
1 // ----------------------------------------------------------------------------
2 // markItUp! Universal MarkUp Engine, JQuery plugin
3 // v 1.1.8
4 // Dual licensed under the MIT and GPL licenses.
5 // ----------------------------------------------------------------------------
6 // Copyright (C) 2007-2010 Jay Salvat
7 // http://markitup.jaysalvat.com/
8 // ----------------------------------------------------------------------------
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 //
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 //
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26 // ----------------------------------------------------------------------------
27
28 /*
29 * Le code original de markitup 1.1.8
30 * a ete modifie pour prendre en compte
31 *
32 * 1) la langue utilisee dans les textarea :
33 * - si un textarea possede un attribut lang='xx' alors
34 * markitup n'affichera que les icones qui correspondent a cette langue
35 * - on peut passer une valeur de langue par defaut a markitup (le textarea peut ne pas en definir)
36 * .markitup(set_spip,{lang:'fr'});
37 * - une option supplementaire optionnelle 'lang' est introduite dans les parametres
38 * des boutons (markupset), par exemple : lang:['fr','es','en']
39 * - si un bouton n'a pas ce parametre, l'icone s'affiche
40 * quelque soit la langue designee dans le textarea ou les parametres de markitup ;
41 * sinon, il faut que la langue soit contenue dedans pour que l'icone s'affiche.
42 * 2) les control + shift (ou alt) + click bouton qui ne semblaient pas fonctionner
43 * en tout cas sous FF3/ubintu/jquery 1.2.6 a verifier chez les autres (opera 9.5/ubuntu ok)
44 * 3) gerer des types de selections differentes :
45 * - normales comme dans markitup (rien a faire)
46 * - 'selectionType':'word' : aux mots le plus proche si pas de selection (sinon la selection)
47 * - 'selectionType':'line' : aux lignes les plus proches
48 * - and 'return' : ugly hack to generate list (and so on) on key 'return' press
49 * 4) forcer des actions multilignes sans avoir besoin de faire control+click
50 * - 'forceMultiline':true : force donc une insertion multiligne
51 * 5) correction de la recuperation des selections d'Opera et de IE
52 * en utilisant une autre fonction de split() qui corrige leurs bugs.
53 * (caretOffset n'est plus necessaire)
54 *
55 */
56 ;(function($) {
57 $.fn.markItUp = function(settings, extraSettings) {
58 var options, ctrlKey, shiftKey, altKey;
59 ctrlKey = shiftKey = altKey = false;
60
61 options = { id: '',
62 nameSpace: '',
63 root: '',
64 lang: '',
65 previewInWindow: '', // 'width=800, height=600, resizable=yes, scrollbars=yes'
66 previewAutoRefresh: true,
67 previewPosition: 'after',
68 previewTemplatePath: '~/templates/preview.html',
69 previewParserPath: '',
70 previewParserVar: 'data',
71 resizeHandle: true,
72 beforeInsert: '',
73 afterInsert: '',
74 onEnter: {},
75 onShiftEnter: {},
76 onCtrlEnter: {},
77 onTab: {},
78 markupSet: [ { /* set */ } ]
79 };
80 $.extend(options, settings, extraSettings);
81
82 // compute markItUp! path
83 if (!options.root) {
84 $('script').each(function(a, tag) {
85 miuScript = $(tag).get(0).src.match(/(.*)jquery\.markitup(\.pack)?\.js$/);
86 if (miuScript !== null) {
87 options.root = miuScript[1];
88 }
89 });
90 }
91
92 return this.each(function() {
93 var $$, textarea, levels, scrollPosition, caretPosition, caretEffectivePosition,
94 clicked, hash, header, footer, previewWindow, template, iFrame, abort,
95 before, after;
96 $$ = $(this);
97 textarea = this;
98 levels = [];
99 abort = false;
100 scrollPosition = caretPosition = 0;
101
102 options.previewParserPath = localize(options.previewParserPath);
103 options.previewTemplatePath = localize(options.previewTemplatePath);
104
105 // apply the computed path to ~/
106 function localize(data, inText) {
107 if (inText) {
108 return data.replace(/("|')~\//g, "$1"+options.root);
109 }
110 return data.replace(/^~\//, options.root);
111 }
112
113 // init and build editor
114 function init() {
115 id = ''; nameSpace = '';
116 if (options.id) {
117 id = 'id="'+options.id+'"';
118 } else if ($$.attr("id")) {
119 id = 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"';
120
121 }
122 if (options.nameSpace) {
123 nameSpace = 'class="'+options.nameSpace+'"';
124 }
125 $$.wrap('<div '+nameSpace+'></div>');
126 $$.wrap('<div '+id+' class="markItUp"></div>');
127 $$.wrap('<div class="markItUpContainer"></div>');
128 $$.addClass("markItUpEditor");
129
130 // add the header before the textarea
131 header = $('<div class="markItUpHeader"></div>').insertBefore($$);
132 $(dropMenus(options.markupSet)).appendTo(header);
133 // remove empty dropMenu
134 $(header).find("li.markItUpDropMenu ul:empty").parent().remove();
135
136 // add the footer after the textarea
137 footer = $('<div class="markItUpFooter"></div>').insertAfter($$);
138
139 // add the resize handle after textarea
140
141 if (options.resizeHandle === true && $.browser.safari !== true) {
142 resizeHandle = $('<div class="markItUpResizeHandle"></div>')
143 .insertAfter($$)
144 .bind("mousedown", function(e) {
145 var h = $$.height(), y = e.clientY, mouseMove, mouseUp;
146 mouseMove = function(e) {
147 $$.css("height", Math.max(20, e.clientY+h-y)+"px");
148 return false;
149 };
150 mouseUp = function(e) {
151 $("html").unbind("mousemove", mouseMove).unbind("mouseup", mouseUp);
152 return false;
153 };
154 $("html").bind("mousemove", mouseMove).bind("mouseup", mouseUp);
155 });
156 footer.append(resizeHandle);
157 }
158
159 // listen key events
160 $$.keydown(keyPressed).keyup(keyPressed);
161
162 // bind an event to catch external calls
163 $$.bind("insertion", function(e, settings) {
164 if (settings.target !== false) {
165 get();
166 }
167 if (textarea === $.markItUp.focused) {
168 markup(settings);
169 }
170 });
171
172 // remember the last focus
173 $$.focus(function() {
174 $.markItUp.focused = this;
175 });
176 }
177
178 // recursively build header with dropMenus from markupset
179 function dropMenus(markupSet) {
180 var ul = $('<ul></ul>'), i = 0;
181 var lang = ($$.attr('lang')||options.lang);
182
183 $('li:hover > ul', ul).css('display', 'block');
184 $.each(markupSet, function() {
185 var button = this, t = '', title, li, j;
186 // pas de langue ou dans la langue ; et uniquement si langue autorisee
187 if ((!lang || !button.lang || ($.inArray(lang, button.lang) != -1))
188 && (!button.lang_not || ($.inArray(lang, button.lang_not) == -1))) {
189 title = (button.key) ? (button.name||'')+' [Ctrl+'+button.key+']' : (button.name||'');
190 key = (button.key) ? 'accesskey="'+button.key+'"' : '';
191 if (button.separator) {
192 li = $('<li class="markItUpSeparator">'+(button.separator||'')+'</li>').appendTo(ul);
193 } else {
194 i++;
195 for (j = levels.length -1; j >= 0; j--) {
196 t += levels[j]+"-";
197 }
198 li = $('<li class="markItUpButton markItUpButton'+t+(i)+' '+(button.className||'')+'"><a href="" '+key+' title="'+title+'"><b>'+(button.name||'')+'</b></a></li>')
199 .bind("contextmenu", function() { // prevent contextmenu on mac and allow ctrl+click
200 return false;
201 }).click(function() {
202 return false;
203 }).focusin(function(){
204 $$.focus();
205 }).mousedown(function() {
206 if (button.call) {
207 eval(button.call)();
208 }
209 setTimeout(function() { markup(button) },1);
210 return false;
211 }).hover(function() {
212 $('> ul', this).show();
213 $(document).one('click', function() { // close dropmenu if click outside
214 $('ul ul', header).hide();
215 }
216 );
217 }, function() {
218 $('> ul', this).hide();
219 }
220 ).appendTo(ul);
221 if (button.dropMenu) {
222 levels.push(i);
223 $(li).addClass('markItUpDropMenu').append(dropMenus(button.dropMenu));
224 }
225 }
226 }
227 });
228 levels.pop();
229 return ul;
230 }
231
232 // markItUp! markups
233 function magicMarkups(string) {
234 if (string) {
235 string = string.toString();
236 string = string.replace(/\(\!\(([\s\S]*?)\)\!\)/g,
237 function(x, a) {
238 var b = a.split('|!|');
239 if (altKey === true) {
240 return (b[1] !== undefined) ? b[1] : b[0];
241 } else {
242 return (b[1] === undefined) ? "" : b[0];
243 }
244 }
245 );
246 // [![prompt]!], [![prompt:!:value]!]
247 string = string.replace(/\[\!\[([\s\S]*?)\]\!\]/g,
248 function(x, a) {
249 var b = a.split(':!:');
250 if (abort === true) {
251 return false;
252 }
253 value = prompt(b[0], (b[1]) ? b[1] : '');
254 if (value === null) {
255 abort = true;
256 }
257 return value;
258 }
259 );
260 return string;
261 }
262 return "";
263 }
264
265 // prepare action
266 function prepare(action) {
267 if ($.isFunction(action)) {
268 action = action(hash);
269 }
270 return magicMarkups(action);
271 }
272
273 // build block to insert
274 function build(string) {
275 openWith = prepare(clicked.openWith);
276 placeHolder = prepare(clicked.placeHolder);
277 replaceWith = prepare(clicked.replaceWith);
278 closeWith = prepare(clicked.closeWith);
279 if (replaceWith !== "") {
280 block = openWith + replaceWith + closeWith;
281 } else if (selection === '' && placeHolder !== '') {
282 block = openWith + placeHolder + closeWith;
283 } else {
284 block = openWith + (string||selection) + closeWith;
285 }
286 return { block:block,
287 openWith:openWith,
288 replaceWith:replaceWith,
289 placeHolder:placeHolder,
290 closeWith:closeWith
291 };
292 }
293
294
295 function selectWord(){
296 selectionBeforeAfter(/\s|[.,;:!¡?¿()]/);
297 selectionSave();
298 }
299 function selectLine(){
300 selectionBeforeAfter(/\r?\n/);
301 selectionSave();
302 }
303
304 function selectionRemoveLast(pattern){
305 // Remove space by default
306 if (!pattern) pattern = /\s/;
307 last = selection[selection.length-1];
308 if (last && last.match(pattern)) {
309 set(caretPosition, selection.length-1);
310 get();
311 $.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } );
312 }
313 }
314
315 function selectionBeforeAfter(pattern) {
316 if (!pattern) pattern = /\s/;
317 before = textarea.value.substring(0, caretEffectivePosition);
318 after = textarea.value.substring(caretEffectivePosition + selection.length - fixIeBug(selection));
319
320 before = before.split(pattern);
321 after = after.split(pattern);
322 }
323
324 function selectionSave(){
325 nb_before = before ? before[before.length-1].length : 0;
326 nb_after = after ? after[0].length : 0;
327
328 nb = nb_before + selection.length + nb_after - fixIeBug(selection);
329 caretPosition = caretPosition - nb_before;
330
331 set(caretPosition, nb);
332 get();
333 $.extend(hash, { selection:selection, caretPosition:caretPosition, scrollPosition:scrollPosition } );
334 }
335
336 // define markup to insert
337 function markup(button) {
338 var len, j, n, i;
339 hash = clicked = button;
340 get();
341
342 $.extend(hash, { line:"",
343 root:options.root,
344 textarea:textarea,
345 selection:(selection||''),
346 caretPosition:caretPosition,
347 ctrlKey:ctrlKey,
348 shiftKey:shiftKey,
349 altKey:altKey
350 }
351 );
352
353 // corrections des selections pour que
354 // - soit le curseur ne change pas
355 // - soit on prend le mot complet (si pas de selection)
356 // - soit on prend la ligne (avant, apres la selection)
357 if (button.selectionType) {
358
359 if (button.selectionType == "word") {
360 if (!selection) {
361 selectWord();
362 } else {
363 // win/ff add space on double click ? (hum, seems strange)
364 selectionRemoveLast(/\s/);
365 }
366 }
367 if (button.selectionType == "line") {
368 selectLine();
369 }
370 // horrible chose, mais tellement plus pratique
371 // car on ne peut pas de l'exerieur (json) utiliser
372 // les fonctions internes de markitup
373 if (button.selectionType == "return"){
374 selectionBeforeAfter(/\r?\n/);
375 before_last = before[before.length-1];
376 after = '';
377 // gestion des listes -# et -*
378 if (r = before_last.match(/^-([*#]+) ?(.*)$/)) {
379 if (r[2]) {
380 button.replaceWith = "\n-"+r[1]+' ';
381 before_last = '';
382 } else {
383 // supprime le -* present
384 // (before le fera)
385 button.replaceWith = "\n";
386 }
387 } else {
388 before_last = '';
389 button.replaceWith = "\n";
390 }
391 before[before.length-1] = before_last;
392 selectionSave();
393 }
394 }
395 // / fin corrections
396
397 // callbacks before insertion
398 prepare(options.beforeInsert);
399 prepare(clicked.beforeInsert);
400 if (ctrlKey === true && shiftKey === true) {
401 prepare(clicked.beforeMultiInsert);
402 }
403 $.extend(hash, { line:1 });
404
405 // insertion forcee en multiligne ou ctrl+click
406 if ((button.forceMultiline === true && selection.length)
407 || (ctrlKey === true && shiftKey === true)) {
408 lines = selection.split(/\r?\n/);
409 for (j = 0, n = lines.length, i = 0; i < n; i++) {
410 if ($.trim(lines[i]) !== '') {
411 $.extend(hash, { line:++j, selection:lines[i] } );
412 lines[i] = build(lines[i]).block;
413 } else {
414 lines[i] = "";
415 }
416 }
417 string = { block:lines.join('\n')};
418 start = caretPosition;
419 len = string.block.length + (($.browser.opera) ? n-1 : 0);
420 } else if (ctrlKey === true) {
421 string = build(selection);
422 start = caretPosition + string.openWith.length;
423 len = string.block.length - string.openWith.length - string.closeWith.length;
424 len -= fixIeBug(string.block);
425 } else if (shiftKey === true) {
426 string = build(selection);
427 start = caretPosition;
428 len = string.block.length;
429 len -= fixIeBug(string.block);
430 } else {
431 string = build(selection);
432 start = caretPosition + string.block.length ;
433 len = 0;
434 start -= fixIeBug(string.block);
435 }
436
437 if (selection === ''){
438 start += fixOperaBug(string.replaceWith);
439 }
440 $.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } );
441
442 if (string.block !== selection && abort === false) {
443 insert(string.block);
444 set(start, len);
445 }
446
447 get();
448
449 $.extend(hash, { line:'', selection:selection });
450
451 // callbacks after insertion
452 if ((button.forceMultiline === true)
453 || (ctrlKey === true && shiftKey === true)) {
454 prepare(clicked.afterMultiInsert);
455 }
456
457 prepare(clicked.afterInsert);
458 prepare(options.afterInsert);
459
460 // refresh preview if opened
461 if (previewWindow && options.previewAutoRefresh) {
462 refreshPreview();
463 }
464
465 // reinit keyevent
466 shiftKey = altKey = ctrlKey = abort = false;
467
468 }
469
470 // Substract linefeed in Opera
471 function fixOperaBug(string) {
472 if ($.browser.opera) {
473 return string.length - string.replace(/\n*/g, '').length;
474 }
475 return 0;
476 }
477 // Substract linefeed in IE
478 function fixIeBug(string) {
479 if ($.browser.msie) {
480 return string.length - string.replace(/\r*/g, '').length;
481 }
482 return 0;
483 }
484
485 // add markup
486 function insert(block) {
487 if (document.selection) {
488 var newSelection = document.selection.createRange();
489 newSelection.text = block;
490 } else {
491 textarea.value = textarea.value.substring(0, caretEffectivePosition) + block + textarea.value.substring(caretEffectivePosition + selection.length, textarea.value.length);
492 }
493 }
494
495 // set a selection
496 function set(start, len) {
497 if (textarea.createTextRange){
498 range = textarea.createTextRange();
499 range.collapse(true);
500 range.moveStart('character', start);
501 range.moveEnd('character', len);
502 range.select();
503 } else if (textarea.setSelectionRange ){
504 textarea.setSelectionRange(start, start + len);
505 }
506 textarea.scrollTop = scrollPosition;
507 textarea.focus();
508 }
509
510 // get the selection
511 function get() {
512 textarea.focus();
513
514 scrollPosition = textarea.scrollTop;
515 if (document.selection) {
516 selection = document.selection.createRange().text;
517 if ($.browser.msie) { // ie
518 var range = document.selection.createRange(), rangeCopy = range.duplicate();
519 rangeCopy.moveToElementText(textarea);
520 caretPosition = -1;
521 while(rangeCopy.inRange(range)) {
522 rangeCopy.moveStart('character');
523 caretPosition ++;
524 }
525 caretEffectivePosition = caretPosition;
526 } else { // opera
527 caretPosition = textarea.selectionStart;
528 lenSelection = selection.length;
529 // calcul du nombre reel de caracteres pour les substr()
530 set(0,caretPosition);
531 opBefore = document.selection.createRange().text;
532 caretEffectivePosition = opBefore.length - fixOperaBug(opBefore);
533 set(caretPosition, lenSelection);
534 selection = document.selection.createRange().text;
535 }
536 } else { // gecko & webkit
537 caretPosition = textarea.selectionStart;
538 caretEffectivePosition = caretPosition;
539 selection = textarea.value.substring(caretPosition, textarea.selectionEnd);
540
541 }
542 return selection;
543 }
544
545 // open preview window
546 function preview() {
547 if (!previewWindow || previewWindow.closed) {
548 if (options.previewInWindow) {
549 previewWindow = window.open('', 'preview', options.previewInWindow);
550 $(window).unload(function() {
551 previewWindow.close();
552 });
553 } else {
554 iFrame = $('<iframe class="markItUpPreviewFrame"></iframe>');
555 if (options.previewPosition == 'after') {
556 iFrame.insertAfter(footer);
557 } else {
558 iFrame.insertBefore(header);
559 }
560 previewWindow = iFrame[iFrame.length - 1].contentWindow || frame[iFrame.length - 1];
561 }
562 } else if (altKey === true) {
563 if (iFrame) {
564 iFrame.remove();
565 } else {
566 previewWindow.close();
567 }
568 previewWindow = iFrame = false;
569 }
570 if (!options.previewAutoRefresh) {
571 refreshPreview();
572 }
573 if (options.previewInWindow) {
574 previewWindow.focus();
575 }
576 }
577
578 // refresh Preview window
579 function refreshPreview() {
580 renderPreview();
581 }
582
583 function renderPreview() {
584 var phtml;
585 if (options.previewParserPath !== '') {
586 $.ajax( {
587 type: 'POST',
588 url: options.previewParserPath,
589 data: options.previewParserVar+'='+encodeURIComponent($$.val()),
590 success: function(data) {
591 writeInPreview( localize(data, 1) );
592 }
593 } );
594 } else {
595 if (!template) {
596 $.ajax( {
597 url: options.previewTemplatePath,
598 success: function(data) {
599 writeInPreview( localize(data, 1).replace(/<!-- content -->/g, $$.val()) );
600 }
601 } );
602 }
603 }
604 return false;
605 }
606
607 function writeInPreview(data) {
608 if (previewWindow.document) {
609 try {
610 sp = previewWindow.document.documentElement.scrollTop
611 } catch(e) {
612 sp = 0;
613 }
614 previewWindow.document.open();
615 previewWindow.document.write(data);
616 previewWindow.document.close();
617 previewWindow.document.documentElement.scrollTop = sp;
618 }
619 }
620
621 // set keys pressed
622 function keyPressed(e) {
623 if (e.type === 'keydown') {
624 if (e.which === 18) {e.altKey = true;} // alt
625 if (e.which === 17) {e.ctrlKey = true;} // control
626 if (e.which === 16) {e.shiftKey = true;} // shift
627 }
628
629 shiftKey = e.shiftKey;
630 altKey = e.altKey;
631 ctrlKey = (!(e.altKey && e.ctrlKey)) ? e.ctrlKey : false;
632
633 if (e.type === 'keydown') {
634 if (ctrlKey === true) {
635 li = $("a[accesskey="+String.fromCharCode(e.which)+"]", header).parent('li');
636 if (li.length !== 0) {
637 ctrlKey = false;
638 setTimeout(function() {
639 li.triggerHandler('mousedown');
640 },1);
641 return false;
642 }
643 }
644 // si opera, on s'embete pas, il cree plus de problemes qu'autre chose
645 // car il ne prend pas en compte l'arret de ces evenements
646 if (!$.browser.opera) {
647 if (e.which === 13 || e.which === 10) { // Enter key
648 if (ctrlKey === true) { // Enter + Ctrl
649 ctrlKey = false;
650 markup(options.onCtrlEnter);
651 return options.onCtrlEnter.keepDefault;
652 } else if (shiftKey === true) { // Enter + Shift
653 shiftKey = false;
654 markup(options.onShiftEnter);
655 return options.onShiftEnter.keepDefault;
656 } else { // only Enter
657 markup(options.onEnter);
658 return options.onEnter.keepDefault;
659 }
660 }
661
662 if (e.which === 9) { // Tab key
663 if (shiftKey == true || ctrlKey == true || altKey == true) {
664 return false;
665 }
666 markup(options.onTab);
667 return options.onTab.keepDefault;
668 }
669 }
670 }
671 }
672
673 init();
674 });
675 };
676
677 $.fn.markItUpRemove = function() {
678 return this.each(function() {
679 var $$ = $(this).unbind().removeClass('markItUpEditor');
680 $$.parent('div').parent('div.markItUp').parent('div').replaceWith($$);
681 }
682 );
683 };
684
685 $.markItUp = function(settings) {
686 var options = { target:false };
687 $.extend(options, settings);
688 if (options.target) {
689 return $(options.target).each(function() {
690 $(this).focus();
691 $(this).trigger('insertion', [options]);
692 });
693 } else {
694 $('textarea').trigger('insertion', [options]);
695 }
696 };
697
698 })(jQuery);