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