1 (function(window, document, undefined) {
4 {name: 'code', tag: 'PRE', klass: 'screen'},
5 {name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
6 {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
8 input_event: function(event) {
9 var sel = document.getSelection();
10 var anchorNode = sel.anchorNode;
11 if (sel.anchorNode.contentEditable === 'true' && (
12 sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
13 // when everything has been removed, add back <p><br></p>
14 var empty_p = document.createElement('P');
15 empty_p.appendChild(document.createElement('BR'));
16 if (anchorNode.childNodes.length) { // lone <br>
17 anchorNode.removeChild(anchorNode.childNodes[0]);
19 anchorNode.appendChild(empty_p);
20 var range = document.createRange();
21 range.setStart(empty_p, 0);
22 sel.removeAllRanges();
26 if (event.originalEvent.inputType == "insertText") {
27 var main_node = get_contenteditable_subnode(sel.anchorNode);
28 if (main_node.tagName != 'PRE') {
29 var anchorNode = sel.anchorNode;
30 var offset = sel.anchorOffset;
31 var orig_text = sel.anchorNode.data;
34 if (event.originalEvent.data === "'") {
35 text = text.slice(0, offset-1) + '’' + text.slice(offset);
37 if (text != orig_text) {
38 var new_text = document.createTextNode(text);
39 anchorNode.replaceWith(new_text);
40 sel.collapse(new_text, offset);
45 if (event.originalEvent.inputType != "insertParagraph") return true;
46 if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
47 // new empty div got inserted, replace it with a <p>
48 var empty_p = document.createElement('P');
49 empty_p.appendChild(document.createElement('BR'));
50 var empty_div = sel.anchorNode;
51 empty_div.replaceWith(empty_p);
52 var range = document.createRange();
53 range.setStart(empty_p, 0);
54 sel.removeAllRanges();
57 if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
58 // new empty li got inserted, insert a <p> within
59 var empty_p = document.createElement('P');
60 empty_p.appendChild(document.createElement('BR'));
61 var empty_li = anchorNode;
62 if (empty_li.childNodes.length) { // lone <br>
63 empty_li.removeChild(empty_li.childNodes[0]);
65 empty_li.appendChild(empty_p);
66 var range = document.createRange();
67 range.setStart(empty_p, 0);
68 sel.removeAllRanges();
71 var prev_p = sel.anchorNode.previousSibling;
73 if (prev_p.tagName != 'P') {
74 prev_p = $(prev_p).parents('p')[0];
75 if (! prev_p || prev_p.tagName != 'P') return;
77 var title_match = prev_p.innerText.match(/^(h[1-6]). /);
79 var title = document.createElement(title_match[1]);
80 title.innerHTML = prev_p.innerHTML;
81 title.textContent = title.textContent.slice(4);
82 prev_p.replaceWith(title);
88 $(document).on('selectionchange', function(event) {
89 if ($('input[name=link-target].shown').length) {
92 var sel = window.getSelection();
93 if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
94 show_inline_style_toolbar(sel);
95 } else if (inline_style_toolbar) {
96 $(inline_style_toolbar).hide();
99 var $image_upload = $('<input type="file" nam="image" id="image-upload">');
100 $image_upload.on('change', upload_image);
101 $image_upload.appendTo(document.body);
103 var $document_upload = $('<input type="file" nam="document" id="document-upload">');
104 $document_upload.on('change', upload_document);
105 $document_upload.appendTo(document.body);
107 document.execCommand('defaultParagraphSeparator', false, 'p');
108 $(document).on('click', 'div.figure span.empty', function() {
109 window.active_figure = this.parentNode;
110 $('#image-upload').trigger('click');
113 $(document).on('click', 'div.document span.empty', function() {
114 window.active_document = this.parentNode;
115 $('#document-upload').trigger('click');
121 $('#image-upload').remove();
122 $('#document-upload').remove();
123 if (block_style_toolbar) { block_style_toolbar.hide(); }
124 if (inline_style_toolbar) { inline_style_toolbar.hide(); }
125 $(document).off('selectionchange');
128 window_keypress: function(ev) {
129 if (inline_style_toolbar && inline_style_toolbar.is(':visible')) {
130 if (event.ctrlKey || event.metaKey) {
131 var key = String.fromCharCode(event.which).toLowerCase();
132 var button = inline_style_toolbar.find('[data-accel="' + key + '"]').first();
134 button.trigger('click');
141 bind_events: function(elem) {
142 $(elem).on('input', Phylly.input_event);
143 $(elem).on('keyup click', update_block_style_toolbar);
144 $(window).on('keydown', this.window_keypress);
147 unbind_events: function(elem) {
148 $(elem).off('input');
149 $(elem).off('keyup click');
150 $(window).off('keydown', this.window_keypress);
154 window.Phylly = Phylly;
156 function upload_image() {
157 if ($(this).prop('files').length > 0) {
158 var file = $(this).prop('files')[0];
159 var params = new FormData();
160 params.append('upload', file);
161 $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
162 var img = document.createElement('IMG');
165 img.setAttribute('data-orig-url', data.orig_url);
167 $(window.active_figure).empty().append(img);
172 function upload_document() {
173 if ($(this).prop('files').length > 0) {
174 var file = $(this).prop('files')[0];
175 var params = new FormData();
176 params.append('upload', file);
177 $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
178 var doc_link = document.createElement('A');
179 doc_link.className = 'button';
180 doc_link.textContent = 'Télécharger ' + data.filename;
181 doc_link.href = data.url;
182 $(window.active_document).empty().append(doc_link);
187 function get_contenteditable_subnode(node) {
188 if (node === null) return null;
189 if (node.contentEditable === 'true') return node; // but we shouldn't arrive at root
190 if (node.parentNode.contentEditable === 'true') return node;
191 return get_contenteditable_subnode(node.parentNode);
193 function get_parent(node, type) {
194 if (node === null) return null;
195 if (node.tagName == type) return node;
196 return get_parent(node.parentNode, type);
198 function get_active_block(node) {
199 var main_node = get_contenteditable_subnode(node);
200 if (main_node === null) return null;
201 for (const block of Phylly.BLOCKS) {
202 if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
208 var block_style_toolbar = null;
209 function block_style() {
210 var sel = window.getSelection();
211 var current_anchor = sel.anchorNode;
212 if (this.action_block.special == 'img') {
213 action = 'insertHTML';
214 param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
215 document.execCommand(action, false, param);
216 current_anchor = $('#new-p')[0];
217 $(current_anchor).attr('id', null);
218 var range = document.createRange();
219 range.setStart(current_anchor, 0);
220 sel.removeAllRanges();
222 update_block_style_toolbar();
225 if (this.action_block.special == 'doc') {
226 action = 'insertHTML';
227 param = '<div class="document"><span class="empty"></span></div><p id="new-p"></p>';
228 document.execCommand(action, false, param);
229 current_anchor = $('#new-p')[0];
230 $(current_anchor).attr('id', null);
231 var range = document.createRange();
232 range.setStart(current_anchor, 0);
233 sel.removeAllRanges();
235 update_block_style_toolbar();
238 if (this.action_block.special == 'list') {
239 if (this.classList.contains('on')) { // toggle off
240 var main_node = get_contenteditable_subnode(sel.anchorNode);
241 var li = get_parent(sel.anchorNode, 'LI');
242 for (var i=li.childNodes.length; i>0; i--) {
243 var child = li.childNodes[i-1];
244 main_node.insertAdjacentElement('afterend', child);
245 var range = document.createRange();
246 range.setStart(child, 0);
247 sel.removeAllRanges();
251 update_block_style_toolbar();
253 var current_node = sel.anchorNode;
254 var ul = document.createElement('UL');
255 ul.className = 'list';
256 var li = document.createElement('LI');
258 sel.anchorNode.parentNode.insertBefore(ul, current_node);
259 li.appendChild(current_node);
260 var range = document.createRange();
261 range.setStart(current_node, 0);
262 sel.removeAllRanges();
267 if (this.classList.contains('on')) { // toggle off
268 if (this.action_block.subtag) {
270 var main_node = get_contenteditable_subnode(current_anchor);
271 $(current_anchor).detach().insertAfter(main_node);
273 document.execCommand('formatBlock', false, 'p');
274 current_anchor = sel.anchorNode;
277 action = this.action_block.subtag || this.action_block.tag;
278 if (this.action_block.subtag) {
279 // enclose current tag into a new parent;
280 var new_parent = document.createElement(this.action_block.tag);
281 new_parent.className = this.action_block.klass;
282 $(current_anchor).wrap(new_parent);
284 document.execCommand('formatBlock', false, this.action_block.tag);
285 sel.anchorNode.className = this.action_block.klass;
286 current_anchor = sel.anchorNode;
289 var range = document.createRange();
290 range.setStart(current_anchor, 0);
291 sel.removeAllRanges();
293 update_block_style_toolbar();
295 function update_block_style_toolbar() {
296 var sel = window.getSelection();
297 if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
298 if (block_style_toolbar) {
299 $(block_style_toolbar).hide();
303 if (block_style_toolbar === null) {
304 block_style_toolbar = $('<div class="block-style-popup"></div>');
305 for (const block of Phylly.BLOCKS) {
306 var button = document.createElement('button');
307 button.action_block = block;
308 button.dataset.action = block.name;
309 button.textContent = block.name;
310 block_style_toolbar.append(button);
312 block_style_toolbar.hide();
313 block_style_toolbar.insertAfter(document.body);
314 block_style_toolbar.find('button').on('click', block_style);
316 block_style_toolbar.css('position', 'absolute');
317 var block = get_active_block(sel.anchorNode);
318 block_style_toolbar.find('button').removeClass('on');
320 block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
321 block_style_toolbar.addClass('selected');
323 block_style_toolbar.removeClass('selected');
325 var anchor = get_contenteditable_subnode(sel.anchorNode);
326 var pos = $(anchor).offset();
327 block_style_toolbar.css('top', pos.top - 33);
328 block_style_toolbar.css('left', pos.left);
329 block_style_toolbar.show();
333 var inline_style_toolbar = null;
334 function update_style() {
335 var action = $(this).data('action');
337 if (action == 'code') {
338 action = 'insertHTML';
339 param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
341 if (action == 'wiki') {
342 action = 'insertHTML';
343 var text = window.getSelection().toString();
344 var $new_link = $('<a></a>', {text: text, href: '#tbd'});
345 var request_id = Math.floor(Math.random() * 10000);
346 $new_link.attr('data-request-id', request_id);
349 params.request_id = request_id;
350 $.post('/wiki/ajax/newpage/', params).success(function(data) {
351 $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
353 param = $new_link[0].outerHTML;
355 if (action == 'createLink') {
356 var sel = window.getSelection();
357 var $input = $('input[name=link-target]');
358 $input[0]._range = sel.getRangeAt(0);
359 if (sel.anchorNode instanceof Element) {
360 var elem = sel.anchorNode.childNodes[sel.anchorOffset];
361 if (elem.tagName == 'A') {
362 $input.val(elem.href);
365 $input.addClass('shown');
369 document.execCommand(action, false, param);
371 function validate_link(ev) {
372 var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
373 if (ev.key == "Enter") {
374 var $input = $(this);
375 var range = this._range;
376 var url = $input.val();
377 $input.removeClass('shown');
378 var sel = window.getSelection();
379 sel.addRange(this._range);
381 document.execCommand('createLink', false, url);
383 document.execCommand('unlink', false, null);
389 function focusout_link(ev) {
390 var $input = $(this);
391 $input.removeClass('shown');
392 var range = this._range;
393 var sel = window.getSelection();
394 sel.addRange(this._range);
397 function show_inline_style_toolbar(sel) {
398 if (inline_style_toolbar === null) {
399 inline_style_toolbar = $('<div class="inline-style-popup">' +
400 '<button data-action="italic" data-accel="i"><i>i</i></button>' +
401 '<button data-action="bold" data-accel="b"><b>b</b></button>' +
402 '<button data-action="code" data-accel="<"><></button>' +
403 '<button data-action="removeFormat" data-accel="m">×</button>' +
404 '<button data-action="createLink">a</button>' +
405 '<input name="link-target"/>' +
407 inline_style_toolbar.hide();
408 inline_style_toolbar.insertAfter(document.body);
409 inline_style_toolbar.find('button').on('click', update_style);
410 inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
412 inline_style_toolbar.css('position', 'absolute');
413 var pos = sel.getRangeAt(0).getClientRects()[0];
414 inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
415 inline_style_toolbar.css('left', pos.left + window.scrollX);
416 inline_style_toolbar.show();
418 }(window, document));
422 $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
423 $('#save').on('click', function() {
424 var text = $('div[contenteditable]')[0].innerHTML;
425 var csrf = $('[name=csrfmiddlewaretoken]').val();
427 { text: text, csrfmiddlewaretoken: csrf}
429 $('#save').css('background', 'red');