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}