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 $('div#main-content .wiki-anchor-auto').each(function(idx, anchor) {
21 $(anchor).parent().removeAttr('id');
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) {
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(window, document, undefined) {
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'},
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]);
56 anchorNode.appendChild(empty_p);
57 var range = document.createRange();
58 range.setStart(empty_p, 0);
59 sel.removeAllRanges();
63 if (event.originalEvent.inputType == "insertText") {
64 var main_node = get_contenteditable_subnode(sel.anchorNode);
65 if (main_node.tagName != 'PRE') {
66 var anchorNode = sel.anchorNode;
67 var offset = sel.anchorOffset;
68 var orig_text = sel.anchorNode.data;
71 if (event.originalEvent.data === "'") {
72 text = text.slice(0, offset-1) + '’' + text.slice(offset);
74 if (text != orig_text) {
75 var new_text = document.createTextNode(text);
76 anchorNode.replaceWith(new_text);
77 sel.collapse(new_text, offset);
82 if (event.originalEvent.inputType != "insertParagraph") return true;
83 if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
84 // new empty div got inserted, replace it with a <p>
85 var empty_p = document.createElement('P');
86 empty_p.appendChild(document.createElement('BR'));
87 var empty_div = sel.anchorNode;
88 empty_div.replaceWith(empty_p);
89 var range = document.createRange();
90 range.setStart(empty_p, 0);
91 sel.removeAllRanges();
94 if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
95 // new empty li got inserted, insert a <p> within
96 var empty_p = document.createElement('P');
97 empty_p.appendChild(document.createElement('BR'));
98 var empty_li = anchorNode;
99 if (empty_li.childNodes.length) { // lone <br>
100 empty_li.removeChild(empty_li.childNodes[0]);
102 empty_li.appendChild(empty_p);
103 var range = document.createRange();
104 range.setStart(empty_p, 0);
105 sel.removeAllRanges();
108 var prev_p = sel.anchorNode.previousSibling;
109 if (! prev_p) return;
110 if (prev_p.tagName != 'P') {
111 prev_p = $(prev_p).parents('p')[0];
112 if (! prev_p || prev_p.tagName != 'P') return;
114 var title_match = prev_p.innerText.match(/^(h[1-6]). /);
116 var title = document.createElement(title_match[1]);
117 title.innerHTML = prev_p.innerHTML;
118 title.textContent = title.textContent.slice(4);
119 prev_p.replaceWith(title);
125 $(document).on('selectionchange', function(event) {
126 if ($('input[name=link-target].shown').length) {
129 var sel = window.getSelection();
130 if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
131 show_inline_style_toolbar(sel);
132 } else if (inline_style_toolbar) {
133 $(inline_style_toolbar).hide();
136 var $image_upload = $('<input type="file" nam="image" id="image-upload">');
137 $image_upload.on('change', upload_image);
138 $image_upload.appendTo(document.body);
140 document.execCommand('defaultParagraphSeparator', false, 'p');
141 $(document).on('click', 'div.figure span.empty', function() {
142 window.active_figure = this.parentNode;
143 $('#image-upload').trigger('click');
149 $('#image-upload').remove();
150 if (block_style_toolbar) { block_style_toolbar.hide(); }
151 if (inline_style_toolbar) { inline_style_toolbar.hide(); }
152 $(document).off('selectionchange');
155 bind_events: function(elem) {
156 $(elem).on('input', Phylly.input_event);
157 $(elem).on('keyup click', update_block_style_toolbar);
160 unbind_events: function(elem) {
161 $(elem).off('input');
162 $(elem).off('keyup click');
166 window.Phylly = Phylly;
168 function upload_image() {
169 if ($(this).prop('files').length > 0) {
170 var file = $(this).prop('files')[0];
171 var params = new FormData();
172 params.append('image', file);
173 $.post({url: '/wiki/ajax/image/', processData: false, data: params, contentType: false}).success(function(data) {
174 var img = document.createElement('IMG');
177 img.setAttribute('data-orig-url', data.orig_url);
179 $(window.active_figure).empty().append(img);
184 function get_contenteditable_subnode(node) {
185 if (node === null) return null;
186 if (node.contentEditable === 'true') return node; // but we shouldn't arrive at root
187 if (node.parentNode.contentEditable === 'true') return node;
188 return get_contenteditable_subnode(node.parentNode);
190 function get_parent(node, type) {
191 if (node === null) return null;
192 if (node.tagName == type) return node;
193 return get_parent(node.parentNode, type);
195 function get_active_block(node) {
196 var main_node = get_contenteditable_subnode(node);
197 if (main_node === null) return null;
198 for (const block of Phylly.BLOCKS) {
199 if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
205 var block_style_toolbar = null;
206 function block_style() {
207 var sel = window.getSelection();
208 var current_anchor = sel.anchorNode;
209 if (this.action_block.special == 'img') {
210 action = 'insertHTML';
211 param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
212 document.execCommand(action, false, param);
213 current_anchor = $('#new-p')[0];
214 $(current_anchor).attr('id', null);
215 var range = document.createRange();
216 range.setStart(current_anchor, 0);
217 sel.removeAllRanges();
219 update_block_style_toolbar();
223 if (this.action_block.special == 'list') {
224 if (this.classList.contains('on')) { // toggle off
225 var main_node = get_contenteditable_subnode(sel.anchorNode);
226 var li = get_parent(sel.anchorNode, 'LI');
227 for (var i=li.childNodes.length; i>0; i--) {
228 var child = li.childNodes[i-1];
229 main_node.insertAdjacentElement('afterend', child);
230 var range = document.createRange();
231 range.setStart(child, 0);
232 sel.removeAllRanges();
236 update_block_style_toolbar();
238 var current_node = sel.anchorNode;
239 var ul = document.createElement('UL');
240 ul.className = 'list';
241 var li = document.createElement('LI');
243 sel.anchorNode.parentNode.insertBefore(ul, current_node);
244 li.appendChild(current_node);
245 var range = document.createRange();
246 range.setStart(current_node, 0);
247 sel.removeAllRanges();
252 if (this.classList.contains('on')) { // toggle off
253 if (this.action_block.subtag) {
255 var main_node = get_contenteditable_subnode(current_anchor);
256 $(current_anchor).detach().insertAfter(main_node);
258 document.execCommand('formatBlock', false, 'p');
259 current_anchor = sel.anchorNode;
262 action = this.action_block.subtag || this.action_block.tag;
263 if (this.action_block.subtag) {
264 // enclose current tag into a new parent;
265 var new_parent = document.createElement(this.action_block.tag);
266 new_parent.className = this.action_block.klass;
267 $(current_anchor).wrap(new_parent);
269 document.execCommand('formatBlock', false, this.action_block.tag);
270 sel.anchorNode.className = this.action_block.klass;
271 current_anchor = sel.anchorNode;
274 var range = document.createRange();
275 range.setStart(current_anchor, 0);
276 sel.removeAllRanges();
278 update_block_style_toolbar();
280 function update_block_style_toolbar() {
281 var sel = window.getSelection();
282 if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
283 if (block_style_toolbar) {
284 $(block_style_toolbar).hide();
288 if (block_style_toolbar === null) {
289 block_style_toolbar = $('<div class="block-style-popup"></div>');
290 for (const block of Phylly.BLOCKS) {
291 var button = document.createElement('button');
292 button.action_block = block;
293 button.dataset.action = block.name;
294 button.textContent = block.name;
295 block_style_toolbar.append(button);
297 block_style_toolbar.hide();
298 block_style_toolbar.insertAfter(document.body);
299 block_style_toolbar.find('button').on('click', block_style);
301 block_style_toolbar.css('position', 'absolute');
302 var block = get_active_block(sel.anchorNode);
303 block_style_toolbar.find('button').removeClass('on');
305 block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
306 block_style_toolbar.addClass('selected');
308 block_style_toolbar.removeClass('selected');
310 var anchor = get_contenteditable_subnode(sel.anchorNode);
311 var pos = $(anchor).offset();
312 block_style_toolbar.css('top', pos.top - 33);
313 block_style_toolbar.css('left', pos.left);
314 block_style_toolbar.show();
318 var inline_style_toolbar = null;
319 function update_style() {
320 var action = $(this).data('action');
322 if (action == 'code') {
323 action = 'insertHTML';
324 param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
326 if (action == 'wiki') {
327 action = 'insertHTML';
328 var text = window.getSelection().toString();
329 var $new_link = $('<a></a>', {text: text, href: '#tbd'});
330 var request_id = Math.floor(Math.random() * 10000);
331 $new_link.attr('data-request-id', request_id);
334 params.request_id = request_id;
335 $.post('/wiki/ajax/newpage/', params).success(function(data) {
336 $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
338 param = $new_link[0].outerHTML;
340 if (action == 'createLink') {
341 var sel = window.getSelection();
342 var $input = $('input[name=link-target]');
343 $input[0]._range = sel.getRangeAt(0);
344 if (sel.anchorNode instanceof Element) {
345 var elem = sel.anchorNode.childNodes[sel.anchorOffset];
346 if (elem.tagName == 'A') {
347 $input.val(elem.href);
350 $input.addClass('shown');
354 document.execCommand(action, false, param);
356 function validate_link(ev) {
357 var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
358 if (ev.key == "Enter") {
359 var $input = $(this);
360 var range = this._range;
361 var url = $input.val();
362 $input.removeClass('shown');
363 var sel = window.getSelection();
364 sel.addRange(this._range);
366 document.execCommand('createLink', false, url);
368 document.execCommand('unlink', false, null);
374 function focusout_link(ev) {
375 var $input = $(this);
376 $input.removeClass('shown');
377 var range = this._range;
378 var sel = window.getSelection();
379 sel.addRange(this._range);
382 function show_inline_style_toolbar(sel) {
383 if (inline_style_toolbar === null) {
384 inline_style_toolbar = $('<div class="inline-style-popup">' +
385 '<button data-action="italic"><i>i</i></button>' +
386 '<button data-action="bold"><b>b</b></button>' +
387 '<button data-action="code"><></button>' +
388 '<button data-action="removeFormat">×</button>' +
389 '<button data-action="wiki">W</button>' +
390 '<button data-action="createLink">a</button>' +
391 '<input name="link-target"/>' +
393 inline_style_toolbar.hide();
394 inline_style_toolbar.insertAfter(document.body);
395 inline_style_toolbar.find('button').on('click', update_style);
396 inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
398 inline_style_toolbar.css('position', 'absolute');
399 var pos = sel.getRangeAt(0).getClientRects()[0];
400 inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
401 inline_style_toolbar.css('left', pos.left + window.scrollX);
402 inline_style_toolbar.show();
404 }(window, document));
407 if (window.localStorage) {
408 var breadcrumbs = window.localStorage.wiki_breadcrumbs;
411 breadcrumbs = JSON.parse(breadcrumbs);
413 breadcrumbs = new Array();
416 breadcrumbs = new Array();
418 var page = {href: window.location.pathname, text: $('div.wiki-section > h3 span.title').text()};
419 for (var i=0; i<breadcrumbs.length; i++) {
420 if (breadcrumbs[i].href === page.href) {
421 breadcrumbs.splice(i, 1);
425 breadcrumbs.reverse();
426 breadcrumbs.push(page);
427 breadcrumbs.reverse();
428 breadcrumbs.splice(5, 1); // only keep 5 elements
429 window.localStorage.wiki_breadcrumbs = JSON.stringify(breadcrumbs);
430 var $links = $('#more-user-links');
431 $links.append('<span class="wiki-breadcrumbs-separator"></span>');
432 for (var i=1; i<breadcrumbs.length; i++) {
433 var $a = $('<a></a>', breadcrumbs[i]);
438 $('#quickedit input').on('change', function() {
439 var enable = $(this).is(':checked');
441 remove_auto_anchors();
442 $('div[data-edit-url] > div').each(function(i, elem) {
443 $(elem).attr('contenteditable', 'true');
444 var $button = $('<button class="save">Enregistrer</button>');
445 $button[0].div_zone = elem;
446 elem.edit_url = $(elem).parents('[data-edit-url]').data('edit-url');
447 $button.insertBefore($('#quickedit label'));
450 $('div[data-edit-url] > div').each(function(i, elem) {
451 Phylly.bind_events(elem);
453 $('.save').on('click', function() {
454 var csrf = $('[name=csrfmiddlewaretoken]').val();
455 attr = this.div_zone.edit_url.replace(/^.*(data_textcell.*)\//, 'c$1-text');
457 params[attr] = this.div_zone.innerHTML;
458 params['csrfmiddlewaretoken'] = csrf;
459 $.post(this.div_zone.edit_url, params).fail(function() {
460 $(this).css('background', 'red');
467 $('button.save').remove();
468 $('div[data-edit-url] > div').each(function(i, elem) {
469 $(elem).attr('contenteditable', 'false');
470 Phylly.unbind_events(elem);
474 $('#quickedit input').trigger('change');