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