01b293c0d282dd4c8d36ee30acf1d3688afa4fe0
   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  img.alt = extractDomain(url, 1);
 133
 134  var link = document.createElement("a");
 135  link.className = "item";
 136  link.href = url;
 137
 138  var rmBtn = document.createElement("span");
 139  rmBtn.className = "remove";
 140  rmBtn.id = "delete-" + li.id;
 141  rmBtn.innerText = "-";
 142  rmBtn.addEventListener("click", removeItem);
 143
 144  link.appendChild(img);
 145  link.insertAdjacentHTML("beforeend", nme);
 146  li.appendChild(link);
 147  li.appendChild(rmBtn);
 148  
 149  return li;
 150
 151}
 152
 153
 154// --------------------------------
 155//
 156// Edit mode
 157//
 158// --------------------------------
 159
 160
 161function edit(event) {
 162  
 163  // Enter/exit edit mode
 164  
 165  if (editMode == true) {
 166    console.log("Exited edit mode");
 167    closeEdit(this);
 168    return false;
 169  }
 170
 171  console.log("Entered edit mode");
 172
 173  this.style.background = hovergrn;
 174  this.innerText = tick;
 175  addBtn= document.getElementById("addcol");
 176  addBtn.style.display = "flex";
 177  var cols = document.getElementsByTagName("ul");
 178
 179  for (let col of cols) {
 180    
 181    var titleDiv = col.getElementsByClassName("title")[0];
 182    titleDiv.getElementsByClassName("add")[0].style.display = "flex";
 183
 184    col.style.bottom = "26px";
 185    col.getElementsByClassName("grip")[0].style.display = "inline-block";
 186
 187    var rmColBtn = document.createElement("span"); 
 188    rmColBtn.className = "rmcol";
 189    rmColBtn.id = "rmcol-" + col.id;
 190    rmColBtn.innerText = rmText;
 191    titleDiv.appendChild(rmColBtn);
 192
 193    var titleStatic = titleDiv.getElementsByTagName("p")[0];
 194    var titleInput = document.createElement("input");
 195    titleInput.type = "text";
 196    titleInput.className = "colname";
 197    titleInput.placeholder = titleStatic.innerText;
 198    titleInput.value = titleStatic.innerText;
 199    titleDiv.insertBefore(titleInput, titleStatic);
 200    titleStatic.remove();
 201
 202  }
 203
 204  updateListeners();
 205  editMode = true;
 206}
 207
 208function closeEdit(editBtn) {
 209
 210  // Exit edit mode and clean up elements
 211
 212  editMode = false;
 213  editBtn.style.background = "";
 214  editBtn.innerText = "e";
 215
 216  var addBtn = document.getElementById("addcol");
 217  addBtn.style.display = "none";
 218
 219  var cols = document.getElementsByTagName("ul");
 220
 221  for (let col of cols) {
 222
 223    col.style.bottom = "0";
 224    col.getElementsByClassName("grip")[0].style.display = "none";
 225
 226    var rmColBtn = col.getElementsByClassName("title")[0].getElementsByClassName("rmcol")[0];
 227    rmColBtn.remove();
 228
 229    var titleDiv = col.getElementsByClassName("title")[0];
 230    titleDiv.getElementsByClassName("add")[0].style.display = "";
 231
 232    titleInput = titleDiv.getElementsByClassName("colname")[0];
 233    titleStatic = document.createElement("p");
 234    titleStatic.innerText = titleInput.value;
 235    if (titleStatic.innerText == "") {
 236      titleStatic.innerText = titleInput.placeholder;
 237    }
 238    titleInput.remove();
 239    titleDiv.appendChild(titleStatic);
 240
 241  }
 242
 243  saveConfig();
 244
 245}
 246
 247function addColumn(event) {
 248
 249  // Create a new column from Edit mode
 250
 251  var ul = document.createElement("ul");
 252
 253  // Make sure columns do not share an id
 254  var ex = document.querySelectorAll('[id^="new "]'); // existing "new" columns
 255  if (ex.length > 0) {
 256    indices = []
 257    for (let i of ex) {
 258     indices.push(Number(i.id.split(" ")[1])); 
 259    }
 260    ul.setAttribute("id", "new " + (Math.max.apply(Math, indices)+1).toString());
 261  }
 262  else {
 263    ul.setAttribute("id", "new 1");
 264  }
 265  ul.setAttribute('draggable', 'false');
 266
 267  var grip = document.createElement("span");
 268  grip.setAttribute("class", "grip");
 269  grip.addEventListener("mousedown", enableDrag);
 270  grip.addEventListener("mouseup", disableDrag);
 271  grip.style.display = "inline-block";
 272  ul.style.bottom = "26px";
 273  ul.appendChild(grip)
 274
 275  var titleDiv = document.createElement("div");
 276  titleDiv.setAttribute("class", "title");
 277
 278  var titleInput = document.createElement("input");
 279  titleInput.type = "text";
 280  titleInput.className = "colname";
 281  titleInput.placeholder = ul.id;
 282  titleDiv.appendChild(titleInput);
 283
 284  var addBtn = document.createElement("span");
 285  addBtn.style.display = "flex";
 286  addBtn.innerText = addText;
 287  addBtn.setAttribute("class", "add");
 288  addBtn.setAttribute("id", "add-" + ul.id);
 289  addBtn.addEventListener("click", addItem);
 290  titleDiv.appendChild(addBtn);
 291
 292  var rmColBtn = document.createElement("span"); 
 293  rmColBtn.className = "rmcol";
 294  rmColBtn.id = "rmcol-" + ul.id;
 295  rmColBtn.innerText = rmText;
 296  titleDiv.appendChild(rmColBtn);
 297
 298  ul.appendChild(titleDiv);
 299
 300  document.getElementById("links").appendChild(ul);
 301  saveConfig();
 302  updateListeners();
 303  titleInput.focus();
 304
 305}
 306
 307
 308function removeColumn(event) {
 309
 310  // Delete column in edit mode
 311  
 312  this.parentNode.parentNode.remove();
 313  saveConfig();
 314
 315}
 316
 317
 318function removeItem(event) {
 319
 320  // Remove link in edit or normal mode
 321  
 322  this.parentNode.outerHTML = "";
 323  delete this.parentNode;
 324  saveConfig();
 325
 326}
 327
 328
 329function addItem(event) {
 330
 331  // Interface for adding a new list item to an existing category
 332
 333  var ul = this.parentNode.parentNode;
 334  var id = ul.id;
 335
 336  // Check if a form has already been generated for this column
 337  existing = ul.getElementsByClassName("new");
 338  if (existing.length > 0) {
 339    existing[0].getElementsByClassName("name")[0].focus();
 340    return false;
 341  }
 342
 343  var li = document.createElement("li");
 344  li.setAttribute("class", "new");
 345  li.addEventListener("keyup", formKeys); 
 346
 347  var saveBtn = document.createElement("span");
 348  saveBtn.className = "save";
 349  saveBtn.tabIndex = "3";
 350  saveBtn.id = "save-" + ul.id;
 351  saveBtn.innerText = tick;
 352  saveBtn.addEventListener("click", saveItem);
 353  saveBtn.addEventListener("mouseover", saveMouseOver);
 354  saveBtn.addEventListener("mouseout", saveMouseOut);
 355  li.appendChild(saveBtn);
 356
 357  var nameInput = document.createElement("input");
 358  nameInput.type = "text";
 359  nameInput.className = "name";
 360  nameInput.placeholder = "name";
 361  nameInput.tabIndex = "1";
 362  li.appendChild(nameInput);
 363
 364  var urlInput = document.createElement("input");
 365  urlInput.type = "url";
 366  urlInput.className = "url";
 367  urlInput.placeholder = "url";
 368  urlInput.tabIndex = "2";
 369  urlInput.spellcheck = "false";
 370  li.appendChild(urlInput);
 371
 372  ul.appendChild(li);
 373  updateListeners();
 374  nameInput.focus();
 375
 376}
 377
 378
 379function saveItem(event) {
 380
 381  // Add new item to a column after pressing the save button in form
 382
 383  var li = this.parentNode;
 384  var ul = this.parentNode.parentNode;
 385  var nameField = li.getElementsByClassName("name")[0];
 386  var urlField = li.getElementsByClassName("url")[0];
 387
 388  if (nameField.value != "" && urlField.value != "" && urlField.validity.typeMismatch== false) {
 389
 390    var newli = genItem(ul, nameField.value, urlField.value);
 391    li.remove();
 392    delete li;
 393
 394    ul.appendChild(newli);
 395    saveConfig();
 396
 397  }
 398  else {
 399
 400    if (nameField.value == "" && urlField.value == "") {
 401      console.log("No data supplied, deleting form");
 402      li.remove();
 403    }
 404    else {
 405      console.log("Missing data, press Esc to delete form");
 406    }
 407
 408  }
 409
 410}
 411
 412
 413// --------------------------------
 414//
 415// UI event listeners
 416//
 417// --------------------------------
 418
 419
 420function updateListeners() {
 421
 422  // Update event listeners for interface elements, since listeners are tied DOM objects which are lost on dataTransfer operations
 423
 424  var addBtns = document.getElementsByClassName("add");
 425  for (let addBtn of addBtns) {
 426    addBtn.addEventListener("click", addItem);
 427  }
 428
 429  var rmBtns = document.getElementsByClassName("rm");
 430  for (let rmBtn of rmBtns) {
 431    rmBtn.addEventListener("click", removeItem);
 432  }
 433
 434  var rmColBtns = document.getElementsByClassName("rmcol");
 435  for (let rmColBtn of rmColBtns) {
 436    rmColBtn.addEventListener("click", removeColumn); 
 437  }
 438
 439  var saveBtns = document.getElementsByClassName("save");
 440  for (let saveBtn of saveBtns) {
 441    saveBtn.addEventListener("click", saveItem);
 442    saveBtn.addEventListener("mouseover", saveMouseOver);
 443    saveBtn.addEventListener("mouseout", saveMouseOut);
 444  }
 445
 446}
 447
 448
 449function saveMouseOver(event) {
 450  nameField = this.parentNode.getElementsByClassName("name")[0];
 451  urlField = this.parentNode.getElementsByClassName("url")[0];
 452  if (nameField.value === ''  || urlField.value === '' || urlField.validity.typeMismatch == true) {
 453    this.style.background = removebg;
 454  }
 455  else {
 456    this.style.background = hovergrn; 
 457  }
 458}
 459
 460
 461function saveMouseOut(event) {
 462  this.style.background = hoverbg2;
 463}
 464
 465
 466function enableDrag() {
 467  // Enable drag & drop when grip is grabbed (otherwise any mouse click triggers dragStart)
 468  console.log("Drag started");
 469  uls = document.getElementsByTagName("ul");
 470  for (let ul of uls) {
 471    ul.setAttribute("draggable", "true");
 472    ul.addEventListener('dragstart', dragStart, false);
 473    ul.addEventListener('dragover', dragOver, false);
 474    ul.addEventListener('drop', drop, false);
 475  }
 476}
 477
 478
 479function disableDrag() {
 480  // Disable drag after grip has been released
 481  console.log("Drag ended");
 482  uls = document.getElementsByTagName("ul");
 483  for (let ul of uls) {
 484    ul.setAttribute("draggable", "false");
 485    ul.removeEventListener('dragstart');
 486    ul.removeEventListener('dragover');
 487    ul.removeEventListener('drop');
 488  }
 489}
 490
 491
 492function dragStart(e) {
 493  dragSrcEl = this;
 494  e.dataTransfer.effectAllowed = 'move';
 495  e.dataTransfer.setData('text/html', this.innerHTML);
 496}
 497
 498
 499function dragOver(e) {
 500  if (e.preventDefault) {
 501    e.preventDefault();
 502  }
 503  e.dataTransfer.dropEffect = 'move';
 504  return false;
 505}
 506
 507
 508function drop(e) {
 509  if (e.stopPropagation); {
 510    e.stopPropagation();
 511  }
 512  if (dragSrcEl != this) {
 513    var srcTitleInput = dragSrcEl.getElementsByClassName("colname")[0].value;
 514    var destTitleInput = this.getElementsByClassName("colname")[0].value;
 515    dragSrcEl.innerHTML = this.innerHTML;
 516    dragSrcEl.getElementsByClassName("colname")[0].value = destTitleInput;
 517    this.innerHTML = e.dataTransfer.getData('text/html');
 518    this.getElementsByClassName("colname")[0].value = srcTitleInput;
 519    saveConfig();
 520    updateListeners();
 521  }
 522  return false;
 523}
 524
 525
 526function formKeys(e) {
 527  var focus = document.activeElement;
 528  if (focus.parentNode.className != "new") {
 529    return false;
 530  }
 531  switch (e.which) {
 532    case 27: // escape
 533      focus.parentNode.remove();
 534      break;
 535    case 13: // enter
 536      focus.parentNode.getElementsByClassName("save")[0].click();
 537      break;
 538  }
 539}
 540
 541
 542// --------------------------------
 543//
 544// Configuration management
 545//
 546// --------------------------------
 547
 548
 549function saveConfig() {
 550  
 551  // Save current DOM structure as JSON data
 552
 553  console.log("Saving settings");
 554  data = []
 555  for (let ul of document.getElementsByTagName("ul")) {
 556    data.push(columnToArray(ul, true));
 557  }
 558  chrome.storage.sync.set( {"lists": data} );
 559
 560}
 561
 562
 563function columnToArray(ul, title = false) {
 564
 565  // Convert a column of data (ul) to a 2D array, optionally including the title
 566
 567  var data = [];
 568  var items = ul.getElementsByClassName("item");
 569
 570  if (title == true && editMode == true) {
 571    data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].value;
 572    if (data[0] == "" || data[0] == null) {
 573      console.log("Using default category name since input was empty");
 574      data[0] = ul.getElementsByClassName("title")[0].getElementsByTagName("input")[0].placeholder;
 575    }
 576  }
 577  else if (title == true) {
 578    data[0] = ul.getElementsByClassName('title')[0].getElementsByTagName("p")[0].textContent;
 579  }
 580
 581  for (let li of items) {
 582    if (li.class == "new" || li.class == "title") {
 583      continue; // Ignore input forms and titles (already handled)
 584    }
 585    else {
 586      data.push([li.innerText, li.getAttribute("href")]);
 587    }
 588  }
 589
 590  return data;
 591
 592}