]> git.0d.be Git - panikdb.git/blob - panikdb/static/js/combo.wiki.js
ee3e943adbeb84d3fb159924d91849de2db67c6f
[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: 'code', tag: 'PRE', klass: 'code'},
43           {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
44     ],
45     input_event: function(event) {
46       var sel = document.getSelection();
47       var anchorNode = sel.anchorNode;
48       if (sel.anchorNode.contentEditable === 'true' && (
49               sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
50         // when everything has been removed, add back <p><br></p>
51         var empty_p = document.createElement('P');
52         empty_p.appendChild(document.createElement('BR'));
53         if (anchorNode.childNodes.length) { // lone <br>
54           anchorNode.removeChild(anchorNode.childNodes[0]);
55         }
56         anchorNode.appendChild(empty_p);
57         var range = document.createRange();
58         range.setStart(empty_p, 0);
59         sel.removeAllRanges();
60         sel.addRange(range);
61         return;
62       }
63       if (event.originalEvent.inputType != "insertParagraph") return true;
64       if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
65         // new empty div got inserted, replace it with a <p>
66         var empty_p = document.createElement('P');
67         empty_p.appendChild(document.createElement('BR'));
68         var empty_div = sel.anchorNode;
69         empty_div.replaceWith(empty_p);
70         var range = document.createRange();
71         range.setStart(empty_p, 0);
72         sel.removeAllRanges();
73         sel.addRange(range);
74       }
75       if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
76         // new empty li got inserted, insert a <p> within
77         var empty_p = document.createElement('P');
78         empty_p.appendChild(document.createElement('BR'));
79         var empty_li = anchorNode;
80         if (empty_li.childNodes.length) { // lone <br>
81           empty_li.removeChild(empty_li.childNodes[0]);
82         }
83         empty_li.appendChild(empty_p);
84         var range = document.createRange();
85         range.setStart(empty_p, 0);
86         sel.removeAllRanges();
87         sel.addRange(range);
88       }
89       var prev_p = sel.anchorNode.previousSibling;
90       if (! prev_p) return;
91       if (prev_p.tagName != 'P') {
92         prev_p = $(prev_p).parents('p')[0];
93         if (! prev_p || prev_p.tagName != 'P') return;
94       }
95       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
96       if (title_match) {
97         var title = document.createElement(title_match[1]);
98         title.innerHTML = prev_p.innerHTML;
99         title.textContent = title.textContent.slice(4);
100         prev_p.replaceWith(title);
101       }
102       return true;
103     },
104
105     init: function() {
106       $(document).on('selectionchange', function(event) {
107         if ($('input[name=link-target].shown').length) {
108           return;
109         }
110         var sel = window.getSelection();
111         if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
112           show_inline_style_toolbar(sel);
113         } else if (inline_style_toolbar) {
114           $(inline_style_toolbar).hide();
115         }
116       });
117       var $image_upload = $('<input type="file" nam="image" id="image-upload">');
118       $image_upload.on('change', upload_image);
119       $image_upload.appendTo(document.body);
120
121       $(document).on('click', 'div.figure span.empty', function() {
122         window.active_figure = this.parentNode;
123         $('#image-upload').trigger('click');
124         return true;
125       });
126     },
127
128     off: function() {
129       $('#image-upload').remove();
130       if (block_style_toolbar) { block_style_toolbar.hide(); }
131       if (inline_style_toolbar) { inline_style_toolbar.hide(); }
132       $(document).off('selectionchange');
133     },
134
135     bind_events: function(elem) {
136       $(elem).on('input', Phylly.input_event);
137       $(elem).on('keyup click', update_block_style_toolbar);
138     },
139
140     unbind_events: function(elem) {
141       $(elem).off('input');
142       $(elem).off('keyup click');
143     },
144
145   }
146   window.Phylly = Phylly;
147
148   function upload_image() {
149     if ($(this).prop('files').length > 0) {
150       var file = $(this).prop('files')[0];
151       var params = new FormData();
152       params.append('image', file);
153       $.post({url: '/wiki/ajax/image/', processData: false, data: params, contentType: false}).success(function(data) {
154         var img = document.createElement('IMG');
155         img.src = data.url;
156         if (data.orig_url) {
157           img.setAttribute('data-orig-url', data.orig_url);
158         }
159         $(window.active_figure).empty().append(img);
160       });
161     }
162   }
163
164   function get_contenteditable_subnode(node) {
165     if (node === null) return null;
166     if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
167     if (node.parentNode.contentEditable === 'true') return node;
168     return get_contenteditable_subnode(node.parentNode);
169   }
170   function get_parent(node, type) {
171    if (node === null) return null;
172    if (node.tagName == type) return node;
173    return get_parent(node.parentNode, type);
174   }
175   function get_active_block(node) {
176     var main_node = get_contenteditable_subnode(node);
177     if (main_node === null) return null;
178     for (const block of Phylly.BLOCKS) {
179       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
180         return block;
181     }
182     return null;
183   }
184
185   var block_style_toolbar = null;
186   function block_style() {
187     var sel = window.getSelection();
188     var current_anchor = sel.anchorNode;
189     if (this.action_block.special == 'img') {
190       action = 'insertHTML';
191       param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
192       document.execCommand(action, false, param);
193       current_anchor = $('#new-p')[0];
194       $(current_anchor).attr('id', null);
195       var range = document.createRange();
196       range.setStart(current_anchor, 0);
197       sel.removeAllRanges();
198       sel.addRange(range);
199       update_block_style_toolbar();
200
201       return;
202     }
203     if (this.action_block.special == 'list') {
204       if (this.classList.contains('on')) { // toggle off
205         var main_node = get_contenteditable_subnode(sel.anchorNode);
206         var li = get_parent(sel.anchorNode, 'LI');
207         for (var i=li.childNodes.length; i>0; i--) {
208           var child = li.childNodes[i-1];
209           main_node.insertAdjacentElement('afterend', child);
210           var range = document.createRange();
211           range.setStart(child, 0);
212           sel.removeAllRanges();
213           sel.addRange(range);
214         }
215         li.remove();
216         update_block_style_toolbar();
217       } else {
218         var current_node = sel.anchorNode;
219         var ul = document.createElement('UL');
220         ul.className = 'list';
221         var li = document.createElement('LI');
222         ul.appendChild(li);
223         sel.anchorNode.parentNode.insertBefore(ul, current_node);
224         li.appendChild(current_node);
225         var range = document.createRange();
226         range.setStart(current_node, 0);
227         sel.removeAllRanges();
228         sel.addRange(range);
229       }
230       return;
231     }
232     if (this.classList.contains('on')) { // toggle off
233       if (this.action_block.subtag) {
234         // unwrap
235         var main_node = get_contenteditable_subnode(current_anchor);
236         $(current_anchor).detach().insertAfter(main_node);
237       } else {
238         document.execCommand('formatBlock', false, 'p');
239         current_anchor = sel.anchorNode;
240       }
241     } else {
242       action = this.action_block.subtag || this.action_block.tag;
243       if (this.action_block.subtag) {
244         // enclose current tag into a new parent;
245         var new_parent = document.createElement(this.action_block.tag);
246         new_parent.className = this.action_block.klass;
247         $(current_anchor).wrap(new_parent);
248       } else {
249         document.execCommand('formatBlock', false, this.action_block.tag);
250         sel.anchorNode.className = this.action_block.klass;
251         current_anchor = sel.anchorNode;
252       }
253     }
254     var range = document.createRange();
255     range.setStart(current_anchor, 0);
256     sel.removeAllRanges();
257     sel.addRange(range);
258     update_block_style_toolbar();
259   }
260   function update_block_style_toolbar() {
261     var sel = window.getSelection();
262     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
263       if (block_style_toolbar) {
264         $(block_style_toolbar).hide();
265       }
266       return true;
267     }
268     if (block_style_toolbar === null) {
269       block_style_toolbar = $('<div class="block-style-popup"></div>');
270       for (const block of Phylly.BLOCKS) {
271         var button = document.createElement('button');
272         button.action_block = block;
273         button.dataset.action = block.name;
274         button.textContent = block.name;
275         block_style_toolbar.append(button);
276       }
277       block_style_toolbar.hide();
278       block_style_toolbar.insertAfter(document.body);
279       block_style_toolbar.find('button').on('click', block_style);
280     }
281     block_style_toolbar.css('position', 'absolute');
282     var block = get_active_block(sel.anchorNode);
283     block_style_toolbar.find('button').removeClass('on');
284     if (block) {
285       block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
286       block_style_toolbar.addClass('selected');
287     } else {
288       block_style_toolbar.removeClass('selected');
289     }
290     var anchor = get_contenteditable_subnode(sel.anchorNode);
291     var pos = $(anchor).offset();
292     block_style_toolbar.css('top', pos.top - 33);
293     block_style_toolbar.css('left', pos.left);
294     block_style_toolbar.show();
295     return true;
296   }
297
298   var inline_style_toolbar = null;
299   function update_style() {
300     var action = $(this).data('action');
301     var param = null;
302     if (action == 'code') {
303       action = 'insertHTML';
304       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
305     }
306     if (action == 'wiki') {
307       action = 'insertHTML';
308       var text = window.getSelection().toString();
309       var $new_link = $('<a></a>', {text: text, href: '#tbd'});
310       var request_id = Math.floor(Math.random() * 10000);
311       $new_link.attr('data-request-id', request_id);
312       var params = {};
313       params.title = text;
314       params.request_id = request_id;
315       $.post('/wiki/ajax/newpage/', params).success(function(data) {
316         $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
317       });
318       param = $new_link[0].outerHTML;
319     }
320     if (action == 'createLink') {
321       var sel = window.getSelection();
322       var $input = $('input[name=link-target]');
323       $input[0]._range = sel.getRangeAt(0);
324       if (sel.anchorNode instanceof Element) {
325         var elem = sel.anchorNode.childNodes[sel.anchorOffset];
326         if (elem.tagName == 'A') {
327           $input.val(elem.href);
328         }
329       }
330       $input.addClass('shown');
331       $input.focus();
332       return;
333     }
334     document.execCommand(action, false, param);
335   }
336   function validate_link(ev) {
337     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
338     if (ev.key == "Enter") {
339       var $input = $(this);
340       var range = this._range;
341       var url = $input.val();
342       $input.removeClass('shown');
343       var sel = window.getSelection();
344       sel.addRange(this._range);
345       if (url) {
346         document.execCommand('createLink', false, url);
347       } else {
348         document.execCommand('unlink', false, null);
349       }
350       sel.empty();
351       $input.val('');
352     }
353   }
354   function focusout_link(ev) {
355     var $input = $(this);
356     $input.removeClass('shown');
357     var range = this._range;
358     var sel = window.getSelection();
359     sel.addRange(this._range);
360   }
361
362   function show_inline_style_toolbar(sel) {
363     if (inline_style_toolbar === null) {
364       inline_style_toolbar = $('<div class="inline-style-popup">' +
365                       '<button data-action="italic"><i>i</i></button>' +
366                       '<button data-action="bold"><b>b</b></button>' +
367                       '<button data-action="code">&lt;&gt;</button>' +
368                       '<button data-action="removeFormat">×</button>' +
369                       '<button data-action="wiki">W</button>' +
370                       '<button data-action="createLink">a</button>' +
371                       '<input name="link-target"/>' +
372                       '</div>');
373       inline_style_toolbar.hide();
374       inline_style_toolbar.insertAfter(document.body);
375       inline_style_toolbar.find('button').on('click', update_style);
376       inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
377     }
378     inline_style_toolbar.css('position', 'absolute');
379     var pos = sel.getRangeAt(0).getClientRects()[0];
380     inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
381     inline_style_toolbar.css('left', pos.left + window.scrollX);
382     inline_style_toolbar.show();
383   };
384 }(window, document));
385
386 $(function() {
387   if (window.localStorage) {
388     var breadcrumbs = window.localStorage.wiki_breadcrumbs;
389     if (breadcrumbs) {
390       try {
391         breadcrumbs = JSON.parse(breadcrumbs);
392       } catch(e) {
393         breadcrumbs = new Array();
394       }
395     } else {
396       breadcrumbs = new Array();
397     }
398     var page = {href: window.location.pathname, text: $('div.wiki-section > h3 span.title').text()};
399     for (var i=0; i<breadcrumbs.length; i++) {
400       if (breadcrumbs[i].href === page.href) {
401         breadcrumbs.splice(i, 1);
402         break;
403       }
404     }
405     breadcrumbs.reverse();
406     breadcrumbs.push(page);
407     breadcrumbs.reverse();
408     breadcrumbs.splice(5, 1);  // only keep 5 elements
409     window.localStorage.wiki_breadcrumbs = JSON.stringify(breadcrumbs);
410     var $links = $('#more-user-links');
411     $links.append('<span class="wiki-breadcrumbs-separator"></span>');
412     for (var i=1; i<breadcrumbs.length; i++) {
413       var $a = $('<a></a>', breadcrumbs[i]);
414       $links.append(' ');
415       $links.append($a);
416     }
417   }
418   $('#quickedit input').on('change', function() {
419     var enable = $(this).is(':checked');
420     if (enable) {
421       remove_auto_anchors();
422       $('div[data-edit-url] > div').each(function(i, elem) {
423         $(elem).attr('contenteditable', 'true');
424         var $button = $('<button class="save">Enregistrer</button>');
425         $button[0].div_zone = elem;
426         elem.edit_url = $(elem).parents('[data-edit-url]').data('edit-url');
427         $button.insertBefore($('#quickedit label'));
428       });
429       Phylly.init(),
430       $('div[data-edit-url] > div').each(function(i, elem) {
431         Phylly.bind_events(elem);
432       });
433       $('.save').on('click', function() {
434         var csrf = $('[name=csrfmiddlewaretoken]').val();
435         attr = this.div_zone.edit_url.replace(/^.*(data_textcell.*)\//, 'c$1-text');
436         var params = {};
437         params[attr] = this.div_zone.innerHTML;
438         params['csrfmiddlewaretoken'] = csrf;
439         $.post(this.div_zone.edit_url, params).fail(function() {
440           $(this).css('background', 'red');
441         });
442         return false;
443       });
444     } else {
445       auto_anchors();
446       Phylly.off(),
447       $('button.save').remove();
448       $('div[data-edit-url] > div').each(function(i, elem) {
449         $(elem).attr('contenteditable', 'false');
450         Phylly.unbind_events(elem);
451       });
452     }
453   });
454   $('#quickedit input').trigger('change');
455 });