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