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