refactor admin.js into more modular functions
authorAndrew Lorimer <andrew@lorimer.id.au>
Sun, 18 Aug 2019 10:43:34 +0000 (20:43 +1000)
committerAndrew Lorimer <andrew@lorimer.id.au>
Sun, 18 Aug 2019 10:43:34 +0000 (20:43 +1000)
admin.js
index ea20d86d1642c1ac25ff925659a41b8621922f29..1a2b0db2d8403e3ad472a1e034b1ce59eaa9ec4f 100644 (file)
--- a/admin.js
+++ b/admin.js
@@ -1,13 +1,14 @@
-var mainlist;
 var rmspan = ["<span class='remove' id='delete-", "'>–</span>"]
 var tick = "✔";
 var rmspan = ["<span class='remove' id='delete-", "'>–</span>"]
 var tick = "✔";
+var addText = "+";
+var rmText = "−";
 var removebg = "#bf616a";
 var hovergrn = "#a3be8c";
 var hoverbg = "#434c5e";
 var hoverbg2 = "#848ead";
 var removebg = "#bf616a";
 var hovergrn = "#a3be8c";
 var hoverbg = "#434c5e";
 var hoverbg2 = "#848ead";
-document.addEventListener("DOMContentLoaded", function() {
-  chrome.storage.sync.get(
-    {"lists": [
+var editMode = false;
+var dragSrcEl = null;
+var defaultColumns= [
       ["General",
         ["Github", "https://github.com"],
         ["Wikipedia", "https://en.wikipedia.org"],
       ["General",
         ["Github", "https://github.com"],
         ["Wikipedia", "https://en.wikipedia.org"],
@@ -23,387 +24,556 @@ document.addEventListener("DOMContentLoaded", function() {
         ["YouTube", "https://youtube.com"],
         ["Instagram", "https://instagram.com"]
       ]
         ["YouTube", "https://youtube.com"],
         ["Instagram", "https://instagram.com"]
       ]
-    ]
-},
-    userListsCallback);
-});
-
-function columnToArray(list) {
-  var l = [];
-  var elem = list.getElementsByClassName("item");
-  for (var i = 0; i < elem.length; ++i) {
-    l[i] = [elem[i].innerText, elem[i].getAttribute("href")];
-  }
-  return l;
+]
+
+
+// --------------------------------
+//
+// Initialisation
+//
+// --------------------------------
+
+
+document.addEventListener("DOMContentLoaded", loadLists);
+
+function loadLists() {
+
+  // Retrieve lists from storage and trigger callback to generate HTML
+  
+  console.log("Getting lists from storage");
+  chrome.storage.sync.get({"lists": defaultColumns}, parseColumns);
+
+  document.getElementById('edit').addEventListener('click', edit, false);
+  document.getElementById('addcol').addEventListener('click', addColumn, false);
+
 }
 
 }
 
-function listToArray(list) {
-  var l = [];
-      console.log("looking at list:");
-      console.log(list);
-  if (editMode == true) {
-    l[0] = list.getElementsByClassName('title')[0].getElementsByTagName("input")[0].value;
-  }
-  else {
-    l[0] = list.getElementsByClassName('title')[0].getElementsByTagName("p")[0].textContent;
-  }
-  var elem = list.getElementsByTagName("li");
-  for (var i = 0; i < elem.length; ++i) {
-    if (elem[i].class == "new") {
-      continue;
-    }
-    else {
-      l[i+1] = [elem[i].getElementsByTagName("a")[0].innerText, elem[i].getElementsByTagName("a")[0].getAttribute("href")];
+
+function parseColumns(config) {
+
+  var columns = config["lists"]
+
+  // Generate elements for each column
+  for (let col of columns) {
+
+    var ul = genColumn(col[0]);
+    document.getElementById("links").appendChild(ul);
+
+    // Iterate through links
+    for(let item of col.slice(1)) {
+      li = genItem(ul, item[0], item[1]);
+      ul.appendChild(li);
     }
     }
+
+    var sortableProperties = { group: "usercolumns", animation: 150, onSort: function (evt) {saveConfig();} };
+    new Sortable(ul, sortableProperties);
+
   }
   }
-  return l;
 }
 
 }
 
-var userListsCallback = function(lists) {
-  mainlist = []
-  for (let x of lists['lists']) {
-    mainlist.push(x[0]);
-  }
-  for(var i=0;i<lists["lists"].length;i++) {
-    document.getElementById('edit').addEventListener('click', edit, false);
-    document.getElementById('addcol').addEventListener('click', addCol, false);
+function genColumn(title) {
+
+    // Generate HTML elements for a column (without items)
+
     var ul = document.createElement("ul");
     var ul = document.createElement("ul");
-    ul.setAttribute("id", mainlist[i]);
-    ul.setAttribute('draggable', 'false'); // Draggable attribute must be set on mousedown event, otherwise any cursor movement causes dragstart()
-    document.getElementById("links").appendChild(ul);
+    ul.setAttribute("id", title);
+    ul.setAttribute('draggable', 'false');
 
     var grip = document.createElement("span");
     grip.setAttribute("class", "grip");
 
     var grip = document.createElement("span");
     grip.setAttribute("class", "grip");
-    grip.addEventListener('mousedown', function() {
-      console.log("Drag started");
-      uls = document.getElementsByTagName("ul");
-      for (let ul of uls) {
-        ul.setAttribute("draggable", "true");
-        ul.addEventListener('dragstart', dragStart, false);
-        ul.addEventListener('dragenter', dragEnter, false);
-        ul.addEventListener('dragover', dragOver, false);
-        ul.addEventListener('dragleave', dragLeave, false);
-        ul.addEventListener('drop', drop, false);
-      }
-    });
-
-    grip.addEventListener('mouseup', function() {
-      console.log("Drag ended");
-      uls = document.getElementsByTagName("ul");
-      for (let ul of uls) {
-        ul.setAttribute("draggable", "false");
-        ul.removeEventListener('dragstart');
-        ul.removeEventListener('dragenter');
-        ul.removeEventListener('dragover');
-        ul.removeEventListener('dragleave');
-        ul.removeEventListener('drop');
-      }
-    });
-
-    var title = document.createElement("div");
-    title.setAttribute("class", "title");
+    grip.addEventListener('mousedown', enableDrag);
+    grip.addEventListener('mouseup', disableDrag);
     ul.appendChild(grip)
     ul.appendChild(grip)
-    ul.appendChild(title);
-
-    var p = document.createElement("p");
-    p.innerText = mainlist[i];
-    title.appendChild(p);
-
-    title.insertAdjacentHTML("beforeend", "<span class='add' id='add-"+mainlist[i]+"'>+</span>");
-
-    var list = lists["lists"][i];
-    for(var j=1;j<lists["lists"][i].length;j++) {
-      var li = document.createElement("li");
-      li.setAttribute("id", mainlist[i]+"-"+j);
-      var siteurl = list[j][1];
-      var nme = list[j][0];
-      var img = document.createElement("img");
-      img.className = "icon";
-      img.src = "https://www.google.com/s2/favicons?domain="+siteurl+"";
-      li.insertAdjacentHTML("beforeend", "<a class='item' href="+siteurl+"><img src="+img.src+" alt="+extractDomain(siteurl,1)+"/> "+nme+"</a>");
-      li.insertAdjacentHTML("beforeend", rmspan[0] + j + "-" + mainlist[i] + rmspan[1]);
-      ul.appendChild(li);
-    }
 
 
-    new Sortable(ul, {
-      group: "userlists",
-      animation: 150,
-      onUpdate: function (evt) {
-        save();
-      }
-    });
-  }
+    var titleDiv = document.createElement("div");
+    titleDiv.setAttribute("class", "title");
 
 
-  menu();
+    var titleText = document.createElement("p");
+    titleText.innerText = title;
+    titleDiv.appendChild(titleText);
 
 
-};
+    var addBtn = document.createElement("span");
+    addBtn.innerText = addText;
+    addBtn.setAttribute("class", "add");
+    addBtn.setAttribute("id", "add-" + title);
+    addBtn.addEventListener("click", addItem);
+    titleDiv.appendChild(addBtn);
 
 
-function dragStart(e) {
-  dragSrcEl = this;
-  e.dataTransfer.effectAllowed = 'move';
-  e.dataTransfer.setData('text/html', this.innerHTML);
-}
+    ul.appendChild(titleDiv);
 
 
-function dragOver(e) {
-  if (e.preventDefault) {
-    e.preventDefault();
-  }
-  e.dataTransfer.dropEffect = 'move';
-  return false;
-}
+    return ul;
 
 
-function dragEnter(e) {
-  this.classList.add('over');
 }
 
 }
 
-function dragLeave(e) {
-  this.classList.remove('over');
-}
+function genItem(ul, nme, url) {
 
 
-function drop(e) {
-  if (e.stopPropagation); {
-    e.stopPropagation();
-  }
-  if (dragSrcEl != this) {
-    dragSrcEl.innerHTML = this.innerHTML;
-    this.innerHTML = e.dataTransfer.getData('text/html');
-    save();
-    menu();
-  }
-  return false;
-}
+  // Generate HTML elements for an item
 
 
-var dragSrcEl = null;
+  var li = document.createElement("li");
+  li.setAttribute("class", ul.id + "-" + (ul.getElementsByTagName("li").length + 1).toString() );
+  li.addEventListener("mouseup", saveConfig);
+
+  var img = document.createElement("img");
+  img.className = "icon";
+  img.src = "https://www.google.com/s2/favicons?domain=" + url;
+  img.alt = extractDomain(url, 1);
+
+  var link = document.createElement("a");
+  link.className = "item";
+  link.href = url;
+
+  var rmBtn = document.createElement("span");
+  rmBtn.className = "remove";
+  rmBtn.id = "delete-" + li.id;
+  rmBtn.innerText = "-";
+  rmBtn.addEventListener("click", removeItem);
+
+  link.appendChild(img);
+  link.insertAdjacentHTML("beforeend", nme);
+  li.appendChild(link);
+  li.appendChild(rmBtn);
+  
+  return li;
 
 
-document.onkeyup=function(e){
-  var e = e || window.event; // for IE to cover IEs window event-object
-  fields = document.getElementsByClassName("new");
-  if(e.which == 27 && fields != null) {
-    fields[fields.length-1].remove();
-    return false;
-  }
-  if(e.which == 13 && fields.length > 0) {
-    fields[fields.length-1].getElementsByClassName("save")[0].click();
-    return false;
-  }
 }
 
 }
 
-var editMode = false;
 
 
-function edit() {
+// --------------------------------
+//
+// Edit mode
+//
+// --------------------------------
+
+
+function edit(event) {
+  
+  // Enter/exit edit mode
+  
   if (editMode == true) {
     console.log("Exited edit mode");
     closeEdit(this);
   if (editMode == true) {
     console.log("Exited edit mode");
     closeEdit(this);
-    return 0;
+    return false;
   }
   }
+
   console.log("Entered edit mode");
   console.log("Entered edit mode");
+
   this.style.background = hovergrn;
   this.innerText = tick;
   this.style.background = hovergrn;
   this.innerText = tick;
-  addbtn = document.getElementById("addcol");
-  addbtn.style.display = "flex";
+  addBtn= document.getElementById("addcol");
+  addBtn.style.display = "flex";
   var cols = document.getElementsByTagName("ul");
   var cols = document.getElementsByTagName("ul");
+
   for (let col of cols) {
   for (let col of cols) {
+    
+    var titleDiv = col.getElementsByClassName("title")[0];
+    titleDiv.getElementsByClassName("add")[0].style.display = "flex";
+
     col.style.bottom = "26px";
     col.style.bottom = "26px";
-    title = col.getElementsByClassName("title")[0];
-    rmbutton = document.createElement("span"); 
-    rmbutton.setAttribute("class", "rmcol");
-    rmbutton.setAttribute("id", "rmcol-"+col.id);
-    rmbutton.innerText = "-";
-    rmbutton.addEventListener('click', function(event){
-      console.log("Deleting column " + this.parentNode.parentNode);
-      this.parentNode.parentNode.remove();
-      var index = mainlist.indexOf(this.parentNode.parentNode.id);
-      console.log("Found deleted node " + this.parentNode.parentNode.id + " at index " + index);
-      if (index > -1) {
-        mainlist.splice(index, 1);
-      }
-      save();
-    });
-    title.appendChild(rmbutton);
-    title.getElementsByClassName("add")[0].style.display = "flex";
-    titlep = title.getElementsByTagName("p")[0];
-    editTitle = document.createElement("input");
-    titlep.insertAdjacentHTML("afterend", "<input type='text' class='colname' placeholder='category' value='" + titlep.innerText + "'>");
-    titlep.remove();
     col.getElementsByClassName("grip")[0].style.display = "inline-block";
     col.getElementsByClassName("grip")[0].style.display = "inline-block";
+
+    var rmColBtn = document.createElement("span"); 
+    rmColBtn.className = "rmcol";
+    rmColBtn.id = "rmcol-" + col.id;
+    rmColBtn.innerText = rmText;
+    titleDiv.appendChild(rmColBtn);
+
+    var titleStatic = titleDiv.getElementsByTagName("p")[0];
+    var titleInput = document.createElement("input");
+    titleInput.type = "text";
+    titleInput.className = "colname";
+    titleInput.placeholder = titleStatic.innerText;
+    titleInput.value = titleStatic.innerText;
+    titleDiv.insertBefore(titleInput, titleStatic);
+    titleStatic.remove();
+
   }
   }
+
+  updateListeners();
   editMode = true;
 }
 
   editMode = true;
 }
 
-function closeEdit(editbtn) {
+function closeEdit(editBtn) {
+
+  // Exit edit mode and clean up elements
+
   editMode = false;
   editMode = false;
-  editbtn.style.background = "";
-  editbtn.innerText = "e";
-  addbtn = document.getElementById("addcol");
-  addbtn.style.display = "none";
+  editBtn.style.background = "";
+  editBtn.innerText = "e";
+
+  var addBtn = document.getElementById("addcol");
+  addBtn.style.display = "none";
+
   var cols = document.getElementsByTagName("ul");
   var cols = document.getElementsByTagName("ul");
+
   for (let col of cols) {
   for (let col of cols) {
+
     col.style.bottom = "0";
     col.style.bottom = "0";
-    rmbutton = col.getElementsByClassName("title")[0].getElementsByClassName("rmcol")[0];
-    rmbutton.remove();
-    title = col.getElementsByClassName("title")[0];
-    title.getElementsByClassName("add")[0].style.display = "";
-    editTitle = title.getElementsByClassName("colname")[0];
-    titlep = document.createElement("p");
-    titlep.innerText = editTitle.value;
-    editTitle.remove();
-    title.appendChild(titlep);
     col.getElementsByClassName("grip")[0].style.display = "none";
     col.getElementsByClassName("grip")[0].style.display = "none";
+
+    var rmColBtn = col.getElementsByClassName("title")[0].getElementsByClassName("rmcol")[0];
+    rmColBtn.remove();
+
+    var titleDiv = col.getElementsByClassName("title")[0];
+    titleDiv.getElementsByClassName("add")[0].style.display = "";
+
+    titleInput = titleDiv.getElementsByClassName("colname")[0];
+    titleStatic = document.createElement("p");
+    titleStatic.innerText = titleInput.value;
+    if (titleStatic.innerText == "") {
+      titleStatic.innerText = titleInput.placeholder;
+    }
+    titleInput.remove();
+    titleDiv.appendChild(titleStatic);
+
   }
   }
-  save();
+
+  saveConfig();
+
 }
 
 }
 
-function addCol() {
+function addColumn(event) {
+
+  // Create a new column from Edit mode
+
   var ul = document.createElement("ul");
   var ul = document.createElement("ul");
-  ul.setAttribute("id", "new");
-  ul.setAttribute('draggable', 'true');
-  ul.addEventListener('dragstart', dragStart, false);
-  ul.addEventListener('dragenter', dragEnter, false);
-  ul.addEventListener('dragover', dragOver, false);
-  ul.addEventListener('dragleave', dragLeave, false);
-  ul.addEventListener('drop', drop, false);
-  document.getElementById("links").appendChild(ul);
+
+  // Make sure columns do not share an id
+  var ex = document.querySelectorAll('[id^="new "]'); // existing "new" columns
+  if (ex.length > 0) {
+    indices = []
+    for (let i of ex) {
+     indices.push(Number(i.id.split(" ")[1])); 
+    }
+    ul.setAttribute("id", "new " + (Math.max.apply(Math, indices)+1).toString());
+  }
+  else {
+    ul.setAttribute("id", "new 1");
+  }
+  ul.setAttribute('draggable', 'false');
 
   var grip = document.createElement("span");
   grip.setAttribute("class", "grip");
 
   var grip = document.createElement("span");
   grip.setAttribute("class", "grip");
+  grip.addEventListener("mousedown", enableDrag);
+  grip.addEventListener("mouseup", disableDrag);
+  grip.style.display = "inline-block";
+  ul.style.bottom = "26px";
+  ul.appendChild(grip)
+
+  var titleDiv = document.createElement("div");
+  titleDiv.setAttribute("class", "title");
+
+  var titleInput = document.createElement("input");
+  titleInput.type = "text";
+  titleInput.className = "colname";
+  titleInput.placeholder = ul.id;
+  titleDiv.appendChild(titleInput);
+
+  var addBtn = document.createElement("span");
+  addBtn.style.display = "flex";
+  addBtn.innerText = addText;
+  addBtn.setAttribute("class", "add");
+  addBtn.setAttribute("id", "add-" + ul.id);
+  addBtn.addEventListener("click", addItem);
+  titleDiv.appendChild(addBtn);
+
+  var rmColBtn = document.createElement("span"); 
+  rmColBtn.className = "rmcol";
+  rmColBtn.id = "rmcol-" + ul.id;
+  rmColBtn.innerText = rmText;
+  titleDiv.appendChild(rmColBtn);
+
+  ul.appendChild(titleDiv);
 
 
-  var title = document.createElement("div");
-  title.setAttribute("class", "title");
-  title.appendChild(grip)
-  ul.appendChild(title);
-
-  var p = document.createElement("p");
-  p.innerText = "new";
-  title.appendChild(p);
-
-  title.insertAdjacentHTML("beforeend", "<span class='add' id='add-new'>+</span>");
-
-  rmbutton = document.createElement("span"); 
-  rmbutton.setAttribute("class", "rmcol");
-  rmbutton.setAttribute("id", "rmcol-new");
-  rmbutton.innerText = "-";
-  rmbutton.addEventListener('click', function(event){
-    console.log("Deleting column " + this.parentNode.parentNode);
-    this.parentNode.parentNode.remove();
-    var index = mainlist.indexOf(this.parentNode.parentNode.id);
-    console.log("Found deleted node " + this.parentNode.parentNode.id + " at index " + index);
-    if (index > -1) {
-      mainlist.splice(index, 1);
-    }
-    save();
-  });
-  title.appendChild(rmbutton);
-  title.getElementsByClassName("add")[0].style.display = "flex";
-  titlep = title.getElementsByTagName("p")[0];
-  editTitle = document.createElement("input");
-  titlep.insertAdjacentHTML("afterend", "<input type='text' class='colname' placeholder='category'>");
-  titlep.remove();
+  document.getElementById("links").appendChild(ul);
+  saveConfig();
+  updateListeners();
+  titleInput.focus();
+
+}
+
+
+function removeColumn(event) {
+
+  // Delete column in edit mode
   
   
-  mainlist.push(["new"]);
-  save();
-  listen(title.getElementsByClassName("add")[0]);
+  this.parentNode.parentNode.remove();
+  saveConfig();
+
 }
 
 }
 
-function listen(li) {
-  li.addEventListener('click', function(event){
-    var r = event.target.id.split("-");
-    if (r[0] == "delete") {
-      var el = document.getElementById(r[2]+"-"+r[1]);
-      el.outerHTML = "";
-      delete el;
-      save();
-    } else {
-      addItem(r, li);
+
+function removeItem(event) {
+
+  // Remove link in edit or normal mode
+  
+  this.parentNode.outerHTML = "";
+  delete this.parentNode;
+  saveConfig();
+
+}
+
+
+function addItem(event) {
+
+  // Interface for adding a new list item to an existing category
+
+  var ul = this.parentNode.parentNode;
+  var id = ul.id;
+
+  // Check if a form has already been generated for this column
+  existing = ul.getElementsByClassName("new");
+  if (existing.length > 0) {
+    existing[0].getElementsByClassName("name")[0].focus();
+    return false;
+  }
+
+  var li = document.createElement("li");
+  li.setAttribute("class", "new");
+  li.addEventListener("keyup", formKeys); 
+
+  var saveBtn = document.createElement("span");
+  saveBtn.className = "save";
+  saveBtn.tabIndex = "3";
+  saveBtn.id = "save-" + ul.id;
+  saveBtn.innerText = tick;
+  saveBtn.addEventListener("click", saveItem);
+  saveBtn.addEventListener("mouseover", saveMouseOver);
+  saveBtn.addEventListener("mouseout", saveMouseOut);
+  li.appendChild(saveBtn);
+
+  var nameInput = document.createElement("input");
+  nameInput.type = "text";
+  nameInput.className = "name";
+  nameInput.placeholder = "name";
+  nameInput.tabIndex = "1";
+  li.appendChild(nameInput);
+
+  var urlInput = document.createElement("input");
+  urlInput.type = "url";
+  urlInput.className = "url";
+  urlInput.placeholder = "url";
+  urlInput.tabIndex = "2";
+  urlInput.spellcheck = "false";
+  li.appendChild(urlInput);
+
+  ul.appendChild(li);
+  updateListeners();
+  nameInput.focus();
+
+}
+
+
+function saveItem(event) {
+
+  // Add new item to a column after pressing the save button in form
+
+  var li = this.parentNode;
+  var ul = this.parentNode.parentNode;
+  var nameField = li.getElementsByClassName("name")[0];
+  var urlField = li.getElementsByClassName("url")[0];
+
+  if (nameField.value != "" && urlField.value != "" && urlField.validity.typeMismatch== false) {
+
+    var newli = genItem(ul, nameField.value, urlField.value);
+    li.remove();
+    delete li;
+
+    ul.appendChild(newli);
+    saveConfig();
+
+  }
+  else {
+
+    if (nameField.value == "" && urlField.value == "") {
+      console.log("No data supplied, deleting form");
+      li.remove();
+    }
+    else {
+      console.log("Missing data, press Esc to delete form");
     }
     }
-  }, true);
+
+  }
+
 }
 
 }
 
-function addItem(caller, li) {
-      if (ul == null) {
-        var ul = li.parentNode.parentNode;
-        var id = ul.id;
-      }
-      else {
-        var ul = document.getElementById(caller[1]);
-        var id = caller[1];
-      }
-      
-      if (document.querySelectorAll("#"+id+" .new").length > 0) {
-        fields = document.querySelector("#"+id+" .new .name");
-        fields.focus(); 
-        return false;
-      }
-      var li = document.createElement("li");
-      li.setAttribute("class", "new");
-      li.insertAdjacentHTML("beforeend", "<span class='save' tabindex='3' id='input-"+columnToArray(li).length.toString()+"'>"+tick+"</span><input type='text' class='name' value='' placeholder='name' tabindex='1'><br /><input type='url' spellcheck=false class='url' value='' placeholder='url' tabindex='2' id='form-"+columnToArray(li).length.toString()+"'>");
-      ul.appendChild(li);
-      var span = document.getElementById("input-"+columnToArray(li).length.toString());
-      var form = document.getElementById("form-"+columnToArray(li).length.toString());
-      span.addEventListener('click', function(event){
-        console.log("Adding item to:");
-        console.log(this);
-        var li = document.getElementsByClassName("new")[0]
-        var ul = li.parentNode;
-        if (li.getElementsByClassName("name")[0].value != "" && li.getElementsByClassName("url")[0].value != "" && li.getElementsByClassName("url")[0].validity.typeMismatch== false) {
-          var newli = document.createElement("li");
-          newli.setAttribute("id",caller[1]+"-"+columnToArray(ul).length.toString());
-          var siteurl = addhttp(li.getElementsByClassName("url")[0].value);
-          var nme = li.getElementsByClassName("name")[0].value;
-          li.remove()
-          delete li;
-
-          newli.insertAdjacentHTML("beforeend", "<a href="+siteurl+">"+nme+"</a>");
-          newli.insertAdjacentHTML("beforeend", rmspan[0]+columnToArray(ul).length.toString()+"-"+caller[1]+rmspan[1]);
-          document.getElementById(id).appendChild(newli);
-          save();
-          listen(newli);
-        }
-        else {
-          if (li.getElementsByClassName("name")[0].value == "" && li.getElementsByClassName("url")[0].value == "") {
-            console.log("No data supplied, deleting form");
-            this.parentNode.remove();
-          }
-          else {
-            console.log("Missing data, press Esc to delete form");
-          }
-        }
-      });
-      span.onmouseover = function() {
-        nme = document.getElementsByClassName("name")[0];
-        url = document.getElementsByClassName("url")[0];
-        if (nme.value === ''  || url.value === '' || url.validity.typeMismatch == true) {
-          this.style.background = removebg;
-        }
-        else {
-          this.style.background = hovergrn; 
-        }
-      };
-      span.onmouseout = function() {
-        this.style.background = hoverbg2;
-      };
-      menu(); // TODO fix bug where form save button loses its eventlistener after generating form for another column twice
-      fields = document.querySelector("#"+id+" .new .name");
-      fields.focus(); 
 
 
+// --------------------------------
+//
+// UI event listeners
+//
+// --------------------------------
+
+
+function updateListeners() {
+
+  // Update event listeners for interface elements, since listeners are tied DOM objects which are lost on dataTransfer operations
+
+  var addBtns = document.getElementsByClassName("add");
+  for (let addBtn of addBtns) {
+    addBtn.addEventListener("click", addItem);
+  }
+
+  var rmBtns = document.getElementsByClassName("rm");
+  for (let rmBtn of rmBtns) {
+    rmBtn.addEventListener("click", removeItem);
+  }
+
+  var rmColBtns = document.getElementsByClassName("rmcol");
+  for (let rmColBtn of rmColBtns) {
+    rmColBtn.addEventListener("click", removeColumn); 
+  }
+
+  var saveBtns = document.getElementsByClassName("save");
+  for (let saveBtn of saveBtns) {
+    saveBtn.addEventListener("click", saveItem);
+    saveBtn.addEventListener("mouseover", saveMouseOver);
+    saveBtn.addEventListener("mouseout", saveMouseOut);
+  }
+
+}
+
+
+function saveMouseOver(event) {
+  nameField = this.parentNode.getElementsByClassName("name")[0];
+  urlField = this.parentNode.getElementsByClassName("url")[0];
+  if (nameField.value === ''  || urlField.value === '' || urlField.validity.typeMismatch == true) {
+    this.style.background = removebg;
+  }
+  else {
+    this.style.background = hovergrn; 
+  }
+}
+
+
+function saveMouseOut(event) {
+  this.style.background = hoverbg2;
 }
 
 }
 
-function menu() {
-  // Update event listeners for all items, their remove buttons, and the add button for each column
-  var allUserLi = document.querySelectorAll('.remove, .add');
 
 
-  allUserLi.forEach(function(li, p_index){
-    listen(li);
-  });
+function enableDrag() {
+  // Enable drag & drop when grip is grabbed (otherwise any mouse click triggers dragStart)
+  console.log("Drag started");
+  uls = document.getElementsByTagName("ul");
+  for (let ul of uls) {
+    ul.setAttribute("draggable", "true");
+    ul.addEventListener('dragstart', dragStart, false);
+    ul.addEventListener('dragover', dragOver, false);
+    ul.addEventListener('drop', drop, false);
+  }
 }
 
 }
 
-function save(l) {
-  console.log(mainlist);
-  var set = l || JSON.parse(JSON.stringify(mainlist));
-  d = []
-  d = set;
+
+function disableDrag() {
+  // Disable drag after grip has been released
+  console.log("Drag ended");
+  uls = document.getElementsByTagName("ul");
+  for (let ul of uls) {
+    ul.setAttribute("draggable", "false");
+    ul.removeEventListener('dragstart');
+    ul.removeEventListener('dragover');
+    ul.removeEventListener('drop');
+  }
+}
+
+
+function dragStart(e) {
+  dragSrcEl = this;
+  e.dataTransfer.effectAllowed = 'move';
+  e.dataTransfer.setData('text/html', this.innerHTML);
+}
+
+
+function dragOver(e) {
+  if (e.preventDefault) {
+    e.preventDefault();
+  }
+  e.dataTransfer.dropEffect = 'move';
+  return false;
+}
+
+
+function drop(e) {
+  if (e.stopPropagation); {
+    e.stopPropagation();
+  }
+  if (dragSrcEl != this) {
+    var srcTitleInput = dragSrcEl.getElementsByClassName("colname")[0].value;
+    var destTitleInput = this.getElementsByClassName("colname")[0].value;
+    dragSrcEl.innerHTML = this.innerHTML;
+    dragSrcEl.getElementsByClassName("colname")[0].value = destTitleInput;
+    this.innerHTML = e.dataTransfer.getData('text/html');
+    this.getElementsByClassName("colname")[0].value = srcTitleInput;
+    saveConfig();
+    updateListeners();
+  }
+  return false;
+}
+
+
+function formKeys(e) {
+  var focus = document.activeElement;
+  if (focus.parentNode.className != "new") {
+    return false;
+  }
+  switch (e.which) {
+    case 27: // escape
+      focus.parentNode.remove();
+      break;
+    case 13: // enter
+      focus.parentNode.getElementsByClassName("save")[0].click();
+      break;
+  }
+}
+
+
+// --------------------------------
+//
+// Configuration management
+//
+// --------------------------------
+
+
+function saveConfig() {
+  
+  // Save current DOM structure as JSON data
+
   console.log("Saving settings");
   console.log("Saving settings");
-  for (var i = 0; i < set.length; ++i) {
-    d[i] = listToArray(document.getElementById(set[i]));
+  data = []
+  for (let ul of document.getElementsByTagName("ul")) {
+    data.push(columnToArray(ul, true));
   }
   }
-  chrome.storage.sync.set( {"lists": d} );
+  chrome.storage.sync.set( {"lists": data} );
+
+}
+
+
+function columnToArray(ul, title = false) {
+
+  // Convert a column of data (ul) to a 2D array, optionally including the title
+
+  var data = [];
+  var items = ul.getElementsByClassName("item");
+
+  if (title == true && editMode == true) {
+    data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].value;
+    if (data[0] == "" || data[0] == null) {
+      console.log("Using default category name since input was empty");
+      data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].placeholder;
+    }
+  }
+  else if (title == true) {
+    data[0] = ul.getElementsByClassName('title')[0].getElementsByTagName("p")[0].textContent;
+  }
+
+  for (let li of items) {
+    if (li.class == "new" || li.class == "title") {
+      continue; // Ignore input forms and titles (already handled)
+    }
+    else {
+      data.push([li.innerText, li.getAttribute("href")]);
+    }
+  }
+
+  return data;
+
 }
 }