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