34f7cdc94057ea8dbc4efb8f65c4006621f395dd
   1var rmspan = ["<span class='remove' id='delete-", "'>–</span>"]
   2var tick = "✔";
   3var addText = "+";
   4var rmText = "−";
   5var removebg = "#bf616a";
   6var hovergrn = "#a3be8c";
   7var hoverbg = "#434c5e";
   8var hoverbg2 = "#848ead";
   9var editMode = false;
  10var dragSrcEl = null;
  11var defaultColumns= [
  12      ["General",
  13        ["Github", "https://github.com"],
  14        ["Wikipedia", "https://en.wikipedia.org"],
  15        ["Gmail", "https://mail.google.com"]
  16      ],
  17      ["Productivity",
  18        ["Desmos", "https://www.desmos.com/calculator"],
  19        ["Wolfram", "https://wolframalpha.com"],
  20        ["Hacker News", "https://news.ycombinator.com"]
  21      ],
  22      ["Social",
  23        ["Reddit", "https://www.reddit.com"],
  24        ["YouTube", "https://youtube.com"],
  25        ["Instagram", "https://instagram.com"]
  26      ]
  27]
  28
  29
  30// --------------------------------
  31//
  32// Initialisation
  33//
  34// --------------------------------
  35
  36
  37document.addEventListener("DOMContentLoaded", loadLists);
  38
  39function loadLists() {
  40
  41  // Retrieve lists from storage and trigger callback to generate HTML
  42  
  43  console.log("Getting lists from storage");
  44  chrome.storage.sync.get({"lists": defaultColumns}, parseColumns);
  45
  46  document.getElementById('edit').addEventListener('click', edit, false);
  47  document.getElementById('addcol').addEventListener('click', addColumn, false);
  48
  49  // Set colours from preferences
  50
  51  var bgCallback = function(colourPref) { document.body.style.background = colourPref["bgvalue"]; };
  52  var fgCallback = function(colourPref) { document.body.style.color = colourPref["fgvalue"]; };
  53  var hrCallback = function(colourPref) {
  54    document.documentElement.style.setProperty('--hover-bg', colourPref["hrvalue"]);
  55    hoverbg = colourPref["hrvalue"];
  56  };
  57
  58  chrome.storage.sync.get({"bgvalue": "#2e3440"}, bgCallback);
  59  chrome.storage.sync.get({"fgvalue": "#d8dee9"}, fgCallback);
  60  chrome.storage.sync.get({"hrvalue": "#434c5e"}, hrCallback);
  61
  62}
  63
  64
  65function parseColumns(config) {
  66
  67  var columns = config["lists"]
  68
  69  // Generate elements for each column
  70  for (let col of columns) {
  71
  72    var ul = genColumn(col[0]);
  73    document.getElementById("links").appendChild(ul);
  74
  75    // Iterate through links
  76    for(let item of col.slice(1)) {
  77      li = genItem(ul, item[0], item[1]);
  78      ul.appendChild(li);
  79    }
  80
  81    var sortableProperties = { group: "usercolumns", animation: 150, onSort: function (evt) {saveConfig();} };
  82    new Sortable(ul, sortableProperties);
  83
  84  }
  85}
  86
  87function genColumn(title) {
  88
  89    // Generate HTML elements for a column (without items)
  90
  91    var ul = document.createElement("ul");
  92    ul.setAttribute("id", title);
  93    ul.setAttribute('draggable', 'false');
  94
  95    var grip = document.createElement("span");
  96    grip.setAttribute("class", "grip");
  97    grip.addEventListener('mousedown', enableDrag);
  98    grip.addEventListener('mouseup', disableDrag);
  99    ul.appendChild(grip)
 100
 101    var titleDiv = document.createElement("div");
 102    titleDiv.setAttribute("class", "title");
 103
 104    var titleText = document.createElement("p");
 105    titleText.innerText = title;
 106    titleDiv.appendChild(titleText);
 107
 108    var addBtn = document.createElement("span");
 109    addBtn.innerText = addText;
 110    addBtn.setAttribute("class", "add");
 111    addBtn.setAttribute("id", "add-" + title);
 112    addBtn.addEventListener("click", addItem);
 113    titleDiv.appendChild(addBtn);
 114
 115    ul.appendChild(titleDiv);
 116
 117    return ul;
 118
 119}
 120
 121function genItem(ul, nme, url) {
 122
 123  // Generate HTML elements for an item
 124
 125  var li = document.createElement("li");
 126  li.setAttribute("class", ul.id + "-" + (ul.getElementsByTagName("li").length + 1).toString() );
 127  li.addEventListener("mouseup", saveConfig);
 128
 129  var img = document.createElement("img");
 130  img.className = "icon";
 131  img.src = "https://www.google.com/s2/favicons?domain=" + url;
 132
 133  var link = document.createElement("a");
 134  link.className = "item";
 135  link.href = url;
 136
 137  var rmBtn = document.createElement("span");
 138  rmBtn.className = "remove";
 139  rmBtn.id = "delete-" + li.id;
 140  rmBtn.innerText = "-";
 141  rmBtn.addEventListener("click", removeItem);
 142
 143  link.appendChild(img);
 144  link.insertAdjacentHTML("beforeend", nme);
 145  li.appendChild(link);
 146  li.appendChild(rmBtn);
 147  
 148  return li;
 149
 150}
 151
 152
 153// --------------------------------
 154//
 155// Edit mode
 156//
 157// --------------------------------
 158
 159
 160function edit(event) {
 161  
 162  // Enter/exit edit mode
 163  
 164  if (editMode == true) {
 165    console.log("Exited edit mode");
 166    closeEdit(this);
 167    return false;
 168  }
 169
 170  console.log("Entered edit mode");
 171
 172  this.style.background = hovergrn;
 173  this.innerText = tick;
 174  addBtn= document.getElementById("addcol");
 175  addBtn.style.display = "flex";
 176  var cols = document.getElementsByTagName("ul");
 177
 178  for (let col of cols) {
 179    
 180    var titleDiv = col.getElementsByClassName("title")[0];
 181    titleDiv.getElementsByClassName("add")[0].style.display = "flex";
 182
 183    col.style.bottom = "26px";
 184    col.getElementsByClassName("grip")[0].style.display = "inline-block";
 185
 186    var rmColBtn = document.createElement("span"); 
 187    rmColBtn.className = "rmcol";
 188    rmColBtn.id = "rmcol-" + col.id;
 189    rmColBtn.innerText = rmText;
 190    titleDiv.appendChild(rmColBtn);
 191
 192    var titleStatic = titleDiv.getElementsByTagName("p")[0];
 193    var titleInput = document.createElement("input");
 194    titleInput.type = "text";
 195    titleInput.className = "colname";
 196    titleInput.placeholder = titleStatic.innerText;
 197    titleInput.value = titleStatic.innerText;
 198    titleDiv.insertBefore(titleInput, titleStatic);
 199    titleStatic.remove();
 200
 201  }
 202
 203  updateListeners();
 204  editMode = true;
 205}
 206
 207function closeEdit(editBtn) {
 208
 209  // Exit edit mode and clean up elements
 210
 211  editMode = false;
 212  editBtn.style.background = "";
 213  editBtn.innerText = "e";
 214
 215  var addBtn = document.getElementById("addcol");
 216  addBtn.style.display = "none";
 217
 218  var cols = document.getElementsByTagName("ul");
 219
 220  for (let col of cols) {
 221
 222    col.style.bottom = "0";
 223    col.getElementsByClassName("grip")[0].style.display = "none";
 224
 225    var rmColBtn = col.getElementsByClassName("title")[0].getElementsByClassName("rmcol")[0];
 226    rmColBtn.remove();
 227
 228    var titleDiv = col.getElementsByClassName("title")[0];
 229    titleDiv.getElementsByClassName("add")[0].style.display = "";
 230
 231    titleInput = titleDiv.getElementsByClassName("colname")[0];
 232    titleStatic = document.createElement("p");
 233    titleStatic.innerText = titleInput.value;
 234    if (titleStatic.innerText == "") {
 235      titleStatic.innerText = titleInput.placeholder;
 236    }
 237    titleInput.remove();
 238    titleDiv.appendChild(titleStatic);
 239
 240  }
 241
 242  saveConfig();
 243
 244}
 245
 246function addColumn(event) {
 247
 248  // Create a new column from Edit mode
 249
 250  var ul = document.createElement("ul");
 251
 252  // Make sure columns do not share an id
 253  var ex = document.querySelectorAll('[id^="new "]'); // existing "new" columns
 254  if (ex.length > 0) {
 255    indices = []
 256    for (let i of ex) {
 257     indices.push(Number(i.id.split(" ")[1])); 
 258    }
 259    ul.setAttribute("id", "new " + (Math.max.apply(Math, indices)+1).toString());
 260  }
 261  else {
 262    ul.setAttribute("id", "new 1");
 263  }
 264  ul.setAttribute('draggable', 'false');
 265
 266  var grip = document.createElement("span");
 267  grip.setAttribute("class", "grip");
 268  grip.addEventListener("mousedown", enableDrag);
 269  grip.addEventListener("mouseup", disableDrag);
 270  grip.style.display = "inline-block";
 271  ul.style.bottom = "26px";
 272  ul.appendChild(grip)
 273
 274  var titleDiv = document.createElement("div");
 275  titleDiv.setAttribute("class", "title");
 276
 277  var titleInput = document.createElement("input");
 278  titleInput.type = "text";
 279  titleInput.className = "colname";
 280  titleInput.placeholder = ul.id;
 281  titleDiv.appendChild(titleInput);
 282
 283  var addBtn = document.createElement("span");
 284  addBtn.style.display = "flex";
 285  addBtn.innerText = addText;
 286  addBtn.setAttribute("class", "add");
 287  addBtn.setAttribute("id", "add-" + ul.id);
 288  addBtn.addEventListener("click", addItem);
 289  titleDiv.appendChild(addBtn);
 290
 291  var rmColBtn = document.createElement("span"); 
 292  rmColBtn.className = "rmcol";
 293  rmColBtn.id = "rmcol-" + ul.id;
 294  rmColBtn.innerText = rmText;
 295  titleDiv.appendChild(rmColBtn);
 296
 297  ul.appendChild(titleDiv);
 298
 299  document.getElementById("links").appendChild(ul);
 300  saveConfig();
 301  updateListeners();
 302  titleInput.focus();
 303
 304}
 305
 306
 307function removeColumn(event) {
 308
 309  // Delete column in edit mode
 310  
 311  this.parentNode.parentNode.remove();
 312  saveConfig();
 313
 314}
 315
 316
 317function removeItem(event) {
 318
 319  // Remove link in edit or normal mode
 320  
 321  this.parentNode.outerHTML = "";
 322  delete this.parentNode;
 323  saveConfig();
 324
 325}
 326
 327
 328function addItem(event) {
 329
 330  // Interface for adding a new list item to an existing category
 331
 332  var ul = this.parentNode.parentNode;
 333  var id = ul.id;
 334
 335  // Check if a form has already been generated for this column
 336  existing = ul.getElementsByClassName("new");
 337  if (existing.length > 0) {
 338    existing[0].getElementsByClassName("name")[0].focus();
 339    return false;
 340  }
 341
 342  var li = document.createElement("li");
 343  li.setAttribute("class", "new");
 344  li.addEventListener("keyup", formKeys); 
 345
 346  var saveBtn = document.createElement("span");
 347  saveBtn.className = "save";
 348  saveBtn.tabIndex = "3";
 349  saveBtn.id = "save-" + ul.id;
 350  saveBtn.innerText = tick;
 351  saveBtn.addEventListener("click", saveItem);
 352  saveBtn.addEventListener("mouseover", saveMouseOver);
 353  saveBtn.addEventListener("mouseout", saveMouseOut);
 354  li.appendChild(saveBtn);
 355
 356  var nameInput = document.createElement("input");
 357  nameInput.type = "text";
 358  nameInput.className = "name";
 359  nameInput.placeholder = "name";
 360  nameInput.tabIndex = "1";
 361  li.appendChild(nameInput);
 362
 363  var urlInput = document.createElement("input");
 364  urlInput.type = "url";
 365  urlInput.className = "url";
 366  urlInput.placeholder = "url";
 367  urlInput.tabIndex = "2";
 368  urlInput.spellcheck = "false";
 369  li.appendChild(urlInput);
 370
 371  ul.appendChild(li);
 372  updateListeners();
 373  nameInput.focus();
 374
 375}
 376
 377
 378function saveItem(event) {
 379
 380  // Add new item to a column after pressing the save button in form
 381
 382  var li = this.parentNode;
 383  var ul = this.parentNode.parentNode;
 384  var nameField = li.getElementsByClassName("name")[0];
 385  var urlField = li.getElementsByClassName("url")[0];
 386
 387  if (nameField.value != "" && urlField.value != "" && urlField.validity.typeMismatch== false) {
 388
 389    var newli = genItem(ul, nameField.value, urlField.value);
 390    li.remove();
 391    delete li;
 392
 393    ul.appendChild(newli);
 394    saveConfig();
 395
 396  }
 397  else {
 398
 399    if (nameField.value == "" && urlField.value == "") {
 400      console.log("No data supplied, deleting form");
 401      li.remove();
 402    }
 403    else {
 404      console.log("Missing data, press Esc to delete form");
 405    }
 406
 407  }
 408
 409}
 410
 411
 412// --------------------------------
 413//
 414// UI event listeners
 415//
 416// --------------------------------
 417
 418
 419function updateListeners() {
 420
 421  // Update event listeners for interface elements, since listeners are tied DOM objects which are lost on dataTransfer operations
 422
 423  var addBtns = document.getElementsByClassName("add");
 424  for (let addBtn of addBtns) {
 425    addBtn.addEventListener("click", addItem);
 426  }
 427
 428  var rmBtns = document.getElementsByClassName("rm");
 429  for (let rmBtn of rmBtns) {
 430    rmBtn.addEventListener("click", removeItem);
 431  }
 432
 433  var rmColBtns = document.getElementsByClassName("rmcol");
 434  for (let rmColBtn of rmColBtns) {
 435    rmColBtn.addEventListener("click", removeColumn); 
 436  }
 437
 438  var saveBtns = document.getElementsByClassName("save");
 439  for (let saveBtn of saveBtns) {
 440    saveBtn.addEventListener("click", saveItem);
 441    saveBtn.addEventListener("mouseover", saveMouseOver);
 442    saveBtn.addEventListener("mouseout", saveMouseOut);
 443  }
 444
 445}
 446
 447
 448function saveMouseOver(event) {
 449  nameField = this.parentNode.getElementsByClassName("name")[0];
 450  urlField = this.parentNode.getElementsByClassName("url")[0];
 451  if (nameField.value === ''  || urlField.value === '' || urlField.validity.typeMismatch == true) {
 452    this.style.background = removebg;
 453  }
 454  else {
 455    this.style.background = hovergrn; 
 456  }
 457}
 458
 459
 460function saveMouseOut(event) {
 461  this.style.background = hoverbg2;
 462}
 463
 464
 465function enableDrag() {
 466  // Enable drag & drop when grip is grabbed (otherwise any mouse click triggers dragStart)
 467  console.log("Drag started");
 468  uls = document.getElementsByTagName("ul");
 469  for (let ul of uls) {
 470    ul.setAttribute("draggable", "true");
 471    ul.addEventListener('dragstart', dragStart, false);
 472    ul.addEventListener('dragover', dragOver, false);
 473    ul.addEventListener('drop', drop, false);
 474  }
 475}
 476
 477
 478function disableDrag() {
 479  // Disable drag after grip has been released
 480  console.log("Drag ended");
 481  uls = document.getElementsByTagName("ul");
 482  for (let ul of uls) {
 483    ul.setAttribute("draggable", "false");
 484    ul.removeEventListener('dragstart');
 485    ul.removeEventListener('dragover');
 486    ul.removeEventListener('drop');
 487  }
 488}
 489
 490
 491function dragStart(e) {
 492  dragSrcEl = this;
 493  e.dataTransfer.effectAllowed = 'move';
 494  e.dataTransfer.setData('text/html', this.innerHTML);
 495}
 496
 497
 498function dragOver(e) {
 499  if (e.preventDefault) {
 500    e.preventDefault();
 501  }
 502  e.dataTransfer.dropEffect = 'move';
 503  return false;
 504}
 505
 506
 507function drop(e) {
 508  if (e.stopPropagation); {
 509    e.stopPropagation();
 510  }
 511  if (dragSrcEl != this) {
 512    var srcTitleInput = dragSrcEl.getElementsByClassName("colname")[0].value;
 513    var destTitleInput = this.getElementsByClassName("colname")[0].value;
 514    dragSrcEl.innerHTML = this.innerHTML;
 515    dragSrcEl.getElementsByClassName("colname")[0].value = destTitleInput;
 516    this.innerHTML = e.dataTransfer.getData('text/html');
 517    this.getElementsByClassName("colname")[0].value = srcTitleInput;
 518    saveConfig();
 519    updateListeners();
 520  }
 521  return false;
 522}
 523
 524
 525function formKeys(e) {
 526  var focus = document.activeElement;
 527  if (focus.parentNode.className != "new") {
 528    return false;
 529  }
 530  switch (e.which) {
 531    case 27: // escape
 532      focus.parentNode.remove();
 533      break;
 534    case 13: // enter
 535      focus.parentNode.getElementsByClassName("save")[0].click();
 536      break;
 537  }
 538}
 539
 540
 541// --------------------------------
 542//
 543// Configuration management
 544//
 545// --------------------------------
 546
 547
 548function saveConfig() {
 549  
 550  // Save current DOM structure as JSON data
 551
 552  console.log("Saving settings");
 553  data = []
 554  for (let ul of document.getElementsByTagName("ul")) {
 555    data.push(columnToArray(ul, true));
 556  }
 557  chrome.storage.sync.set( {"lists": data} );
 558
 559}
 560
 561
 562function columnToArray(ul, title = false) {
 563
 564  // Convert a column of data (ul) to a 2D array, optionally including the title
 565
 566  var data = [];
 567  var items = ul.getElementsByClassName("item");
 568
 569  if (title == true && editMode == true) {
 570    data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].value;
 571    if (data[0] == "" || data[0] == null) {
 572      console.log("Using default category name since input was empty");
 573      data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].placeholder;
 574    }
 575  }
 576  else if (title == true) {
 577    data[0] = ul.getElementsByClassName('title')[0].getElementsByTagName("p")[0].textContent;
 578  }
 579
 580  for (let li of items) {
 581    if (li.class == "new" || li.class == "title") {
 582      continue; // Ignore input forms and titles (already handled)
 583    }
 584    else {
 585      data.push([li.innerText, li.getAttribute("href")]);
 586    }
 587  }
 588
 589  return data;
 590
 591}