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