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