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