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'},
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]);
19 anchorNode.appendChild(empty_p);
20 var range = document.createRange();
21 range.setStart(empty_p, 0);
22 sel.removeAllRanges();
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();
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]);
46 empty_li.appendChild(empty_p);
47 var range = document.createRange();
48 range.setStart(empty_p, 0);
49 sel.removeAllRanges();
52 var prev_p = sel.anchorNode.previousSibling;
54 if (prev_p.tagName != 'P') {
55 prev_p = $(prev_p).parents('p')[0];
56 if (! prev_p || prev_p.tagName != 'P') return;
58 var title_match = prev_p.innerText.match(/^(h[1-6]). /);
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);
69 $(document).on('selectionchange', function(event) {
70 if ($('input[name=link-target].shown').length) {
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();
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);
84 $(document).on('click', 'div.figure span.empty', function() {
85 window.active_figure = this.parentNode;
86 $('#image-upload').trigger('click');
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');
98 bind_events: function(elem) {
99 $(elem).on('input', Phylly.input_event);
100 $(elem).on('keyup click', update_block_style_toolbar);
103 unbind_events: function(elem) {
104 $(elem).off('input');
105 $(elem).off('keyup click');
109 window.Phylly = Phylly;
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');
120 img.setAttribute('data-orig-url', data.orig_url);
122 $(window.active_figure).empty().append(img);
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);
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);
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))
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();
162 update_block_style_toolbar();
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();
179 update_block_style_toolbar();
181 var current_node = sel.anchorNode;
182 var ul = document.createElement('UL');
183 ul.className = 'list';
184 var li = document.createElement('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();
195 if (this.classList.contains('on')) { // toggle off
196 if (this.action_block.subtag) {
198 var main_node = get_contenteditable_subnode(current_anchor);
199 $(current_anchor).detach().insertAfter(main_node);
201 document.execCommand('formatBlock', false, 'p');
202 current_anchor = sel.anchorNode;
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);
212 document.execCommand('formatBlock', false, this.action_block.tag);
213 sel.anchorNode.className = this.action_block.klass;
214 current_anchor = sel.anchorNode;
217 var range = document.createRange();
218 range.setStart(current_anchor, 0);
219 sel.removeAllRanges();
221 update_block_style_toolbar();
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();
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);
240 block_style_toolbar.hide();
241 block_style_toolbar.insertAfter(document.body);
242 block_style_toolbar.find('button').on('click', block_style);
244 block_style_toolbar.css('position', 'absolute');
245 var block = get_active_block(sel.anchorNode);
246 block_style_toolbar.find('button').removeClass('on');
248 block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
249 block_style_toolbar.addClass('selected');
251 block_style_toolbar.removeClass('selected');
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();
261 var inline_style_toolbar = null;
262 function update_style() {
263 var action = $(this).data('action');
265 if (action == 'code') {
266 action = 'insertHTML';
267 param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
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);
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');
281 param = $new_link[0].outerHTML;
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);
293 $input.addClass('shown');
297 document.execCommand(action, false, param);
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);
309 document.execCommand('createLink', false, url);
311 document.execCommand('unlink', false, null);
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);
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"><></button>' +
331 '<button data-action="removeFormat">×</button>' +
332 '<button data-action="createLink">a</button>' +
333 '<input name="link-target"/>' +
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);
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();
346 }(window, document));
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();
355 { text: text, csrfmiddlewaretoken: csrf}
357 $('#save').css('background', 'red');