]> git.0d.be Git - panikdb.git/blob - panikdb/static/js/combo.wiki.js
wiki: add document upload support
[panikdb.git] / panikdb / static / js / combo.wiki.js
1 // from django/contrib/admin/static/admin/js/urlify.js
2 var LATIN_MAP = {
3     'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE',
4     'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I',
5     'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O',
6     'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U',
7     'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a',
8     'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c',
9     'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i',
10     'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o',
11     'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
12     'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
13 };
14
15 function downcode(string) {
16   return string.toLowerCase().replace(/[^A-Za-z0-9\[\] ]/g,function(a){ return LATIN_MAP[a]||a }).replace(/[^-\w\s]/g, '').replace(/^\s+|\s+$/g, '').replace(/[-\s]+/g, '-');
17 };
18
19 function remove_auto_anchors() {
20   $('div#main-content .wiki-anchor-auto').each(function(idx, anchor) {
21           $(anchor).parent().removeAttr('id');
22           $(anchor).remove();
23   });
24 }
25
26 function auto_anchors() {
27   $('div#main-content div.textcell h1, div#main-content div.textcell h2, div#main-content div.textcell h3').each(function(idx, elem) {
28     var $elem = $(elem);
29     if ($elem.attr('id')) return;
30     if ($elem.find('.wiki-anchor').length) return;
31     $elem.attr('id', downcode($elem.text()));
32     $('<a class="wiki-anchor wiki-anchor-auto" href="#' + $elem.attr('id') + '">¶</a>').appendTo($elem);
33   });
34 }
35
36 (function(window, document, undefined) {
37   var Phylly = {
38     BLOCKS: [
39           {name: 'intertitre', tag: 'H4', klass: 'intertitle'},
40           {name: 'liste', special: 'list', tag: 'UL', klass: 'list'},
41           {name: 'illustration', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
42           {name: 'document', special: 'doc', tag: 'DIV', subtag: true, klass: 'document'},
43           {name: 'code', tag: 'PRE', klass: 'code'},
44           {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
45     ],
46     input_event: function(event) {
47       var sel = document.getSelection();
48       var anchorNode = sel.anchorNode;
49       if (sel.anchorNode.contentEditable === 'true' && (
50               sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
51         // when everything has been removed, add back <p><br></p>
52         var empty_p = document.createElement('P');
53         empty_p.appendChild(document.createElement('BR'));
54         if (anchorNode.childNodes.length) { // lone <br>
55           anchorNode.removeChild(anchorNode.childNodes[0]);
56         }
57         anchorNode.appendChild(empty_p);
58         var range = document.createRange();
59         range.setStart(empty_p, 0);
60         sel.removeAllRanges();
61         sel.addRange(range);
62         return;
63       }
64       if (event.originalEvent.inputType == "insertText") {
65         var main_node = get_contenteditable_subnode(sel.anchorNode);
66         if (main_node.tagName != 'PRE') {
67           var anchorNode = sel.anchorNode;
68           var offset = sel.anchorOffset;
69           var orig_text = sel.anchorNode.data;
70           var text = orig_text;
71           // typography
72           if (event.originalEvent.data === "'") {
73             text = text.slice(0, offset-1) + '’' + text.slice(offset);
74           }
75           if (text != orig_text) {
76             var new_text = document.createTextNode(text);
77             anchorNode.replaceWith(new_text);
78             sel.collapse(new_text, offset);
79           }
80         }
81         return;
82       }
83       if (event.originalEvent.inputType != "insertParagraph") return true;
84       if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
85         // new empty div got inserted, replace it with a <p>
86         var empty_p = document.createElement('P');
87         empty_p.appendChild(document.createElement('BR'));
88         var empty_div = sel.anchorNode;
89         empty_div.replaceWith(empty_p);
90         var range = document.createRange();
91         range.setStart(empty_p, 0);
92         sel.removeAllRanges();
93         sel.addRange(range);
94       }
95       if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
96         // new empty li got inserted, insert a <p> within
97         var empty_p = document.createElement('P');
98         empty_p.appendChild(document.createElement('BR'));
99         var empty_li = anchorNode;
100         if (empty_li.childNodes.length) { // lone <br>
101           empty_li.removeChild(empty_li.childNodes[0]);
102         }
103         empty_li.appendChild(empty_p);
104         var range = document.createRange();
105         range.setStart(empty_p, 0);
106         sel.removeAllRanges();
107         sel.addRange(range);
108       }
109       var prev_p = sel.anchorNode.previousSibling;
110       if (! prev_p) return;
111       if (prev_p.tagName != 'P') {
112         prev_p = $(prev_p).parents('p')[0];
113         if (! prev_p || prev_p.tagName != 'P') return;
114       }
115       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
116       if (title_match) {
117         var title = document.createElement(title_match[1]);
118         title.innerHTML = prev_p.innerHTML;
119         title.textContent = title.textContent.slice(4);
120         prev_p.replaceWith(title);
121       }
122       return true;
123     },
124
125     init: function() {
126       $(document).on('selectionchange', function(event) {
127         if ($('input[name=link-target].shown').length) {
128           return;
129         }
130         var sel = window.getSelection();
131         if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
132           show_inline_style_toolbar(sel);
133         } else if (inline_style_toolbar) {
134           $(inline_style_toolbar).hide();
135         }
136       });
137       var $image_upload = $('<input type="file" nam="image" id="image-upload" accept="image/*">');
138       $image_upload.on('change', upload_image);
139       $image_upload.appendTo(document.body);
140
141       var $document_upload = $('<input type="file" nam="document" id="document-upload">');
142       $document_upload.on('change', upload_document);
143       $document_upload.appendTo(document.body);
144
145       document.execCommand('defaultParagraphSeparator', false, 'p');
146       $(document).on('click', 'div.figure span.empty', function() {
147         window.active_figure = this.parentNode;
148         $('#image-upload').trigger('click');
149         return true;
150       });
151       $(document).on('click', 'div.document span.empty', function() {
152         window.active_document = this.parentNode;
153         $('#document-upload').trigger('click');
154         return true;
155       });
156     },
157
158     off: function() {
159       $('#image-upload').remove();
160       $('#document-upload').remove();
161       if (block_style_toolbar) { block_style_toolbar.hide(); }
162       if (inline_style_toolbar) { inline_style_toolbar.hide(); }
163       $(document).off('selectionchange');
164     },
165
166     window_keypress: function(ev) {
167       if (inline_style_toolbar && inline_style_toolbar.is(':visible')) {
168         if (event.ctrlKey || event.metaKey) {
169           var key = String.fromCharCode(event.which).toLowerCase();
170           var button = inline_style_toolbar.find('[data-accel="' + key + '"]').first();
171           if (button.length) {
172             button.trigger('click');
173             ev.preventDefault();
174           }
175         }
176       }
177     },
178
179     bind_events: function(elem) {
180       $(elem).on('input', Phylly.input_event);
181       $(elem).on('keyup click', update_block_style_toolbar);
182       $(window).on('keydown', this.window_keypress);
183     },
184
185     unbind_events: function(elem) {
186       $(elem).off('input');
187       $(elem).off('keyup click');
188       $(window).off('keydown', this.window_keypress);
189     },
190
191   }
192   window.Phylly = Phylly;
193
194   function upload_image() {
195     if ($(this).prop('files').length > 0) {
196       var file = $(this).prop('files')[0];
197       var params = new FormData();
198       params.append('upload', file);
199       $.post({url: '/wiki/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
200         var img = document.createElement('IMG');
201         img.src = data.url;
202         if (data.orig_url) {
203           img.setAttribute('data-orig-url', data.orig_url);
204         }
205         $(window.active_figure).empty().append(img);
206       });
207     }
208   }
209
210   function upload_document() {
211     if ($(this).prop('files').length > 0) {
212       var file = $(this).prop('files')[0];
213       var params = new FormData();
214       params.append('upload', file);
215       $.post({url: '/wiki/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
216         var doc_link = document.createElement('A');
217         doc_link.className = 'button';
218         doc_link.textContent = 'Télécharger ' + data.filename;
219         doc_link.href = data.url;
220         $(window.active_document).empty().append(doc_link);
221       });
222     }
223   }
224
225   function get_contenteditable_subnode(node) {
226     if (node === null) return null;
227     if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
228     if (node.parentNode.contentEditable === 'true') return node;
229     return get_contenteditable_subnode(node.parentNode);
230   }
231   function get_parent(node, type) {
232    if (node === null) return null;
233    if (node.tagName == type) return node;
234    return get_parent(node.parentNode, type);
235   }
236   function get_active_block(node) {
237     var main_node = get_contenteditable_subnode(node);
238     if (main_node === null) return null;
239     for (const block of Phylly.BLOCKS) {
240       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
241         return block;
242     }
243     return null;
244   }
245
246   var block_style_toolbar = null;
247   function block_style() {
248     var sel = window.getSelection();
249     var current_anchor = sel.anchorNode;
250     if (this.action_block.special == 'img') {
251       action = 'insertHTML';
252       param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
253       document.execCommand(action, false, param);
254       current_anchor = $('#new-p')[0];
255       $(current_anchor).attr('id', null);
256       var range = document.createRange();
257       range.setStart(current_anchor, 0);
258       sel.removeAllRanges();
259       sel.addRange(range);
260       update_block_style_toolbar();
261       return;
262     }
263     if (this.action_block.special == 'doc') {
264       action = 'insertHTML';
265       param = '<div class="document"><span class="empty"></span></div><p id="new-p"></p>';
266       document.execCommand(action, false, param);
267       current_anchor = $('#new-p')[0];
268       $(current_anchor).attr('id', null);
269       var range = document.createRange();
270       range.setStart(current_anchor, 0);
271       sel.removeAllRanges();
272       sel.addRange(range);
273       update_block_style_toolbar();
274       return;
275     }
276     if (this.action_block.special == 'list') {
277       if (this.classList.contains('on')) { // toggle off
278         var main_node = get_contenteditable_subnode(sel.anchorNode);
279         var li = get_parent(sel.anchorNode, 'LI');
280         for (var i=li.childNodes.length; i>0; i--) {
281           var child = li.childNodes[i-1];
282           main_node.insertAdjacentElement('afterend', child);
283           var range = document.createRange();
284           range.setStart(child, 0);
285           sel.removeAllRanges();
286           sel.addRange(range);
287         }
288         li.remove();
289         update_block_style_toolbar();
290       } else {
291         var current_node = sel.anchorNode;
292         var ul = document.createElement('UL');
293         ul.className = 'list';
294         var li = document.createElement('LI');
295         ul.appendChild(li);
296         sel.anchorNode.parentNode.insertBefore(ul, current_node);
297         li.appendChild(current_node);
298         var range = document.createRange();
299         range.setStart(current_node, 0);
300         sel.removeAllRanges();
301         sel.addRange(range);
302       }
303       return;
304     }
305     if (this.classList.contains('on')) { // toggle off
306       if (this.action_block.subtag) {
307         // unwrap
308         var main_node = get_contenteditable_subnode(current_anchor);
309         $(current_anchor).detach().insertAfter(main_node);
310       } else {
311         document.execCommand('formatBlock', false, 'p');
312         current_anchor = sel.anchorNode;
313       }
314     } else {
315       action = this.action_block.subtag || this.action_block.tag;
316       if (this.action_block.subtag) {
317         // enclose current tag into a new parent;
318         var new_parent = document.createElement(this.action_block.tag);
319         new_parent.className = this.action_block.klass;
320         $(current_anchor).wrap(new_parent);
321       } else {
322         document.execCommand('formatBlock', false, this.action_block.tag);
323         sel.anchorNode.className = this.action_block.klass;
324         current_anchor = sel.anchorNode;
325       }
326     }
327     var range = document.createRange();
328     range.setStart(current_anchor, 0);
329     sel.removeAllRanges();
330     sel.addRange(range);
331     update_block_style_toolbar();
332   }
333   function update_block_style_toolbar() {
334     var sel = window.getSelection();
335     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
336       if (block_style_toolbar) {
337         $(block_style_toolbar).hide();
338       }
339       return true;
340     }
341     if (block_style_toolbar === null) {
342       block_style_toolbar = $('<div class="block-style-popup"></div>');
343       for (const block of Phylly.BLOCKS) {
344         var button = document.createElement('button');
345         button.action_block = block;
346         button.dataset.action = block.name;
347         button.textContent = block.name;
348         block_style_toolbar.append(button);
349       }
350       block_style_toolbar.hide();
351       block_style_toolbar.insertAfter(document.body);
352       block_style_toolbar.find('button').on('click', block_style);
353     }
354     block_style_toolbar.css('position', 'absolute');
355     var block = get_active_block(sel.anchorNode);
356     block_style_toolbar.find('button').removeClass('on');
357     if (block) {
358       block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
359       block_style_toolbar.addClass('selected');
360     } else {
361       block_style_toolbar.removeClass('selected');
362     }
363     var anchor = get_contenteditable_subnode(sel.anchorNode);
364     var pos = $(anchor).offset();
365     block_style_toolbar.css('top', pos.top - 33);
366     block_style_toolbar.css('left', pos.left);
367     block_style_toolbar.show();
368     return true;
369   }
370
371   var inline_style_toolbar = null;
372   function update_style() {
373     var action = $(this).data('action');
374     var param = null;
375     if (action == 'code') {
376       action = 'insertHTML';
377       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
378     }
379     if (action == 'wiki') {
380       action = 'insertHTML';
381       var text = window.getSelection().toString();
382       var $new_link = $('<a></a>', {text: text, href: '#tbd'});
383       var request_id = Math.floor(Math.random() * 10000);
384       $new_link.attr('data-request-id', request_id);
385       var params = {};
386       params.title = text;
387       params.request_id = request_id;
388       $.post('/wiki/ajax/newpage/', params).success(function(data) {
389         $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
390       });
391       param = $new_link[0].outerHTML;
392     }
393     if (action == 'createLink') {
394       var sel = window.getSelection();
395       var $input = $('input[name=link-target]');
396       $input[0]._range = sel.getRangeAt(0);
397       if (sel.anchorNode instanceof Element) {
398         var elem = sel.anchorNode.childNodes[sel.anchorOffset];
399         if (elem.tagName == 'A') {
400           $input.val(elem.href);
401         }
402       }
403       $input.addClass('shown');
404       $input.focus();
405       return;
406     }
407     document.execCommand(action, false, param);
408   }
409   function validate_link(ev) {
410     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
411     if (ev.key == "Enter") {
412       var $input = $(this);
413       var range = this._range;
414       var url = $input.val();
415       $input.removeClass('shown');
416       var sel = window.getSelection();
417       sel.addRange(this._range);
418       if (url) {
419         document.execCommand('createLink', false, url);
420       } else {
421         document.execCommand('unlink', false, null);
422       }
423       sel.empty();
424       $input.val('');
425     }
426   }
427   function focusout_link(ev) {
428     var $input = $(this);
429     $input.removeClass('shown');
430     var range = this._range;
431     var sel = window.getSelection();
432     sel.addRange(this._range);
433   }
434
435   function show_inline_style_toolbar(sel) {
436     if (inline_style_toolbar === null) {
437       inline_style_toolbar = $('<div class="inline-style-popup">' +
438                       '<button data-action="italic" data-accel="i"><i>i</i></button>' +
439                       '<button data-action="bold" data-accel="b"><b>b</b></button>' +
440                       '<button data-action="code" data-accel="<">&lt;&gt;</button>' +
441                       '<button data-action="removeFormat" data-accel="m">×</button>' +
442                       '<button data-action="wiki">W</button>' +
443                       '<button data-action="createLink">a</button>' +
444                       '<input name="link-target"/>' +
445                       '</div>');
446       inline_style_toolbar.hide();
447       inline_style_toolbar.insertAfter(document.body);
448       inline_style_toolbar.find('button').on('click', update_style);
449       inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
450     }
451     inline_style_toolbar.css('position', 'absolute');
452     var pos = sel.getRangeAt(0).getClientRects()[0];
453     inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
454     inline_style_toolbar.css('left', pos.left + window.scrollX);
455     inline_style_toolbar.show();
456   };
457 }(window, document));
458
459 function init_page() {
460   $('#search-results').html('');
461   if (window.localStorage) {
462     var breadcrumbs = window.localStorage.wiki_breadcrumbs;
463     if (breadcrumbs) {
464       try {
465         breadcrumbs = JSON.parse(breadcrumbs);
466       } catch(e) {
467         breadcrumbs = new Array();
468       }
469     } else {
470       breadcrumbs = new Array();
471     }
472     var page = {href: window.location.pathname, text: $('div.wiki-section > h3 span.title').text()};
473     for (var i=0; i<breadcrumbs.length; i++) {
474       if (breadcrumbs[i].href === page.href) {
475         breadcrumbs.splice(i, 1);
476         break;
477       }
478     }
479     breadcrumbs.reverse();
480     breadcrumbs.push(page);
481     breadcrumbs.reverse();
482     breadcrumbs.splice(10, 1);  // only keep 10 elements
483     window.localStorage.wiki_breadcrumbs = JSON.stringify(breadcrumbs);
484     var $links = $('#more-user-links');
485     $links.append('<span class="wiki-breadcrumbs-separator"></span>');
486     for (var i=1; i<breadcrumbs.length; i++) {
487       var $a = $('<a></a>', breadcrumbs[i]);
488       $links.append(' ');
489       $links.append($a);
490     }
491   }
492   $('#quickedit input').on('change', function() {
493     var enable = $(this).is(':checked');
494     if (enable) {
495       remove_auto_anchors();
496       $('div[data-edit-url] > div').each(function(i, elem) {
497         $(elem).attr('contenteditable', 'true');
498         var $button = $('<button class="save">Enregistrer</button>');
499         $button[0].div_zone = elem;
500         elem.edit_url = $(elem).parents('[data-edit-url]').data('edit-url');
501         $button.insertBefore($('#quickedit label'));
502       });
503       Phylly.init(),
504       $('div[data-edit-url] > div').each(function(i, elem) {
505         Phylly.bind_events(elem);
506       });
507       $('.save').on('click', function() {
508         var csrf = $('[name=csrfmiddlewaretoken]').val();
509         attr = this.div_zone.edit_url.replace(/^.*(data_textcell.*)\//, 'c$1-text');
510         var params = {};
511         params[attr] = this.div_zone.innerHTML;
512         params['csrfmiddlewaretoken'] = csrf;
513         $.post(this.div_zone.edit_url, params).fail(function() {
514           $(this).css('background', 'red');
515         });
516         return false;
517       });
518     } else {
519       auto_anchors();
520       Phylly.off(),
521       $('button.save').remove();
522       $('div[data-edit-url] > div').each(function(i, elem) {
523         $(elem).attr('contenteditable', 'false');
524         Phylly.unbind_events(elem);
525       });
526     }
527   });
528   $('#quickedit input').trigger('change');
529
530   function load_page(href, push_history) {
531     $('body').addClass('loading');
532     var content = $.ajax({
533       url: href,
534       dataType: 'html',
535       success: function(html, status, xhr) {
536         Phylly.off(),
537         $('div[data-edit-url] > div').each(function(i, elem) {
538           Phylly.unbind_events(elem);
539         });
540         if (push_history) history.pushState({}, '', href);
541         $('body').removeClass('loading');
542         $('#main-content').replaceWith($(html).find('#main-content'));
543         if (push_history) window.scroll(0, 0);
544         init_page();
545       },
546       error: function() {
547         $('body').removeClass('loading');
548       },
549     });
550   }
551
552   $(window).off('popstate').on('popstate', function(e) {
553     load_page(location.href, false);
554   });
555
556   $('#main-content, #search-results').on('click', 'a[href]:not([rel])', function(e) {
557     var href = this.attributes.href.nodeValue;
558     if (e.which === 2) return true;
559     if (href.slice(0, 6) == '/wiki/') {
560       load_page(this.href, true);
561       return false;
562     }
563   });
564   $('div.wiki-section').on('click', 'img[data-orig-url]', function() {
565     window.location = $(this).data('orig-url');
566   });
567 }
568
569 $(function() {
570   init_page();
571   $('#search input').attr('placeholder', 'Rechercher dans le wiki');
572   var wiki_search_base_url = $('#search form').data('wiki-search-base-url');
573   $('#search form').on('submit', function() {
574     var value = $('input[name=q]').val();
575     $.ajax({url: wiki_search_base_url, data: {q: value}}).success(function(data) {
576       $('#search-results').html(data);
577     });
578     return false;
579   });
580 });