+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 editMode = false;
+var dragSrcEl = null;
+var defaultColumns= [
+ ["General",
+ ["Github", "https://github.com"],
+ ["Wikipedia", "https://en.wikipedia.org"],
+ ["Gmail", "https://mail.google.com"]
+ ],
+ ["Productivity",
+ ["Desmos", "https://www.desmos.com/calculator"],
+ ["Wolfram", "https://wolframalpha.com"],
+ ["Hacker News", "https://news.ycombinator.com"]
+ ],
+ ["Social",
+ ["Reddit", "https://www.reddit.com"],
+ ["YouTube", "https://youtube.com"],
+ ["Instagram", "https://instagram.com"]
+ ]
+]
+
+
+// --------------------------------
+//
+// 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);
+
+ // Set colours from preferences
+
+ var bgCallback = function(colourPref) { document.body.style.background = colourPref["bgvalue"]; };
+ var fgCallback = function(colourPref) { document.body.style.color = colourPref["fgvalue"]; };
+ var hrCallback = function(colourPref) {
+ document.documentElement.style.setProperty('--hover-bg', colourPref["hrvalue"]);
+ hoverbg = colourPref["hrvalue"];
+ };
+
+ chrome.storage.sync.get({"bgvalue": "#2e3440"}, bgCallback);
+ chrome.storage.sync.get({"fgvalue": "#d8dee9"}, fgCallback);
+ chrome.storage.sync.get({"hrvalue": "#434c5e"}, hrCallback);
+
+}
+
+
+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);
+
+ }
+}
+
+function genColumn(title) {
+
+ // Generate HTML elements for a column (without items)
+
+ var ul = document.createElement("ul");
+ ul.setAttribute("id", title);
+ ul.setAttribute('draggable', 'false');
+
+ var grip = document.createElement("span");
+ grip.setAttribute("class", "grip");
+ grip.addEventListener('mousedown', enableDrag);
+ grip.addEventListener('mouseup', disableDrag);
+ ul.appendChild(grip)
+
+ var titleDiv = document.createElement("div");
+ titleDiv.setAttribute("class", "title");
+
+ 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);
+
+ ul.appendChild(titleDiv);
+
+ return ul;
+
+}
+
+function genItem(ul, nme, url) {
+
+ // Generate HTML elements for an item
+
+ 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;
+
+}
+
+
+// --------------------------------
+//
+// Edit mode
+//
+// --------------------------------
+
+
+function edit(event) {
+
+ // Enter/exit edit mode
+
+ if (editMode == true) {
+ console.log("Exited edit mode");
+ closeEdit(this);
+ return false;
+ }
+
+ console.log("Entered edit mode");
+
+ this.style.background = hovergrn;
+ this.innerText = tick;
+ addBtn= document.getElementById("addcol");
+ addBtn.style.display = "flex";
+ var cols = document.getElementsByTagName("ul");
+
+ for (let col of cols) {
+
+ var titleDiv = col.getElementsByClassName("title")[0];
+ titleDiv.getElementsByClassName("add")[0].style.display = "flex";
+
+ col.style.bottom = "26px";
+ 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;
+}
+
+function closeEdit(editBtn) {
+
+ // Exit edit mode and clean up elements
+
+ editMode = false;
+ editBtn.style.background = "";
+ editBtn.innerText = "e";
+
+ var addBtn = document.getElementById("addcol");
+ addBtn.style.display = "none";
+
+ var cols = document.getElementsByTagName("ul");
+
+ for (let col of cols) {
+
+ col.style.bottom = "0";
+ 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);
+
+ }
+
+ saveConfig();
+
+}
+
+function addColumn(event) {
+
+ // Create a new column from Edit mode
+
+ var ul = document.createElement("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");
+ 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);
+
+ document.getElementById("links").appendChild(ul);
+ saveConfig();
+ updateListeners();
+ titleInput.focus();
+
+}
+
+
+function removeColumn(event) {
+
+ // Delete column in edit mode
+
+ this.parentNode.parentNode.remove();
+ saveConfig();
+
+}
+
+
+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");
+ }
+
+ }
+
+}
+
+
+// --------------------------------
+//
+// 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 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 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");
+ data = []
+ for (let ul of document.getElementsByTagName("ul")) {
+ data.push(columnToArray(ul, true));
+ }
+ 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;
+
+}