]> git.0d.be Git - chloro.git/blobdiff - chloro/phyll/static/js/chloro.js
sync live edit js from panikdb
[chloro.git] / chloro / phyll / static / js / chloro.js
index 4030c7c600ab6a1c87bb2103621972a2c01b3133..6b831ac2588c7f92e10609c2a5b643512a0659a7 100644 (file)
@@ -1,12 +1,59 @@
-$(function() {
-  $('div[contenteditable]').on('input', function(event) {
-    if (event.originalEvent.inputType == "insertParagraph") {
+(function(window, document, undefined) {
+  var Phylly = {
+    BLOCKS: [
+          {name: 'code', tag: 'PRE', klass: 'screen'},
+          {name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
+          {name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
+    ],
+    input_event: function(event) {
       var sel = document.getSelection();
       var anchorNode = sel.anchorNode;
+      if (sel.anchorNode.contentEditable === 'true' && (
+              sel.anchorNode.innerHTML == '<br>' || !sel.anchorNode.innerHTML)) {
+        // when everything has been removed, add back <p><br></p>
+        var empty_p = document.createElement('P');
+        empty_p.appendChild(document.createElement('BR'));
+        if (anchorNode.childNodes.length) { // lone <br>
+          anchorNode.removeChild(anchorNode.childNodes[0]);
+        }
+        anchorNode.appendChild(empty_p);
+        var range = document.createRange();
+        range.setStart(empty_p, 0);
+        sel.removeAllRanges();
+        sel.addRange(range);
+        return;
+      }
+      if (event.originalEvent.inputType != "insertParagraph") return true;
+      if (sel.anchorNode.tagName == "DIV" && sel.anchorNode.innerHTML == "<br>") {
+        // new empty div got inserted, replace it with a <p>
+        var empty_p = document.createElement('P');
+        empty_p.appendChild(document.createElement('BR'));
+        var empty_div = sel.anchorNode;
+        empty_div.replaceWith(empty_p);
+        var range = document.createRange();
+        range.setStart(empty_p, 0);
+        sel.removeAllRanges();
+        sel.addRange(range);
+      }
+      if (sel.anchorNode.tagName == "LI" && sel.anchorNode.innerHTML == "<br>") {
+        // new empty li got inserted, insert a <p> within
+        var empty_p = document.createElement('P');
+        empty_p.appendChild(document.createElement('BR'));
+        var empty_li = anchorNode;
+        if (empty_li.childNodes.length) { // lone <br>
+          empty_li.removeChild(empty_li.childNodes[0]);
+        }
+        empty_li.appendChild(empty_p);
+        var range = document.createRange();
+        range.setStart(empty_p, 0);
+        sel.removeAllRanges();
+        sel.addRange(range);
+      }
       var prev_p = sel.anchorNode.previousSibling;
       if (! prev_p) return;
       if (prev_p.tagName != 'P') {
         prev_p = $(prev_p).parents('p')[0];
+        if (! prev_p || prev_p.tagName != 'P') return;
       }
       var title_match = prev_p.innerText.match(/^(h[1-6]). /);
       if (title_match) {
@@ -15,74 +62,203 @@ $(function() {
         title.textContent = title.textContent.slice(4);
         prev_p.replaceWith(title);
       }
+      return true;
+    },
+
+    init: function() {
+      $(document).on('selectionchange', function(event) {
+        if ($('input[name=link-target].shown').length) {
+          return;
+        }
+        var sel = window.getSelection();
+        if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
+          show_inline_style_toolbar(sel);
+        } else if (inline_style_toolbar) {
+          $(inline_style_toolbar).hide();
+        }
+      });
+      var $image_upload = $('<input type="file" nam="image" id="image-upload">');
+      $image_upload.on('change', upload_image);
+      $image_upload.appendTo(document.body);
+
+      $(document).on('click', 'div.figure span.empty', function() {
+        window.active_figure = this.parentNode;
+        $('#image-upload').trigger('click');
+        return true;
+      });
+    },
+
+    off: function() {
+      $('#image-upload').remove();
+      if (block_style_toolbar) { block_style_toolbar.hide(); }
+      if (inline_style_toolbar) { inline_style_toolbar.hide(); }
+      $(document).off('selectionchange');
+    },
+
+    bind_events: function(elem) {
+      $(elem).on('input', Phylly.input_event);
+      $(elem).on('keyup click', update_block_style_toolbar);
+    },
+
+    unbind_events: function(elem) {
+      $(elem).off('input');
+      $(elem).off('keyup click');
+    },
+
+  }
+  window.Phylly = Phylly;
+
+  function upload_image() {
+    if ($(this).prop('files').length > 0) {
+      var file = $(this).prop('files')[0];
+      var params = new FormData();
+      params.append('image', file);
+      $.post({url: '/wiki/ajax/image/', processData: false, data: params, contentType: false}).success(function(data) {
+        var img = document.createElement('IMG');
+        img.src = data.url;
+        if (data.orig_url) {
+          img.setAttribute('data-orig-url', data.orig_url);
+        }
+        $(window.active_figure).empty().append(img);
+      });
     }
-    return true;
-  });
-  $('div[contenteditable]').on('keyup', function(event) {
-    var sel = document.getSelection();
-    if (sel.anchorNode instanceof Element && sel.anchorOffset == 0 && sel.isCollapsed) {
-      // start of line
-      show_block_style_popup();
-    } else {
-      hide_block_style_popup();
-    }
-    return true;
-  });
+  }
 
-  $('#save').on('click', function() {
-    var text = $('div[contenteditable]')[0].innerHTML;
-    var csrf = $('[name=csrfmiddlewaretoken]').val();
-    $.post('api-save/',
-      { text: text, csrfmiddlewaretoken: csrf}
-    ).fail(function() {
-      $('#save').css('background', 'red');
-    });
-    return false;
-  });
+  function get_contenteditable_subnode(node) {
+    if (node === null) return null;
+    if (node.contentEditable === 'true') return node;  // but we shouldn't arrive at root
+    if (node.parentNode.contentEditable === 'true') return node;
+    return get_contenteditable_subnode(node.parentNode);
+  }
+  function get_parent(node, type) {
+   if (node === null) return null;
+   if (node.tagName == type) return node;
+   return get_parent(node.parentNode, type);
+  }
+  function get_active_block(node) {
+    var main_node = get_contenteditable_subnode(node);
+    if (main_node === null) return null;
+    for (const block of Phylly.BLOCKS) {
+      if (main_node.tagName === block.tag && main_node.classList.contains(block.klass))
+        return block;
+    }
+    return null;
+  }
 
-  var block_style_popup = null;
+  var block_style_toolbar = null;
   function block_style() {
-    var action = $(this).data('action');
-    var class_name = null;
-    if (action == 'code') {
-      action = 'pre';
-      class_name = 'screen';
-    }
-    document.execCommand('formatBlock', false, action);
     var sel = window.getSelection();
-    if (class_name) {
-      $(sel.anchorNode).addClass(class_name);
+    var current_anchor = sel.anchorNode;
+    if (this.action_block.special == 'img') {
+      action = 'insertHTML';
+      param = '<div class="figure"><span class="empty"></span></div><p id="new-p"></p>';
+      document.execCommand(action, false, param);
+      current_anchor = $('#new-p')[0];
+      $(current_anchor).attr('id', null);
+      var range = document.createRange();
+      range.setStart(current_anchor, 0);
+      sel.removeAllRanges();
+      sel.addRange(range);
+      update_block_style_toolbar();
+
+      return;
+    }
+    if (this.action_block.special == 'list') {
+      if (this.classList.contains('on')) { // toggle off
+        var main_node = get_contenteditable_subnode(sel.anchorNode);
+        var li = get_parent(sel.anchorNode, 'LI');
+        for (var i=li.childNodes.length; i>0; i--) {
+          var child = li.childNodes[i-1];
+          main_node.insertAdjacentElement('afterend', child);
+          var range = document.createRange();
+          range.setStart(child, 0);
+          sel.removeAllRanges();
+          sel.addRange(range);
+        }
+        li.remove();
+        update_block_style_toolbar();
+      } else {
+        var current_node = sel.anchorNode;
+        var ul = document.createElement('UL');
+        ul.className = 'list';
+        var li = document.createElement('LI');
+        ul.appendChild(li);
+        sel.anchorNode.parentNode.insertBefore(ul, current_node);
+        li.appendChild(current_node);
+        var range = document.createRange();
+        range.setStart(current_node, 0);
+        sel.removeAllRanges();
+        sel.addRange(range);
+      }
+      return;
+    }
+    if (this.classList.contains('on')) { // toggle off
+      if (this.action_block.subtag) {
+        // unwrap
+        var main_node = get_contenteditable_subnode(current_anchor);
+        $(current_anchor).detach().insertAfter(main_node);
+      } else {
+        document.execCommand('formatBlock', false, 'p');
+        current_anchor = sel.anchorNode;
+      }
+    } else {
+      action = this.action_block.subtag || this.action_block.tag;
+      if (this.action_block.subtag) {
+        // enclose current tag into a new parent;
+        var new_parent = document.createElement(this.action_block.tag);
+        new_parent.className = this.action_block.klass;
+        $(current_anchor).wrap(new_parent);
+      } else {
+        document.execCommand('formatBlock', false, this.action_block.tag);
+        sel.anchorNode.className = this.action_block.klass;
+        current_anchor = sel.anchorNode;
+      }
     }
     var range = document.createRange();
-    range.setStart(sel.anchorNode, 0);
+    range.setStart(current_anchor, 0);
     sel.removeAllRanges();
     sel.addRange(range);
-    hide_block_style_popup();
+    update_block_style_toolbar();
   }
-  function show_block_style_popup() {
-    if (block_style_popup === null) {
-      block_style_popup = $(
-                      '<div class="style-popup">' +
-                      '<button data-action="code">code</button>' +
-                      '</div>');
-      block_style_popup.hide();
-      block_style_popup.insertAfter($('.actions'));
-      block_style_popup.find('button').on('click', block_style);
-    }
-    block_style_popup.css('position', 'absolute');
+  function update_block_style_toolbar() {
     var sel = window.getSelection();
-    var pos = $(sel.anchorNode).offset();
-    block_style_popup.css('top', pos.top - 33);
-    block_style_popup.css('left', pos.left);
-    block_style_popup.show();
-  }
-  function hide_block_style_popup() {
-    if (block_style_popup) {
-      $(block_style_popup).hide();
+    if (! ((sel.anchorNode instanceof Element && (sel.anchorOffset == 0 && sel.isCollapsed)) || get_active_block(sel.anchorNode))) {
+      if (block_style_toolbar) {
+        $(block_style_toolbar).hide();
+      }
+      return true;
+    }
+    if (block_style_toolbar === null) {
+      block_style_toolbar = $('<div class="block-style-popup"></div>');
+      for (const block of Phylly.BLOCKS) {
+        var button = document.createElement('button');
+        button.action_block = block;
+        button.dataset.action = block.name;
+        button.textContent = block.name;
+        block_style_toolbar.append(button);
+      }
+      block_style_toolbar.hide();
+      block_style_toolbar.insertAfter(document.body);
+      block_style_toolbar.find('button').on('click', block_style);
     }
+    block_style_toolbar.css('position', 'absolute');
+    var block = get_active_block(sel.anchorNode);
+    block_style_toolbar.find('button').removeClass('on');
+    if (block) {
+      block_style_toolbar.find('[data-action=' + block.name + ']').addClass('on');
+      block_style_toolbar.addClass('selected');
+    } else {
+      block_style_toolbar.removeClass('selected');
+    }
+    var anchor = get_contenteditable_subnode(sel.anchorNode);
+    var pos = $(anchor).offset();
+    block_style_toolbar.css('top', pos.top - 33);
+    block_style_toolbar.css('left', pos.left);
+    block_style_toolbar.show();
+    return true;
   }
 
-  var style_popup = null;
+  var inline_style_toolbar = null;
   function update_style() {
     var action = $(this).data('action');
     var param = null;
@@ -90,6 +266,20 @@ $(function() {
       action = 'insertHTML';
       param = $('<code></code>', {text: window.getSelection().toString()})[0].outerHTML;
     }
+    if (action == 'wiki') {
+      action = 'insertHTML';
+      var text = window.getSelection().toString();
+      var $new_link = $('<a></a>', {text: text, href: '#tbd'});
+      var request_id = Math.floor(Math.random() * 10000);
+      $new_link.attr('data-request-id', request_id);
+      var params = {};
+      params.title = text;
+      params.request_id = request_id;
+      $.post('/wiki/ajax/newpage/', params).success(function(data) {
+        $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
+      });
+      param = $new_link[0].outerHTML;
+    }
     if (action == 'createLink') {
       var sel = window.getSelection();
       var $input = $('input[name=link-target]');
@@ -132,36 +322,40 @@ $(function() {
     sel.addRange(this._range);
   }
 
-  function show_style_popup(sel) {
-    if (style_popup === null) {
-      style_popup = $('<div class="style-popup short">' +
+  function show_inline_style_toolbar(sel) {
+    if (inline_style_toolbar === null) {
+      inline_style_toolbar = $('<div class="inline-style-popup">' +
                       '<button data-action="italic"><i>i</i></button>' +
                       '<button data-action="bold"><b>b</b></button>' +
-                      '<button data-action="code">#</button>' +
+                      '<button data-action="code">&lt;&gt;</button>' +
                       '<button data-action="removeFormat">×</button>' +
                       '<button data-action="createLink">a</button>' +
                       '<input name="link-target"/>' +
                       '</div>');
-      style_popup.hide();
-      style_popup.insertAfter($('.actions'));
-      style_popup.find('button').on('click', update_style);
-      style_popup.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
+      inline_style_toolbar.hide();
+      inline_style_toolbar.insertAfter(document.body);
+      inline_style_toolbar.find('button').on('click', update_style);
+      inline_style_toolbar.find('[name=link-target]').on('keypress', validate_link).on('focusout', focusout_link);
     }
-    style_popup.css('position', 'absolute');
+    inline_style_toolbar.css('position', 'absolute');
     var pos = sel.getRangeAt(0).getClientRects()[0];
-    style_popup.css('top', pos.top + window.scrollY - 33);
-    style_popup.css('left', pos.left + window.scrollX);
-    style_popup.show();
+    inline_style_toolbar.css('top', pos.top + window.scrollY - 33);
+    inline_style_toolbar.css('left', pos.left + window.scrollX);
+    inline_style_toolbar.show();
   };
-  $(document).on('selectionchange', function(event) {
-    if ($('input[name=link-target].shown').length) {
-      return;
-    }
-    var sel = window.getSelection();
-    if ($(sel.anchorNode).parents('div[contenteditable]').length && sel.toString()) {
-      show_style_popup(sel);
-    } else if (style_popup) {
-      $(style_popup).hide();
-    }
+}(window, document));
+
+$(function() {
+  Phylly.init(),
+  $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
+  $('#save').on('click', function() {
+    var text = $('div[contenteditable]')[0].innerHTML;
+    var csrf = $('[name=csrfmiddlewaretoken]').val();
+    $.post('api-save/',
+      { text: text, csrfmiddlewaretoken: csrf}
+    ).fail(function() {
+      $('#save').css('background', 'red');
+    });
+    return false;
   });
 });