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}