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