6b831ac2588c7f92e10609c2a5b643512a0659a7
[chloro.git] / chloro / phyll / static / js / chloro.js
1 (function(window, document, undefined) {
2   var Phylly = {
3     BLOCKS: [
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     ],
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]);
18         }
19         anchorNode.appendChild(empty_p);
20         var range = document.createRange();
21         range.setStart(empty_p, 0);
22         sel.removeAllRanges();
23         sel.addRange(range);
24         return;
25       }
26       if (event.originalEvent.inputType != "insertParagraph") return true;
27       if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
28         // new empty div got inserted, replace it with a <p>
29         var empty_p = document.createElement('P');
30         empty_p.appendChild(document.createElement('BR'));
31         var empty_div = sel.anchorNode;
32         empty_div.replaceWith(empty_p);
33         var range = document.createRange();
34         range.setStart(empty_p, 0);
35         sel.removeAllRanges();
36         sel.addRange(range);
37       }
38       if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
39         // new empty li got inserted, insert a <p> within
40         var empty_p = document.createElement('P');
41         empty_p.appendChild(document.createElement('BR'));
42         var empty_li = anchorNode;
43         if (empty_li.childNodes.length) { // lone <br>
44           empty_li.removeChild(empty_li.childNodes[0]);
45         }
46         empty_li.appendChild(empty_p);
47         var range = document.createRange();
48         range.setStart(empty_p, 0);
49         sel.removeAllRanges();
50         sel.addRange(range);
51       }
52       var prev_p = sel.anchorNode.previousSibling;
53       if (! prev_p) return;
54       if (prev_p.tagName != 'P') {
55         prev_p = $(prev_p).parents('p')[0];
56         if (! prev_p || prev_p.tagName != 'P') return;
57       }
58       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
59       if (title_match) {
60         var title = document.createElement(title_match[1]);
61         title.innerHTML = prev_p.innerHTML;
62         title.textContent = title.textContent.slice(4);
63         prev_p.replaceWith(title);
64       }
65       return true;
66     },
67
68     init: function() {
69       $(document).on('selectionchange', function(event) {
70         if ($('input[name=link-target].shown').length) {
71           return;
72         }
73         var sel = window.getSelection();
74         if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
75           show_inline_style_toolbar(sel);
76         } else if (inline_style_toolbar) {
77           $(inline_style_toolbar).hide();
78         }
79       });
80       var $image_upload = $('<input type="file" nam="image" id="image-upload">');
81       $image_upload.on('change', upload_image);
82       $image_upload.appendTo(document.body);
83
84       $(document).on('click', 'div.figure span.empty', function() {
85         window.active_figure = this.parentNode;
86         $('#image-upload').trigger('click');
87         return true;
88       });
89     },
90
91     off: function() {
92       $('#image-upload').remove();
93       if (block_style_toolbar) { block_style_toolbar.hide(); }
94       if (inline_style_toolbar) { inline_style_toolbar.hide(); }
95       $(document).off('selectionchange');
96     },
97
98     bind_events: function(elem) {
99       $(elem).on('input', Phylly.input_event);
100       $(elem).on('keyup click', update_block_style_toolbar);
101     },
102
103     unbind_events: function(elem) {
104       $(elem).off('input');
105       $(elem).off('keyup click');
106     },
107
108   }
109   window.Phylly = Phylly;
110
111   function upload_image() {
112     if ($(this).prop('files').length > 0) {
113       var file = $(this).prop('files')[0];
114       var params = new FormData();
115       params.append('image', file);
116       $.post({url: '/wiki/ajax/image/', processData: false, data: params, contentType: false}).success(function(data) {
117         var img = document.createElement('IMG');
118         img.src = data.url;
119         if (data.orig_url) {
120           img.setAttribute('data-orig-url', data.orig_url);
121         }
122         $(window.active_figure).empty().append(img);
123       });
124     }
125   }
126
127   function get_contenteditable_subnode(node) {
128     if (node === null) return null;
129     if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
130     if (node.parentNode.contentEditable === 'true') return node;
131     return get_contenteditable_subnode(node.parentNode);
132   }
133   function get_parent(node, type) {
134    if (node === null) return null;
135    if (node.tagName == type) return node;
136    return get_parent(node.parentNode, type);
137   }
138   function get_active_block(node) {
139     var main_node = get_contenteditable_subnode(node);
140     if (main_node === null) return null;
141     for (const block of Phylly.BLOCKS) {
142       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
143         return block;
144     }
145     return null;
146   }
147
148   var block_style_toolbar = null;
149   function block_style() {
150     var sel = window.getSelection();
151     var current_anchor = sel.anchorNode;
152     if (this.action_block.special == 'img') {
153       action = 'insertHTML';
154       param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
155       document.execCommand(action, false, param);
156       current_anchor = $('#new-p')[0];
157       $(current_anchor).attr('id', null);
158       var range = document.createRange();
159       range.setStart(current_anchor, 0);
160       sel.removeAllRanges();
161       sel.addRange(range);
162       update_block_style_toolbar();
163
164       return;
165     }
166     if (this.action_block.special == 'list') {
167       if (this.classList.contains('on')) { // toggle off
168         var main_node = get_contenteditable_subnode(sel.anchorNode);
169         var li = get_parent(sel.anchorNode, 'LI');
170         for (var i=li.childNodes.length; i>0; i--) {
171           var child = li.childNodes[i-1];
172           main_node.insertAdjacentElement('afterend', child);
173           var range = document.createRange();
174           range.setStart(child, 0);
175           sel.removeAllRanges();
176           sel.addRange(range);
177         }
178         li.remove();
179         update_block_style_toolbar();
180       } else {
181         var current_node = sel.anchorNode;
182         var ul = document.createElement('UL');
183         ul.className = 'list';
184         var li = document.createElement('LI');
185         ul.appendChild(li);
186         sel.anchorNode.parentNode.insertBefore(ul, current_node);
187         li.appendChild(current_node);
188         var range = document.createRange();
189         range.setStart(current_node, 0);
190         sel.removeAllRanges();
191         sel.addRange(range);
192       }
193       return;
194     }
195     if (this.classList.contains('on')) { // toggle off
196       if (this.action_block.subtag) {
197         // unwrap
198         var main_node = get_contenteditable_subnode(current_anchor);
199         $(current_anchor).detach().insertAfter(main_node);
200       } else {
201         document.execCommand('formatBlock', false, 'p');
202         current_anchor = sel.anchorNode;
203       }
204     } else {
205       action = this.action_block.subtag || this.action_block.tag;
206       if (this.action_block.subtag) {
207         // enclose current tag into a new parent;
208         var new_parent = document.createElement(this.action_block.tag);
209         new_parent.className = this.action_block.klass;
210         $(current_anchor).wrap(new_parent);
211       } else {
212         document.execCommand('formatBlock', false, this.action_block.tag);
213         sel.anchorNode.className = this.action_block.klass;
214         current_anchor = sel.anchorNode;
215       }
216     }
217     var range = document.createRange();
218     range.setStart(current_anchor, 0);
219     sel.removeAllRanges();
220     sel.addRange(range);
221     update_block_style_toolbar();
222   }
223   function update_block_style_toolbar() {
224     var sel = window.getSelection();
225     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
226       if (block_style_toolbar) {
227         $(block_style_toolbar).hide();
228       }
229       return true;
230     }
231     if (block_style_toolbar === null) {
232       block_style_toolbar = $('<div class="block-style-popup"></div>');
233       for (const block of Phylly.BLOCKS) {
234         var button = document.createElement('button');
235         button.action_block = block;
236         button.dataset.action = block.name;
237         button.textContent = block.name;
238         block_style_toolbar.append(button);
239       }
240       block_style_toolbar.hide();
241       block_style_toolbar.insertAfter(document.body);
242       block_style_toolbar.find('button').on('click', block_style);
243     }
244     block_style_toolbar.css('position', 'absolute');
245     var block = get_active_block(sel.anchorNode);
246     block_style_toolbar.find('button').removeClass('on');
247     if (block) {
248       block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
249       block_style_toolbar.addClass('selected');
250     } else {
251       block_style_toolbar.removeClass('selected');
252     }
253     var anchor = get_contenteditable_subnode(sel.anchorNode);
254     var pos = $(anchor).offset();
255     block_style_toolbar.css('top', pos.top - 33);
256     block_style_toolbar.css('left', pos.left);
257     block_style_toolbar.show();
258     return true;
259   }
260
261   var inline_style_toolbar = null;
262   function update_style() {
263     var action = $(this).data('action');
264     var param = null;
265     if (action == 'code') {
266       action = 'insertHTML';
267       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
268     }
269     if (action == 'wiki') {
270       action = 'insertHTML';
271       var text = window.getSelection().toString();
272       var $new_link = $('<a></a>', {text: text, href: '#tbd'});
273       var request_id = Math.floor(Math.random() * 10000);
274       $new_link.attr('data-request-id', request_id);
275       var params = {};
276       params.title = text;
277       params.request_id = request_id;
278       $.post('/wiki/ajax/newpage/', params).success(function(data) {
279         $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
280       });
281       param = $new_link[0].outerHTML;
282     }
283     if (action == 'createLink') {
284       var sel = window.getSelection();
285       var $input = $('input[name=link-target]');
286       $input[0]._range = sel.getRangeAt(0);
287       if (sel.anchorNode instanceof Element) {
288         var elem = sel.anchorNode.childNodes[sel.anchorOffset];
289         if (elem.tagName == 'A') {
290           $input.val(elem.href);
291         }
292       }
293       $input.addClass('shown');
294       $input.focus();
295       return;
296     }
297     document.execCommand(action, false, param);
298   }
299   function validate_link(ev) {
300     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
301     if (ev.key == "Enter") {
302       var $input = $(this);
303       var range = this._range;
304       var url = $input.val();
305       $input.removeClass('shown');
306       var sel = window.getSelection();
307       sel.addRange(this._range);
308       if (url) {
309         document.execCommand('createLink', false, url);
310       } else {
311         document.execCommand('unlink', false, null);
312       }
313       sel.empty();
314       $input.val('');
315     }
316   }
317   function focusout_link(ev) {
318     var $input = $(this);
319     $input.removeClass('shown');
320     var range = this._range;
321     var sel = window.getSelection();
322     sel.addRange(this._range);
323   }
324
325   function show_inline_style_toolbar(sel) {
326     if (inline_style_toolbar === null) {
327       inline_style_toolbar = $('<div class="inline-style-popup">' +
328                       '<button data-action="italic"><i>i</i></button>' +
329                       '<button data-action="bold"><b>b</b></button>' +
330                       '<button data-action="code">&lt;&gt;</button>' +
331                       '<button data-action="removeFormat">×</button>' +
332                       '<button data-action="createLink">a</button>' +
333                       '<input name="link-target"/>' +
334                       '</div>');
335       inline_style_toolbar.hide();
336       inline_style_toolbar.insertAfter(document.body);
337       inline_style_toolbar.find('button').on('click', update_style);
338       inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
339     }
340     inline_style_toolbar.css('position', 'absolute');
341     var pos = sel.getRangeAt(0).getClientRects()[0];
342     inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
343     inline_style_toolbar.css('left', pos.left + window.scrollX);
344     inline_style_toolbar.show();
345   };
346 }(window, document));
347
348 $(function() {
349   Phylly.init(),
350   $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
351   $('#save').on('click', function() {
352     var text = $('div[contenteditable]')[0].innerHTML;
353     var csrf = $('[name=csrfmiddlewaretoken]').val();
354     $.post('api-save/',
355       { text: text, csrfmiddlewaretoken: csrf}
356     ).fail(function() {
357       $('#save').css('background', 'red');
358     });
359     return false;
360   });
361 });