]> git.0d.be Git - chloro.git/blob - chloro/phyll/static/js/chloro.js
do not include non-feed posts on homepage
[chloro.git] / chloro / phyll / static / js / chloro.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   $('article .wiki-anchor-auto').each(function(idx, anchor) {
21           $(anchor).parent().removeAttr('id');
22           $(anchor).remove();
23   });
24 }
25
26 function auto_anchors() {
27   $('article h2, article h3, article h4').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 create_toc() {
37   $('#toc').remove();
38   if ($('article h2').length == 0) return;
39   $div_toc = $('<div id="toc"><ul></ul></div>');
40   $div_toc_ul = $div_toc.find('ul');
41   var li_titles = Array();
42   $('article h2').each(function(idx, elem) {
43     var $elem = $(elem);
44     var slug = elem.id;
45     var $a_title = $('<a></a>', {href: '#' + slug, text: $elem.text().replace(/¶$/, '')});
46     var $li_title = $('<li></li>');
47     $li_title[0].related_position =  $(elem).position().top;
48     li_titles.push($li_title[0]);
49     $a_title.appendTo($li_title);
50     $li_title.appendTo($div_toc_ul);
51   });
52   $('article h1').first().after($div_toc);
53
54   li_titles = li_titles.reverse();
55
56   $(window).on('load', function() {
57     // update positions after images have been loaded
58     $('article h2').each(function(idx, elem) {
59       $('#toc li')[idx].related_position =  $(elem).position().top;
60     });
61     $(window).trigger('scroll');
62   });
63
64   var scroll_timeout_id = null;
65   $(window).on('scroll', function() {
66     if (scroll_timeout_id) clearTimeout(scroll_timeout_id);
67     scroll_timeout_id = setTimeout(function() {  // throttle
68       scroll_timeout_id = null;
69       var current_position = window.scrollY;
70       $('#toc li').removeClass('active');
71       for (const li_title of li_titles) {
72         if (li_title.related_position < current_position - 25) {
73           $(li_title).addClass('active');
74           break;
75         }
76       }
77     }, 50);
78   });
79 };
80
81 (function(window, document, undefined) {
82   var Phylly = {
83     BLOCKS: [
84           {name: 'intertitle', tag: 'H2', klass: 'intertitle'},
85           {name: 'code', tag: 'PRE', klass: 'screen'},
86           {name: 'list', special: 'list', tag: 'UL', klass: 'list'},
87           {name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
88           {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
89           {name: 'quote', tag: 'BLOCKQUOTE', subtag: true, klass: 'quote'},
90     ],
91     input_event: function(event) {
92       var sel = document.getSelection();
93       var anchorNode = sel.anchorNode;
94       if (sel.anchorNode.contentEditable === 'true' && (
95               sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
96         // when everything has been removed, add back <p><br></p>
97         var empty_p = document.createElement('P');
98         empty_p.appendChild(document.createElement('BR'));
99         if (anchorNode.childNodes.length) { // lone <br>
100           anchorNode.removeChild(anchorNode.childNodes[0]);
101         }
102         anchorNode.appendChild(empty_p);
103         var range = document.createRange();
104         range.setStart(empty_p, 0);
105         sel.removeAllRanges();
106         sel.addRange(range);
107         return;
108       }
109       if (event.originalEvent.inputType == "insertText") {
110         var main_node = get_contenteditable_subnode(sel.anchorNode);
111         if (main_node.tagName != 'PRE') {
112           var anchorNode = sel.anchorNode;
113           var offset = sel.anchorOffset;
114           var orig_text = sel.anchorNode.data;
115           var text = orig_text;
116           // typography
117           if (event.originalEvent.data === "'") {
118             text = text.slice(0, offset-1) + '’' + text.slice(offset);
119           }
120           if (text != orig_text) {
121             var new_text = document.createTextNode(text);
122             anchorNode.replaceWith(new_text);
123             sel.collapse(new_text, offset);
124           }
125         }
126         return;
127       }
128       if (event.originalEvent.inputType != "insertParagraph") return true;
129       if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
130         // new empty div got inserted, replace it with a <p>
131         var empty_p = document.createElement('P');
132         empty_p.appendChild(document.createElement('BR'));
133         var empty_div = sel.anchorNode;
134         empty_div.replaceWith(empty_p);
135         var range = document.createRange();
136         range.setStart(empty_p, 0);
137         sel.removeAllRanges();
138         sel.addRange(range);
139       }
140       if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
141         // new empty li got inserted, insert a <p> within
142         var empty_p = document.createElement('P');
143         empty_p.appendChild(document.createElement('BR'));
144         var empty_li = anchorNode;
145         if (empty_li.childNodes.length) { // lone <br>
146           empty_li.removeChild(empty_li.childNodes[0]);
147         }
148         empty_li.appendChild(empty_p);
149         var range = document.createRange();
150         range.setStart(empty_p, 0);
151         sel.removeAllRanges();
152         sel.addRange(range);
153       }
154       var prev_p = sel.anchorNode.previousSibling;
155       if (! prev_p) return;
156       if (prev_p.tagName != 'P') {
157         prev_p = $(prev_p).parents('p')[0];
158         if (! prev_p || prev_p.tagName != 'P') return;
159       }
160       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
161       if (title_match) {
162         var title = document.createElement(title_match[1]);
163         title.innerHTML = prev_p.innerHTML;
164         title.textContent = title.textContent.slice(4);
165         prev_p.replaceWith(title);
166       }
167       return true;
168     },
169
170     init: function() {
171       $(document).on('selectionchange', function(event) {
172         if ($('input[name=link-target].shown').length) {
173           return;
174         }
175         var sel = window.getSelection();
176         if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
177           show_inline_style_toolbar(sel);
178         } else if (inline_style_toolbar) {
179           $(inline_style_toolbar).hide();
180         }
181         if ($(sel.anchorNode).is('div.figure') && $(sel.anchorNode).find('img').length) {
182           show_figure_toolbar(sel);
183         } else if ($(sel.anchorNode).parents('.figure-toolbar').length == 0) {
184           $(figure_toolbar).hide();
185         }
186       });
187       var $image_upload = $('<input type="file" nam="image" id="image-upload">');
188       $image_upload.on('change', upload_image);
189       $image_upload.appendTo(document.body);
190
191       var $document_upload = $('<input type="file" nam="document" id="document-upload">');
192       $document_upload.on('change', upload_document);
193       $document_upload.appendTo(document.body);
194
195       document.execCommand('defaultParagraphSeparator', false, 'p');
196       $(document).on('click', 'div.figure span.empty', function() {
197         window.active_figure = this.parentNode;
198         $('#image-upload').trigger('click');
199         return true;
200       });
201       $(document).on('click', 'div.document span.empty', function() {
202         window.active_document = this.parentNode;
203         $('#document-upload').trigger('click');
204         return true;
205       });
206     },
207
208     off: function() {
209       $('#image-upload').remove();
210       $('#document-upload').remove();
211       if (block_style_toolbar) { block_style_toolbar.hide(); }
212       if (inline_style_toolbar) { inline_style_toolbar.hide(); }
213       $(document).off('selectionchange');
214     },
215
216     window_keypress: function(ev) {
217       if (inline_style_toolbar && inline_style_toolbar.is(':visible')) {
218         if (event.ctrlKey || event.metaKey) {
219           var key = String.fromCharCode(event.which).toLowerCase();
220           var button = inline_style_toolbar.find('[data-accel="' + key + '"]').first();
221           if (button.length) {
222             button.trigger('click');
223             ev.preventDefault();
224           }
225         }
226       }
227     },
228
229     bind_events: function(elem) {
230       $(elem).on('input', Phylly.input_event);
231       $(elem).on('keyup click', update_block_style_toolbar);
232       $(window).on('keydown', this.window_keypress);
233     },
234
235     unbind_events: function(elem) {
236       $(elem).off('input');
237       $(elem).off('keyup click');
238       $(window).off('keydown', this.window_keypress);
239     },
240
241   }
242   window.Phylly = Phylly;
243
244   function upload_image() {
245     if ($(this).prop('files').length > 0) {
246       var file = $(this).prop('files')[0];
247       var params = new FormData();
248       params.append('upload', file);
249       $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
250         var img = document.createElement('IMG');
251         img.src = data.url;
252         if (data.orig_url) {
253           img.setAttribute('data-orig-url', data.orig_url);
254         }
255         $(window.active_figure).empty().append(img);
256       });
257     }
258   }
259
260   function upload_document() {
261     if ($(this).prop('files').length > 0) {
262       var file = $(this).prop('files')[0];
263       var params = new FormData();
264       params.append('upload', file);
265       $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
266         var doc_link = document.createElement('A');
267         doc_link.className = 'button';
268         doc_link.textContent = 'Télécharger ' + data.filename;
269         doc_link.href = data.url;
270         $(window.active_document).empty().append(doc_link);
271       });
272     }
273   }
274
275   function get_contenteditable_subnode(node) {
276     if (node === null) return null;
277     if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
278     if (node.parentNode.contentEditable === 'true') return node;
279     return get_contenteditable_subnode(node.parentNode);
280   }
281   function get_parent(node, type) {
282    if (node === null) return null;
283    if (node.tagName == type) return node;
284    return get_parent(node.parentNode, type);
285   }
286   function get_active_block(node) {
287     var main_node = get_contenteditable_subnode(node);
288     if (main_node === null) return null;
289     for (const block of Phylly.BLOCKS) {
290       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
291         return block;
292     }
293     return null;
294   }
295
296   var block_style_toolbar = null;
297   function block_style() {
298     var sel = window.getSelection();
299     var current_anchor = sel.anchorNode;
300     if (this.action_block.special == 'img') {
301       action = 'insertHTML';
302       param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
303       document.execCommand(action, false, param);
304       current_anchor = $('#new-p')[0];
305       $(current_anchor).attr('id', null);
306       var range = document.createRange();
307       range.setStart(current_anchor, 0);
308       sel.removeAllRanges();
309       sel.addRange(range);
310       update_block_style_toolbar();
311       return;
312     }
313     if (this.action_block.special == 'doc') {
314       action = 'insertHTML';
315       param = '<div class="document"><span class="empty"></span></div><p id="new-p"></p>';
316       document.execCommand(action, false, param);
317       current_anchor = $('#new-p')[0];
318       $(current_anchor).attr('id', null);
319       var range = document.createRange();
320       range.setStart(current_anchor, 0);
321       sel.removeAllRanges();
322       sel.addRange(range);
323       update_block_style_toolbar();
324       return;
325     }
326     if (this.action_block.special == 'list') {
327       if (this.classList.contains('on')) { // toggle off
328         var main_node = get_contenteditable_subnode(sel.anchorNode);
329         var li = get_parent(sel.anchorNode, 'LI');
330         for (var i=li.childNodes.length; i>0; i--) {
331           var child = li.childNodes[i-1];
332           main_node.insertAdjacentElement('afterend', child);
333           var range = document.createRange();
334           range.setStart(child, 0);
335           sel.removeAllRanges();
336           sel.addRange(range);
337         }
338         li.remove();
339         update_block_style_toolbar();
340       } else {
341         var current_node = sel.anchorNode;
342         var ul = document.createElement('UL');
343         ul.className = 'list';
344         var li = document.createElement('LI');
345         ul.appendChild(li);
346         sel.anchorNode.parentNode.insertBefore(ul, current_node);
347         li.appendChild(current_node);
348         var range = document.createRange();
349         range.setStart(current_node, 0);
350         sel.removeAllRanges();
351         sel.addRange(range);
352       }
353       return;
354     }
355     if (this.classList.contains('on')) { // toggle off
356       if (this.action_block.subtag) {
357         // unwrap
358         var main_node = get_contenteditable_subnode(current_anchor);
359         $(current_anchor).detach().insertAfter(main_node);
360       } else {
361         document.execCommand('formatBlock', false, 'p');
362         current_anchor = sel.anchorNode;
363       }
364     } else {
365       action = this.action_block.subtag || this.action_block.tag;
366       if (this.action_block.subtag) {
367         // enclose current tag into a new parent;
368         var new_parent = document.createElement(this.action_block.tag);
369         new_parent.className = this.action_block.klass;
370         $(current_anchor).wrap(new_parent);
371       } else {
372         document.execCommand('formatBlock', false, this.action_block.tag);
373         sel.anchorNode.className = this.action_block.klass;
374         current_anchor = sel.anchorNode;
375       }
376     }
377     var range = document.createRange();
378     range.setStart(current_anchor, 0);
379     sel.removeAllRanges();
380     sel.addRange(range);
381     update_block_style_toolbar();
382   }
383   function update_block_style_toolbar() {
384     var sel = window.getSelection();
385     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
386       if (block_style_toolbar) {
387         $(block_style_toolbar).hide();
388       }
389       return true;
390     }
391     if (block_style_toolbar === null) {
392       block_style_toolbar = $('<div class="block-style-popup"></div>');
393       for (const block of Phylly.BLOCKS) {
394         var button = document.createElement('button');
395         button.action_block = block;
396         button.dataset.action = block.name;
397         button.textContent = block.name;
398         block_style_toolbar.append(button);
399       }
400       block_style_toolbar.hide();
401       block_style_toolbar.insertAfter(document.body);
402       block_style_toolbar.find('button').on('click', block_style);
403     }
404     block_style_toolbar.css('position', 'absolute');
405     var block = get_active_block(sel.anchorNode);
406     block_style_toolbar.find('button').removeClass('on');
407     if (block) {
408       block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
409       block_style_toolbar.addClass('selected');
410     } else {
411       block_style_toolbar.removeClass('selected');
412     }
413     var anchor = get_contenteditable_subnode(sel.anchorNode);
414     var pos = $(anchor).offset();
415     block_style_toolbar.css('top', pos.top - 33);
416     block_style_toolbar.css('left', pos.left);
417     block_style_toolbar.show();
418     return true;
419   }
420
421   var inline_style_toolbar = null;
422   function update_style() {
423     var action = $(this).data('action');
424     var param = null;
425     if (action == 'code') {
426       action = 'insertHTML';
427       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
428     }
429     if (action == 'wiki') {
430       action = 'insertHTML';
431       var text = window.getSelection().toString();
432       var $new_link = $('<a></a>', {text: text, href: '#tbd'});
433       var request_id = Math.floor(Math.random() * 10000);
434       $new_link.attr('data-request-id', request_id);
435       var params = {};
436       params.title = text;
437       params.request_id = request_id;
438       $.post('/ajax/newpage/', params).done(function(data) {
439         $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
440       });
441       param = $new_link[0].outerHTML;
442     }
443     if (action == 'createLink') {
444       var sel = window.getSelection();
445       var selected_link = get_parent(sel.anchorNode, 'A');
446       if (sel.anchorNode.nodeType == Node.TEXT_NODE) {
447         if (sel.anchorNode.length == sel.anchorOffset && sel.anchorNode.nextSibling.nodeName == 'A') {
448           selected_link = sel.anchorNode.nextSibling;
449         }
450       }
451       var $input = $('input[name=link-target]');
452       $input[0]._range = sel.getRangeAt(0);
453       if (selected_link) {
454         $input[0]._selected_link = selected_link;
455         $input.val(selected_link.href);
456       }
457       $input.addClass('shown');
458       $input.focus();
459       return;
460     }
461     document.execCommand(action, false, param);
462   }
463   function validate_link(ev) {
464     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
465     if (ev.key == "Enter") {
466       var $input = $(this);
467       var range = this._range;
468       var url = $input.val();
469       $input.removeClass('shown');
470       var sel = window.getSelection();
471       sel.addRange(this._range);
472       var selected_link = $input[0]._selected_link;
473       if (url) {
474         if (selected_link) {
475           selected_link.href = url;
476         } else {
477           var $new_link = $('<a></a>', {text: sel.toString(), href: url});
478           this._range.deleteContents();
479           this._range.insertNode($new_link[0]);
480           sel.empty();
481           sel.collapse($new_link[0]);
482           sel.empty();
483         }
484       } else {
485         if (selected_link) {
486           selected_link.replaceWith(document.createTextNode(selected_link.textContent));
487         }
488       }
489       $input.val('');
490       $input[0]._selected_link = null;
491     }
492   }
493   function focusout_link(ev) {
494     var $input = $(this);
495     $input.removeClass('shown');
496     var range = this._range;
497     var sel = window.getSelection();
498     sel.addRange(this._range);
499   }
500
501   function show_inline_style_toolbar(sel) {
502     if (inline_style_toolbar === null) {
503       inline_style_toolbar = $('<div class="inline-style-popup">' +
504                       '<button data-action="italic" data-accel="i"><i>i</i></button>' +
505                       '<button data-action="bold" data-accel="b"><b>b</b></button>' +
506                       '<button data-action="code" data-accel="<">&lt;&gt;</button>' +
507                       '<button data-action="removeFormat" data-accel="m">×</button>' +
508                       '<button data-action="wiki">W</button>' +
509                       '<button data-action="createLink">a</button>' +
510                       '<input name="link-target"/>' +
511                       '</div>');
512       inline_style_toolbar.hide();
513       inline_style_toolbar.insertAfter(document.body);
514       inline_style_toolbar.find('button').on('click', update_style);
515       inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
516     }
517     inline_style_toolbar.css('position', 'absolute');
518     var pos = sel.getRangeAt(0).getClientRects()[0];
519     inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
520     inline_style_toolbar.css('left', pos.left + window.scrollX);
521     inline_style_toolbar.show();
522   };
523
524   var figure_toolbar = null;
525   function show_figure_toolbar(sel) {
526     if (figure_toolbar === null) {
527       figure_toolbar = $('<div class="figure-toolbar"><label>Alt: <input name="figure-alt"></label></div>')
528       figure_toolbar.hide();
529       figure_toolbar.insertAfter(document.body);
530       $('[name="figure-alt"]').on('change', function() {
531        $(this.img).attr('alt', $(this).val());
532       });
533     }
534     figure_toolbar.css('position', 'absolute');
535     var pos = sel.getRangeAt(0).getClientRects()[0];
536     figure_toolbar.css('top', pos.bottom + window.scrollY);
537     figure_toolbar.css('left', pos.left + window.scrollX);
538     figure_toolbar.show();
539     $('[name="figure-alt"]')[0].img = $(sel.anchorNode).find('img');
540     $('[name="figure-alt"]').val($('[name="figure-alt"]')[0].img.attr('alt') || '');
541   };
542
543 }(window, document));
544
545 $(function() {
546   $('#quickedit input').on('change', function() {
547     var enable = $(this).is(':checked');
548     if (enable) {
549       remove_auto_anchors();
550       $('div[data-editable]').each(function(i, elem) {
551         $(elem).attr('contenteditable', 'true');
552         var $button = $('<button class="save">Enregistrer</button>');
553         $button[0].div_zone = elem;
554         $button.insertBefore($('#quickedit label'));
555       });
556       Phylly.init(),
557       $('div[data-editable]').each(function(i, elem) {
558         Phylly.bind_events(elem);
559       });
560       $('.save').on('click', function() {
561         var text = $('div[contenteditable]')[0].innerHTML;
562         var csrf = $('[name=csrfmiddlewaretoken]').val();
563         $.post('api-save/',
564           { text: text, csrfmiddlewaretoken: csrf}
565         ).fail(function() {
566           $('.save').addClass('error');
567         }).done(function() {
568           $('.save').removeClass('error');
569         });
570         return false;
571       });
572     } else {
573       auto_anchors();
574       if ($('main.phyll-toc').length) create_toc();
575       Phylly.off(),
576       $('button.save').remove();
577       $('div[data-editable]').each(function(i, elem) {
578         $(elem).attr('contenteditable', 'false');
579         Phylly.unbind_events(elem);
580       });
581     }
582   });
583   $('#quickedit input').trigger('change');
584   if ($('#quickedit input').length == 0) {
585     create_toc();
586   }
587   $('.search-results').empty();
588   $('#search-enable').on('change', function() {
589     if ($(this).is(':checked')) {
590       $('.search-field input').focus();
591     }
592   });
593   $('.search-field').on('submit', function() {
594     var value = $('input[name=q]').val();
595     $.ajax({url: '/ajax/search/', data: {q: value}}).done(function(data) {
596       $('.search-results').empty();
597       for (const hit of data.data) {
598         var $a_hit = $('<a>', {text: hit.title, href: hit.url});
599         var $li_hit = $('<li>');
600         $li_hit.append($a_hit);
601         $('.search-results').append($li_hit);
602       }
603     });
604     return false;
605   });
606 });