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