1 // from django/contrib/admin/static/admin/js/urlify.js
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'
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, '-');
19 function remove_auto_anchors() {
20 $('article .wiki-anchor-auto').each(function(idx, anchor) {
21 $(anchor).parent().removeAttr('id');
26 function auto_anchors() {
27 $('article h2, article h3, article h4').each(function(idx, 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);
36 function create_toc() {
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) {
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);
52 $('article h1').first().after($div_toc);
54 li_titles = li_titles.reverse();
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;
61 $(window).trigger('scroll');
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');
81 (function(window, document, undefined) {
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'},
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]);
102 anchorNode.appendChild(empty_p);
103 var range = document.createRange();
104 range.setStart(empty_p, 0);
105 sel.removeAllRanges();
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;
117 if (event.originalEvent.data === "'") {
118 text = text.slice(0, offset-1) + '’' + text.slice(offset);
120 if (text != orig_text) {
121 var new_text = document.createTextNode(text);
122 anchorNode.replaceWith(new_text);
123 sel.collapse(new_text, offset);
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();
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]);
148 empty_li.appendChild(empty_p);
149 var range = document.createRange();
150 range.setStart(empty_p, 0);
151 sel.removeAllRanges();
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;
160 var title_match = prev_p.innerText.match(/^(h[1-6]). /);
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);
171 $(document).on('selectionchange', function(event) {
172 if ($('input[name=link-target].shown').length) {
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();
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();
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);
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);
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');
201 $(document).on('click', 'div.document span.empty', function() {
202 window.active_document = this.parentNode;
203 $('#document-upload').trigger('click');
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');
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();
222 button.trigger('click');
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);
235 unbind_events: function(elem) {
236 $(elem).off('input');
237 $(elem).off('keyup click');
238 $(window).off('keydown', this.window_keypress);
242 window.Phylly = Phylly;
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');
253 img.setAttribute('data-orig-url', data.orig_url);
255 $(window.active_figure).empty().append(img);
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);
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);
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);
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))
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();
310 update_block_style_toolbar();
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();
323 update_block_style_toolbar();
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();
339 update_block_style_toolbar();
341 var current_node = sel.anchorNode;
342 var ul = document.createElement('UL');
343 ul.className = 'list';
344 var li = document.createElement('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();
355 if (this.classList.contains('on')) { // toggle off
356 if (this.action_block.subtag) {
358 var main_node = get_contenteditable_subnode(current_anchor);
359 $(current_anchor).detach().insertAfter(main_node);
361 document.execCommand('formatBlock', false, 'p');
362 current_anchor = sel.anchorNode;
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);
372 document.execCommand('formatBlock', false, this.action_block.tag);
373 sel.anchorNode.className = this.action_block.klass;
374 current_anchor = sel.anchorNode;
377 var range = document.createRange();
378 range.setStart(current_anchor, 0);
379 sel.removeAllRanges();
381 update_block_style_toolbar();
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();
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);
400 block_style_toolbar.hide();
401 block_style_toolbar.insertAfter(document.body);
402 block_style_toolbar.find('button').on('click', block_style);
404 block_style_toolbar.css('position', 'absolute');
405 var block = get_active_block(sel.anchorNode);
406 block_style_toolbar.find('button').removeClass('on');
408 block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
409 block_style_toolbar.addClass('selected');
411 block_style_toolbar.removeClass('selected');
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();
421 var inline_style_toolbar = null;
422 function update_style() {
423 var action = $(this).data('action');
425 if (action == 'code') {
426 action = 'insertHTML';
427 param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
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);
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');
441 param = $new_link[0].outerHTML;
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;
451 var $input = $('input[name=link-target]');
452 $input[0]._range = sel.getRangeAt(0);
454 $input[0]._selected_link = selected_link;
455 $input.val(selected_link.href);
457 $input.addClass('shown');
461 document.execCommand(action, false, param);
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;
475 selected_link.href = url;
477 var $new_link = $('<a></a>', {text: sel.toString(), href: url});
478 this._range.deleteContents();
479 this._range.insertNode($new_link[0]);
481 sel.collapse($new_link[0]);
486 selected_link.replaceWith(document.createTextNode(selected_link.textContent));
490 $input[0]._selected_link = null;
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);
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="<"><></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"/>' +
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);
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();
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());
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') || '');
543 }(window, document));
546 $('#quickedit input').on('change', function() {
547 var enable = $(this).is(':checked');
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'));
557 $('div[data-editable]').each(function(i, elem) {
558 Phylly.bind_events(elem);
560 $('.save').on('click', function() {
561 var text = $('div[contenteditable]')[0].innerHTML;
562 var csrf = $('[name=csrfmiddlewaretoken]').val();
564 { text: text, csrfmiddlewaretoken: csrf}
566 $('.save').addClass('error');
568 $('.save').removeClass('error');
574 if ($('main.phyll-toc').length) create_toc();
576 $('button.save').remove();
577 $('div[data-editable]').each(function(i, elem) {
578 $(elem).attr('contenteditable', 'false');
579 Phylly.unbind_events(elem);
583 $('#quickedit input').trigger('change');
584 $('.search-results').empty();
585 $('#search-enable').on('change', function() {
586 if ($(this).is(':checked')) {
587 $('.search-field input').focus();
590 $('.search-field').on('submit', function() {
591 var value = $('input[name=q]').val();
592 $.ajax({url: '/ajax/search/', data: {q: value}}).done(function(data) {
593 $('.search-results').empty();
594 for (const hit of data.data) {
595 var $a_hit = $('<a>', {text: hit.title, href: hit.url});
596 var $li_hit = $('<li>');
597 $li_hit.append($a_hit);
598 $('.search-results').append($li_hit);