4b0ffea80cc354a6f7df8a98ab155c2cb9445e2d
[chloro.git] / chloro / phyll / static / js / chloro.js
1 $(function() {
2   var BLOCKS = [
3           {name: 'code', tag: 'PRE', klass: 'screen'},
4           {name: 'figure', tag: 'DIV', subtag: true, klass: 'figure'},
5           {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
6   ];
7   function get_contenteditable_subnode(node) {
8     if (node === null) return null;
9     if (node.parentNode.contentEditable === 'true') return node;
10     return get_contenteditable_subnode(node.parentNode);
11   }
12   function get_active_block(node) {
13     var main_node = get_contenteditable_subnode(node);
14     if (main_node === null) return null;
15     for (const block of BLOCKS) {
16       if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
17         return block;
18     }
19     return null;
20   }
21
22   $('div[contenteditable]').on('input', function(event) {
23     if (event.originalEvent.inputType == "insertParagraph") {
24       var sel = document.getSelection();
25       var anchorNode = sel.anchorNode;
26       var prev_p = sel.anchorNode.previousSibling;
27       if (! prev_p) return;
28       if (prev_p.tagName != 'P') {
29         prev_p = $(prev_p).parents('p')[0];
30       }
31       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
32       if (title_match) {
33         var title = document.createElement(title_match[1]);
34         title.innerHTML = prev_p.innerHTML;
35         title.textContent = title.textContent.slice(4);
36         prev_p.replaceWith(title);
37       }
38     }
39     return true;
40   });
41   $('div[contenteditable]').on('keyup click', update_block_style_popup);
42
43   $('#save').on('click', function() {
44     var text = $('div[contenteditable]')[0].innerHTML;
45     var csrf = $('[name=csrfmiddlewaretoken]').val();
46     $.post('api-save/',
47       { text: text, csrfmiddlewaretoken: csrf}
48     ).fail(function() {
49       $('#save').css('background', 'red');
50     });
51     return false;
52   });
53
54   var block_style_popup = null;
55   function block_style() {
56     var sel = window.getSelection();
57     var current_anchor = sel.anchorNode;
58     if (this.classList.contains('on')) { // toggle off
59       if (this.action_block.subtag) {
60         // unwrap
61         var main_node = get_contenteditable_subnode(current_anchor);
62         $(current_anchor).detach().insertAfter(main_node);
63       } else {
64         document.execCommand('formatBlock', false, 'p');
65         current_anchor = sel.anchorNode;
66       }
67     } else {
68       action = this.action_block.subtag || this.action_block.tag;
69       if (this.action_block.subtag) {
70         // enclose current tag into a new parent;
71         var new_parent = document.createElement(this.action_block.tag);
72         new_parent.className = this.action_block.klass;
73         $(current_anchor).wrap(new_parent);
74       } else {
75         document.execCommand('formatBlock', false, this.action_block.tag);
76         sel.anchorNode.className = this.action_block.klass;
77         current_anchor = sel.anchorNode;
78       }
79     }
80     var range = document.createRange();
81     range.setStart(current_anchor, 0);
82     sel.removeAllRanges();
83     sel.addRange(range);
84     update_block_style_popup();
85   }
86   function update_block_style_popup() {
87     var sel = window.getSelection();
88     if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
89       if (block_style_popup) {
90         $(block_style_popup).hide();
91       }
92       return true;
93     }
94     if (block_style_popup === null) {
95       block_style_popup = $('<div class="block-style-popup"></div>');
96       for (const block of BLOCKS) {
97         var button = document.createElement('button');
98         button.action_block = block;
99         button.dataset.action = block.name;
100         button.textContent = block.name;
101         block_style_popup.append(button);
102       }
103       block_style_popup.hide();
104       block_style_popup.insertAfter(document.body);
105       block_style_popup.find('button').on('click', block_style);
106     }
107     block_style_popup.css('position', 'absolute');
108     var block = get_active_block(sel.anchorNode);
109     block_style_popup.find('button').removeClass('on');
110     if (block) {
111       block_style_popup.find('[data-action=' + block.name + ']').addClass('on');
112       block_style_popup.addClass('selected');
113     } else {
114       block_style_popup.removeClass('selected');
115     }
116     var anchor = get_contenteditable_subnode(sel.anchorNode);
117     var pos = $(anchor).offset();
118     block_style_popup.css('top', pos.top - 33);
119     block_style_popup.css('left', pos.left);
120     block_style_popup.show();
121     return true;
122   }
123
124   var style_popup = null;
125   function update_style() {
126     var action = $(this).data('action');
127     var param = null;
128     if (action == 'code') {
129       action = 'insertHTML';
130       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
131     }
132     if (action == 'createLink') {
133       var sel = window.getSelection();
134       var $input = $('input[name=link-target]');
135       $input[0]._range = sel.getRangeAt(0);
136       if (sel.anchorNode instanceof Element) {
137         var elem = sel.anchorNode.childNodes[sel.anchorOffset];
138         if (elem.tagName == 'A') {
139           $input.val(elem.href);
140         }
141       }
142       $input.addClass('shown');
143       $input.focus();
144       return;
145     }
146     document.execCommand(action, false, param);
147   }
148   function validate_link(ev) {
149     var charCode = typeof ev.which == "number" ? ev.which : ev.keyCode;
150     if (ev.key == "Enter") {
151       var $input = $(this);
152       var range = this._range;
153       var url = $input.val();
154       $input.removeClass('shown');
155       var sel = window.getSelection();
156       sel.addRange(this._range);
157       if (url) {
158         document.execCommand('createLink', false, url);
159       } else {
160         document.execCommand('unlink', false, null);
161       }
162       sel.empty();
163       $input.val('');
164     }
165   }
166   function focusout_link(ev) {
167     var $input = $(this);
168     $input.removeClass('shown');
169     var range = this._range;
170     var sel = window.getSelection();
171     sel.addRange(this._range);
172   }
173
174   function show_style_popup(sel) {
175     if (style_popup === null) {
176       style_popup = $('<div class="inline-style-popup">' +
177                       '<button data-action="italic"><i>i</i></button>' +
178                       '<button data-action="bold"><b>b</b></button>' +
179                       '<button data-action="code">#</button>' +
180                       '<button data-action="removeFormat">×</button>' +
181                       '<button data-action="createLink">a</button>' +
182                       '<input name="link-target"/>' +
183                       '</div>');
184       style_popup.hide();
185       style_popup.insertAfter($('.actions'));
186       style_popup.find('button').on('click', update_style);
187       style_popup.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
188     }
189     style_popup.css('position', 'absolute');
190     var pos = sel.getRangeAt(0).getClientRects()[0];
191     style_popup.css('top', pos.top + window.scrollY - 33);
192     style_popup.css('left', pos.left + window.scrollX);
193     style_popup.show();
194   };
195   $(document).on('selectionchange', function(event) {
196     if ($('input[name=link-target].shown').length) {
197       return;
198     }
199     var sel = window.getSelection();
200     if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
201       show_style_popup(sel);
202     } else if (style_popup) {
203       $(style_popup).hide();
204     }
205   });
206 });