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'},
7 {name: 'quote', tag: 'BLOCKQUOTE', subtag: true, klass: 'quote'},
9 input_event: function(event) {
10 var sel = document.getSelection();
11 var anchorNode = sel.anchorNode;
12 if (sel.anchorNode.contentEditable === 'true' && (
13 sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
14 // when everything has been removed, add back <p><br></p>
15 var empty_p = document.createElement('P');
16 empty_p.appendChild(document.createElement('BR'));
17 if (anchorNode.childNodes.length) { // lone <br>
18 anchorNode.removeChild(anchorNode.childNodes[0]);
20 anchorNode.appendChild(empty_p);
21 var range = document.createRange();
22 range.setStart(empty_p, 0);
23 sel.removeAllRanges();
27 if (event.originalEvent.inputType == "insertText") {
28 var main_node = get_contenteditable_subnode(sel.anchorNode);
29 if (main_node.tagName != 'PRE') {
30 var anchorNode = sel.anchorNode;
31 var offset = sel.anchorOffset;
32 var orig_text = sel.anchorNode.data;
35 if (event.originalEvent.data === "'") {
36 text = text.slice(0, offset-1) + '’' + text.slice(offset);
38 if (text != orig_text) {
39 var new_text = document.createTextNode(text);
40 anchorNode.replaceWith(new_text);
41 sel.collapse(new_text, offset);
46 if (event.originalEvent.inputType != "insertParagraph") return true;
47 if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
48 // new empty div got inserted, replace it with a <p>
49 var empty_p = document.createElement('P');
50 empty_p.appendChild(document.createElement('BR'));
51 var empty_div = sel.anchorNode;
52 empty_div.replaceWith(empty_p);
53 var range = document.createRange();
54 range.setStart(empty_p, 0);
55 sel.removeAllRanges();
58 if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
59 // new empty li got inserted, insert a <p> within
60 var empty_p = document.createElement('P');
61 empty_p.appendChild(document.createElement('BR'));
62 var empty_li = anchorNode;
63 if (empty_li.childNodes.length) { // lone <br>
64 empty_li.removeChild(empty_li.childNodes[0]);
66 empty_li.appendChild(empty_p);
67 var range = document.createRange();
68 range.setStart(empty_p, 0);
69 sel.removeAllRanges();
72 var prev_p = sel.anchorNode.previousSibling;
74 if (prev_p.tagName != 'P') {
75 prev_p = $(prev_p).parents('p')[0];
76 if (! prev_p || prev_p.tagName != 'P') return;
78 var title_match = prev_p.innerText.match(/^(h[1-6]). /);
80 var title = document.createElement(title_match[1]);
81 title.innerHTML = prev_p.innerHTML;
82 title.textContent = title.textContent.slice(4);
83 prev_p.replaceWith(title);
89 $(document).on('selectionchange', function(event) {
90 if ($('input[name=link-target].shown').length) {
93 var sel = window.getSelection();
94 if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
95 show_inline_style_toolbar(sel);
96 } else if (inline_style_toolbar) {
97 $(inline_style_toolbar).hide();
100 var $image_upload = $('<input type="file" nam="image" id="image-upload">');
101 $image_upload.on('change', upload_image);
102 $image_upload.appendTo(document.body);
104 var $document_upload = $('<input type="file" nam="document" id="document-upload">');
105 $document_upload.on('change', upload_document);
106 $document_upload.appendTo(document.body);
108 document.execCommand('defaultParagraphSeparator', false, 'p');
109 $(document).on('click', 'div.figure span.empty', function() {
110 window.active_figure = this.parentNode;
111 $('#image-upload').trigger('click');
114 $(document).on('click', 'div.document span.empty', function() {
115 window.active_document = this.parentNode;
116 $('#document-upload').trigger('click');
122 $('#image-upload').remove();
123 $('#document-upload').remove();
124 if (block_style_toolbar) { block_style_toolbar.hide(); }
125 if (inline_style_toolbar) { inline_style_toolbar.hide(); }
126 $(document).off('selectionchange');
129 window_keypress: function(ev) {
130 if (inline_style_toolbar && inline_style_toolbar.is(':visible')) {
131 if (event.ctrlKey || event.metaKey) {
132 var key = String.fromCharCode(event.which).toLowerCase();
133 var button = inline_style_toolbar.find('[data-accel="' + key + '"]').first();
135 button.trigger('click');
142 bind_events: function(elem) {
143 $(elem).on('input', Phylly.input_event);
144 $(elem).on('keyup click', update_block_style_toolbar);
145 $(window).on('keydown', this.window_keypress);
148 unbind_events: function(elem) {
149 $(elem).off('input');
150 $(elem).off('keyup click');
151 $(window).off('keydown', this.window_keypress);
155 window.Phylly = Phylly;
157 function upload_image() {
158 if ($(this).prop('files').length > 0) {
159 var file = $(this).prop('files')[0];
160 var params = new FormData();
161 params.append('upload', file);
162 $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
163 var img = document.createElement('IMG');
166 img.setAttribute('data-orig-url', data.orig_url);
168 $(window.active_figure).empty().append(img);
173 function upload_document() {
174 if ($(this).prop('files').length > 0) {
175 var file = $(this).prop('files')[0];
176 var params = new FormData();
177 params.append('upload', file);
178 $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
179 var doc_link = document.createElement('A');
180 doc_link.className = 'button';
181 doc_link.textContent = 'Télécharger ' + data.filename;
182 doc_link.href = data.url;
183 $(window.active_document).empty().append(doc_link);
188 function get_contenteditable_subnode(node) {
189 if (node === null) return null;
190 if (node.contentEditable === 'true') return node; // but we shouldn't arrive at root
191 if (node.parentNode.contentEditable === 'true') return node;
192 return get_contenteditable_subnode(node.parentNode);
194 function get_parent(node, type) {
195 if (node === null) return null;
196 if (node.tagName == type) return node;
197 return get_parent(node.parentNode, type);
199 function get_active_block(node) {
200 var main_node = get_contenteditable_subnode(node);
201 if (main_node === null) return null;
202 for (const block of Phylly.BLOCKS) {
203 if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
209 var block_style_toolbar = null;
210 function block_style() {
211 var sel = window.getSelection();
212 var current_anchor = sel.anchorNode;
213 if (this.action_block.special == 'img') {
214 action = 'insertHTML';
215 param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
216 document.execCommand(action, false, param);
217 current_anchor = $('#new-p')[0];
218 $(current_anchor).attr('id', null);
219 var range = document.createRange();
220 range.setStart(current_anchor, 0);
221 sel.removeAllRanges();
223 update_block_style_toolbar();
226 if (this.action_block.special == 'doc') {
227 action = 'insertHTML';
228 param = '<div class="document"><span class="empty"></span></div><p id="new-p"></p>';
229 document.execCommand(action, false, param);
230 current_anchor = $('#new-p')[0];
231 $(current_anchor).attr('id', null);
232 var range = document.createRange();
233 range.setStart(current_anchor, 0);
234 sel.removeAllRanges();
236 update_block_style_toolbar();
239 if (this.action_block.special == 'list') {
240 if (this.classList.contains('on')) { // toggle off
241 var main_node = get_contenteditable_subnode(sel.anchorNode);
242 var li = get_parent(sel.anchorNode, 'LI');
243 for (var i=li.childNodes.length; i>0; i--) {
244 var child = li.childNodes[i-1];
245 main_node.insertAdjacentElement('afterend', child);
246 var range = document.createRange();
247 range.setStart(child, 0);
248 sel.removeAllRanges();
252 update_block_style_toolbar();
254 var current_node = sel.anchorNode;
255 var ul = document.createElement('UL');
256 ul.className = 'list';
257 var li = document.createElement('LI');
259 sel.anchorNode.parentNode.insertBefore(ul, current_node);
260 li.appendChild(current_node);
261 var range = document.createRange();
262 range.setStart(current_node, 0);
263 sel.removeAllRanges();
268 if (this.classList.contains('on')) { // toggle off
269 if (this.action_block.subtag) {
271 var main_node = get_contenteditable_subnode(current_anchor);
272 $(current_anchor).detach().insertAfter(main_node);
274 document.execCommand('formatBlock', false, 'p');
275 current_anchor = sel.anchorNode;
278 action = this.action_block.subtag || this.action_block.tag;
279 if (this.action_block.subtag) {
280 // enclose current tag into a new parent;
281 var new_parent = document.createElement(this.action_block.tag);
282 new_parent.className = this.action_block.klass;
283 $(current_anchor).wrap(new_parent);
285 document.execCommand('formatBlock', false, this.action_block.tag);
286 sel.anchorNode.className = this.action_block.klass;
287 current_anchor = sel.anchorNode;
290 var range = document.createRange();
291 range.setStart(current_anchor, 0);
292 sel.removeAllRanges();
294 update_block_style_toolbar();
296 function update_block_style_toolbar() {
297 var sel = window.getSelection();
298 if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
299 if (block_style_toolbar) {
300 $(block_style_toolbar).hide();
304 if (block_style_toolbar === null) {
305 block_style_toolbar = $('<div class="block-style-popup"></div>');
306 for (const block of Phylly.BLOCKS) {
307 var button = document.createElement('button');
308 button.action_block = block;
309 button.dataset.action = block.name;
310 button.textContent = block.name;
311 block_style_toolbar.append(button);
313 block_style_toolbar.hide();
314 block_style_toolbar.insertAfter(document.body);
315 block_style_toolbar.find('button').on('click', block_style);
317 block_style_toolbar.css('position', 'absolute');
318 var block = get_active_block(sel.anchorNode);
319 block_style_toolbar.find('button').removeClass('on');
321 block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
322 block_style_toolbar.addClass('selected');
324 block_style_toolbar.removeClass('selected');
326 var anchor = get_contenteditable_subnode(sel.anchorNode);
327 var pos = $(anchor).offset();
328 block_style_toolbar.css('top', pos.top - 33);
329 block_style_toolbar.css('left', pos.left);
330 block_style_toolbar.show();
334 var inline_style_toolbar = null;
335 function update_style() {
336 var action = $(this).data('action');
338 if (action == 'code') {
339 action = 'insertHTML';
340 param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
342 if (action == 'wiki') {
343 action = 'insertHTML';
344 var text = window.getSelection().toString();
345 var $new_link = $('<a></a>', {text: text, href: '#tbd'});
346 var request_id = Math.floor(Math.random() * 10000);
347 $new_link.attr('data-request-id', request_id);
350 params.request_id = request_id;
351 $.post('/wiki/ajax/newpage/', params).success(function(data) {
352 $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
354 param = $new_link[0].outerHTML;
356 if (action == 'createLink') {
357 var sel = window.getSelection();
358 var $input = $('input[name=link-target]');
359 $input[0]._range = sel.getRangeAt(0);
360 if (sel.anchorNode instanceof Element) {
361 var elem = sel.anchorNode.childNodes[sel.anchorOffset];
362 if (elem.tagName == 'A') {
363 $input.val(elem.href);
366 $input.addClass('shown');
370 document.execCommand(action, false, param);
372 function validate_link(ev) {
373 var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
374 if (ev.key == "Enter") {
375 var $input = $(this);
376 var range = this._range;
377 var url = $input.val();
378 $input.removeClass('shown');
379 var sel = window.getSelection();
380 sel.addRange(this._range);
382 document.execCommand('createLink', false, url);
384 document.execCommand('unlink', false, null);
390 function focusout_link(ev) {
391 var $input = $(this);
392 $input.removeClass('shown');
393 var range = this._range;
394 var sel = window.getSelection();
395 sel.addRange(this._range);
398 function show_inline_style_toolbar(sel) {
399 if (inline_style_toolbar === null) {
400 inline_style_toolbar = $('<div class="inline-style-popup">' +
401 '<button data-action="italic" data-accel="i"><i>i</i></button>' +
402 '<button data-action="bold" data-accel="b"><b>b</b></button>' +
403 '<button data-action="code" data-accel="<"><></button>' +
404 '<button data-action="removeFormat" data-accel="m">×</button>' +
405 '<button data-action="createLink">a</button>' +
406 '<input name="link-target"/>' +
408 inline_style_toolbar.hide();
409 inline_style_toolbar.insertAfter(document.body);
410 inline_style_toolbar.find('button').on('click', update_style);
411 inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
413 inline_style_toolbar.css('position', 'absolute');
414 var pos = sel.getRangeAt(0).getClientRects()[0];
415 inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
416 inline_style_toolbar.css('left', pos.left + window.scrollX);
417 inline_style_toolbar.show();
419 }(window, document));
423 $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
424 $('#save').on('click', function() {
425 var text = $('div[contenteditable]')[0].innerHTML;
426 var csrf = $('[name=csrfmiddlewaretoken]').val();
428 { text: text, csrfmiddlewaretoken: csrf}
430 $('#save').css('background', 'red');