]> git.0d.be Git - chloro.git/blob - chloro/phyll/static/js/chloro.js
add editor button for intertitles
[chloro.git] / chloro / phyll / static / js / chloro.js
1 (function(window, document, undefined) {
2   var Phylly = {
3     BLOCKS: [
4           {name: 'intertitle', tag: 'H2', klass: 'intertitle'},
5           {name: 'code', tag: 'PRE', klass: 'screen'},
6           {name: 'list', special: 'list', tag: 'UL', klass: 'list'},
7           {name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
8           {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
9           {name: 'quote', tag: 'BLOCKQUOTE', subtag: true, klass: 'quote'},
10     ],
11     input_event: function(event) {
12       var sel = document.getSelection();
13       var anchorNode = sel.anchorNode;
14       if (sel.anchorNode.contentEditable === 'true' && (
15               sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
16         // when everything has been removed, add back <p><br></p>
17         var empty_p = document.createElement('P');
18         empty_p.appendChild(document.createElement('BR'));
19         if (anchorNode.childNodes.length) { // lone <br>
20           anchorNode.removeChild(anchorNode.childNodes[0]);
21         }
22         anchorNode.appendChild(empty_p);
23         var range = document.createRange();
24         range.setStart(empty_p, 0);
25         sel.removeAllRanges();
26         sel.addRange(range);
27         return;
28       }
29       if (event.originalEvent.inputType == "insertText") {
30         var main_node = get_contenteditable_subnode(sel.anchorNode);
31         if (main_node.tagName != 'PRE') {
32           var anchorNode = sel.anchorNode;
33           var offset = sel.anchorOffset;
34           var orig_text = sel.anchorNode.data;
35           var text = orig_text;
36           // typography
37           if (event.originalEvent.data === "'") {
38             text = text.slice(0, offset-1) + '’' + text.slice(offset);
39           }
40           if (text != orig_text) {
41             var new_text = document.createTextNode(text);
42             anchorNode.replaceWith(new_text);
43             sel.collapse(new_text, offset);
44           }
45         }
46         return;
47       }
48       if (event.originalEvent.inputType != "insertParagraph") return true;
49       if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
50         // new empty div got inserted, replace it with a <p>
51         var empty_p = document.createElement('P');
52         empty_p.appendChild(document.createElement('BR'));
53         var empty_div = sel.anchorNode;
54         empty_div.replaceWith(empty_p);
55         var range = document.createRange();
56         range.setStart(empty_p, 0);
57         sel.removeAllRanges();
58         sel.addRange(range);
59       }
60       if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
61         // new empty li got inserted, insert a <p> within
62         var empty_p = document.createElement('P');
63         empty_p.appendChild(document.createElement('BR'));
64         var empty_li = anchorNode;
65         if (empty_li.childNodes.length) { // lone <br>
66           empty_li.removeChild(empty_li.childNodes[0]);
67         }
68         empty_li.appendChild(empty_p);
69         var range = document.createRange();
70         range.setStart(empty_p, 0);
71         sel.removeAllRanges();
72         sel.addRange(range);
73       }
74       var prev_p = sel.anchorNode.previousSibling;
75       if (! prev_p) return;
76       if (prev_p.tagName != 'P') {
77         prev_p = $(prev_p).parents('p')[0];
78         if (! prev_p || prev_p.tagName != 'P') return;
79       }
80       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
81       if (title_match) {
82         var title = document.createElement(title_match[1]);
83         title.innerHTML = prev_p.innerHTML;
84         title.textContent = title.textContent.slice(4);
85         prev_p.replaceWith(title);
86       }
87       return true;
88     },
89
90     init: function() {
91       $(document).on('selectionchange', function(event) {
92         if ($('input[name=link-target].shown').length) {
93           return;
94         }
95         var sel = window.getSelection();
96         if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
97           show_inline_style_toolbar(sel);
98         } else if (inline_style_toolbar) {
99           $(inline_style_toolbar).hide();
100         }
101       });
102       var $image_upload = $('<input type="file" nam="image" id="image-upload">');
103       $image_upload.on('change', upload_image);
104       $image_upload.appendTo(document.body);
105
106       var $document_upload = $('<input type="file" nam="document" id="document-upload">');
107       $document_upload.on('change', upload_document);
108       $document_upload.appendTo(document.body);
109
110       document.execCommand('defaultParagraphSeparator', false, 'p');
111       $(document).on('click', 'div.figure span.empty', function() {
112         window.active_figure = this.parentNode;
113         $('#image-upload').trigger('click');
114         return true;
115       });
116       $(document).on('click', 'div.document span.empty', function() {
117         window.active_document = this.parentNode;
118         $('#document-upload').trigger('click');
119         return true;
120       });
121     },
122
123     off: function() {
124       $('#image-upload').remove();
125       $('#document-upload').remove();
126       if (block_style_toolbar) { block_style_toolbar.hide(); }
127       if (inline_style_toolbar) { inline_style_toolbar.hide(); }
128       $(document).off('selectionchange');
129     },
130
131     window_keypress: function(ev) {
132       if (inline_style_toolbar && inline_style_toolbar.is(':visible')) {
133         if (event.ctrlKey || event.metaKey) {
134           var key = String.fromCharCode(event.which).toLowerCase();
135           var button = inline_style_toolbar.find('[data-accel="' + key + '"]').first();
136           if (button.length) {
137             button.trigger('click');
138             ev.preventDefault();
139           }
140         }
141       }
142     },
143
144     bind_events: function(elem) {
145       $(elem).on('input', Phylly.input_event);
146       $(elem).on('keyup click', update_block_style_toolbar);
147       $(window).on('keydown', this.window_keypress);
148     },
149
150     unbind_events: function(elem) {
151       $(elem).off('input');
152       $(elem).off('keyup click');
153       $(window).off('keydown', this.window_keypress);
154     },
155
156   }
157   window.Phylly = Phylly;
158
159   function upload_image() {
160     if ($(this).prop('files').length > 0) {
161       var file = $(this).prop('files')[0];
162       var params = new FormData();
163       params.append('upload', file);
164       $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
165         var img = document.createElement('IMG');
166         img.src = data.url;
167         if (data.orig_url) {
168           img.setAttribute('data-orig-url', data.orig_url);
169         }
170         $(window.active_figure).empty().append(img);
171       });
172     }
173   }
174
175   function upload_document() {
176     if ($(this).prop('files').length > 0) {
177       var file = $(this).prop('files')[0];
178       var params = new FormData();
179       params.append('upload', file);
180       $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
181         var doc_link = document.createElement('A');
182         doc_link.className = 'button';
183         doc_link.textContent = 'Télécharger ' + data.filename;
184         doc_link.href = data.url;
185         $(window.active_document).empty().append(doc_link);
186       });
187     }
188   }
189
190   function get_contenteditable_subnode(node) {
191     if (node === null) return null;
192     if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
193     if (node.parentNode.contentEditable === 'true') return node;
194     return get_contenteditable_subnode(node.parentNode);
195   }
196   function get_parent(node, type) {
197    if (node === null) return null;
198    if (node.tagName == type) return node;
199    return get_parent(node.parentNode, type);
200   }
201   function get_active_block(node) {
202     var main_node = get_contenteditable_subnode(node);
203     if (main_node === null) return null;
204     for (const block of Phylly.BLOCKS) {
205       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
206         return block;
207     }
208     return null;
209   }
210
211   var block_style_toolbar = null;
212   function block_style() {
213     var sel = window.getSelection();
214     var current_anchor = sel.anchorNode;
215     if (this.action_block.special == 'img') {
216       action = 'insertHTML';
217       param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
218       document.execCommand(action, false, param);
219       current_anchor = $('#new-p')[0];
220       $(current_anchor).attr('id', null);
221       var range = document.createRange();
222       range.setStart(current_anchor, 0);
223       sel.removeAllRanges();
224       sel.addRange(range);
225       update_block_style_toolbar();
226       return;
227     }
228     if (this.action_block.special == 'doc') {
229       action = 'insertHTML';
230       param = '<div class="document"><span class="empty"></span></div><p id="new-p"></p>';
231       document.execCommand(action, false, param);
232       current_anchor = $('#new-p')[0];
233       $(current_anchor).attr('id', null);
234       var range = document.createRange();
235       range.setStart(current_anchor, 0);
236       sel.removeAllRanges();
237       sel.addRange(range);
238       update_block_style_toolbar();
239       return;
240     }
241     if (this.action_block.special == 'list') {
242       if (this.classList.contains('on')) { // toggle off
243         var main_node = get_contenteditable_subnode(sel.anchorNode);
244         var li = get_parent(sel.anchorNode, 'LI');
245         for (var i=li.childNodes.length; i>0; i--) {
246           var child = li.childNodes[i-1];
247           main_node.insertAdjacentElement('afterend', child);
248           var range = document.createRange();
249           range.setStart(child, 0);
250           sel.removeAllRanges();
251           sel.addRange(range);
252         }
253         li.remove();
254         update_block_style_toolbar();
255       } else {
256         var current_node = sel.anchorNode;
257         var ul = document.createElement('UL');
258         ul.className = 'list';
259         var li = document.createElement('LI');
260         ul.appendChild(li);
261         sel.anchorNode.parentNode.insertBefore(ul, current_node);
262         li.appendChild(current_node);
263         var range = document.createRange();
264         range.setStart(current_node, 0);
265         sel.removeAllRanges();
266         sel.addRange(range);
267       }
268       return;
269     }
270     if (this.classList.contains('on')) { // toggle off
271       if (this.action_block.subtag) {
272         // unwrap
273         var main_node = get_contenteditable_subnode(current_anchor);
274         $(current_anchor).detach().insertAfter(main_node);
275       } else {
276         document.execCommand('formatBlock', false, 'p');
277         current_anchor = sel.anchorNode;
278       }
279     } else {
280       action = this.action_block.subtag || this.action_block.tag;
281       if (this.action_block.subtag) {
282         // enclose current tag into a new parent;
283         var new_parent = document.createElement(this.action_block.tag);
284         new_parent.className = this.action_block.klass;
285         $(current_anchor).wrap(new_parent);
286       } else {
287         document.execCommand('formatBlock', false, this.action_block.tag);
288         sel.anchorNode.className = this.action_block.klass;
289         current_anchor = sel.anchorNode;
290       }
291     }
292     var range = document.createRange();
293     range.setStart(current_anchor, 0);
294     sel.removeAllRanges();
295     sel.addRange(range);
296     update_block_style_toolbar();
297   }
298   function update_block_style_toolbar() {
299     var sel = window.getSelection();
300     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
301       if (block_style_toolbar) {
302         $(block_style_toolbar).hide();
303       }
304       return true;
305     }
306     if (block_style_toolbar === null) {
307       block_style_toolbar = $('<div class="block-style-popup"></div>');
308       for (const block of Phylly.BLOCKS) {
309         var button = document.createElement('button');
310         button.action_block = block;
311         button.dataset.action = block.name;
312         button.textContent = block.name;
313         block_style_toolbar.append(button);
314       }
315       block_style_toolbar.hide();
316       block_style_toolbar.insertAfter(document.body);
317       block_style_toolbar.find('button').on('click', block_style);
318     }
319     block_style_toolbar.css('position', 'absolute');
320     var block = get_active_block(sel.anchorNode);
321     block_style_toolbar.find('button').removeClass('on');
322     if (block) {
323       block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
324       block_style_toolbar.addClass('selected');
325     } else {
326       block_style_toolbar.removeClass('selected');
327     }
328     var anchor = get_contenteditable_subnode(sel.anchorNode);
329     var pos = $(anchor).offset();
330     block_style_toolbar.css('top', pos.top - 33);
331     block_style_toolbar.css('left', pos.left);
332     block_style_toolbar.show();
333     return true;
334   }
335
336   var inline_style_toolbar = null;
337   function update_style() {
338     var action = $(this).data('action');
339     var param = null;
340     if (action == 'code') {
341       action = 'insertHTML';
342       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
343     }
344     if (action == 'wiki') {
345       action = 'insertHTML';
346       var text = window.getSelection().toString();
347       var $new_link = $('<a></a>', {text: text, href: '#tbd'});
348       var request_id = Math.floor(Math.random() * 10000);
349       $new_link.attr('data-request-id', request_id);
350       var params = {};
351       params.title = text;
352       params.request_id = request_id;
353       $.post('/ajax/newpage/', params).done(function(data) {
354         $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
355       });
356       param = $new_link[0].outerHTML;
357     }
358     if (action == 'createLink') {
359       var sel = window.getSelection();
360       var selected_link = get_parent(sel.anchorNode, 'A');
361       if (sel.anchorNode.nodeType == Node.TEXT_NODE) {
362         if (sel.anchorNode.length == sel.anchorOffset && sel.anchorNode.nextSibling.nodeName == 'A') {
363           selected_link = sel.anchorNode.nextSibling;
364         }
365       }
366       var $input = $('input[name=link-target]');
367       $input[0]._range = sel.getRangeAt(0);
368       if (selected_link) {
369         $input[0]._selected_link = selected_link;
370         $input.val(selected_link.href);
371       }
372       $input.addClass('shown');
373       $input.focus();
374       return;
375     }
376     document.execCommand(action, false, param);
377   }
378   function validate_link(ev) {
379     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
380     if (ev.key == "Enter") {
381       var $input = $(this);
382       var range = this._range;
383       var url = $input.val();
384       $input.removeClass('shown');
385       var sel = window.getSelection();
386       sel.addRange(this._range);
387       var selected_link = $input[0]._selected_link;
388       if (url) {
389         if (selected_link) {
390           selected_link.href = url;
391         } else {
392           var $new_link = $('<a></a>', {text: sel.toString(), href: url});
393           this._range.deleteContents();
394           this._range.insertNode($new_link[0]);
395           sel.empty();
396           sel.collapse($new_link[0]);
397           sel.empty();
398         }
399       } else {
400         if (selected_link) {
401           selected_link.replaceWith(document.createTextNode(selected_link.textContent));
402         }
403       }
404       $input.val('');
405       $input[0]._selected_link = null;
406     }
407   }
408   function focusout_link(ev) {
409     var $input = $(this);
410     $input.removeClass('shown');
411     var range = this._range;
412     var sel = window.getSelection();
413     sel.addRange(this._range);
414   }
415
416   function show_inline_style_toolbar(sel) {
417     if (inline_style_toolbar === null) {
418       inline_style_toolbar = $('<div class="inline-style-popup">' +
419                       '<button data-action="italic" data-accel="i"><i>i</i></button>' +
420                       '<button data-action="bold" data-accel="b"><b>b</b></button>' +
421                       '<button data-action="code" data-accel="<">&lt;&gt;</button>' +
422                       '<button data-action="removeFormat" data-accel="m">×</button>' +
423                       '<button data-action="wiki">W</button>' +
424                       '<button data-action="createLink">a</button>' +
425                       '<input name="link-target"/>' +
426                       '</div>');
427       inline_style_toolbar.hide();
428       inline_style_toolbar.insertAfter(document.body);
429       inline_style_toolbar.find('button').on('click', update_style);
430       inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
431     }
432     inline_style_toolbar.css('position', 'absolute');
433     var pos = sel.getRangeAt(0).getClientRects()[0];
434     inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
435     inline_style_toolbar.css('left', pos.left + window.scrollX);
436     inline_style_toolbar.show();
437   };
438 }(window, document));
439
440 $(function() {
441   Phylly.init(),
442   $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
443   $('#save').on('click', function() {
444     var text = $('div[contenteditable]')[0].innerHTML;
445     var csrf = $('[name=csrfmiddlewaretoken]').val();
446     $.post('api-save/',
447       { text: text, csrfmiddlewaretoken: csrf}
448     ).fail(function() {
449       $('#save').css('background', 'red');
450     });
451     return false;
452   });
453 });