Skip to content
This repository has been archived by the owner on Feb 24, 2023. It is now read-only.

Commit

Permalink
explode ui.ts in to smaller files
Browse files Browse the repository at this point in the history
  • Loading branch information
moribellamy committed May 22, 2020
1 parent a75d0cf commit 477691a
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 304 deletions.
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Webpack uses MiniCssExtractPlugin when it sees this.
import './scss/app.scss';

import { graytabby } from './app/ui';
import { graytabby } from './app/graytabby';
import { PAGE_LOAD } from './lib/globals';

graytabby().then(
Expand Down
23 changes: 23 additions & 0 deletions src/app/debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DOCUMENT } from '../lib/globals';
import { loadAllTabGroups } from './tabs_store';
import { ingestTabs } from './tabs_ui';

export class Debugger {
async double(): Promise<void> {
const groups = await loadAllTabGroups();
let earliest = Math.min(...groups.map(g => g.date));
const groupDiv: HTMLDivElement = DOCUMENT.get().querySelector('#groups');
const promises: Promise<void>[] = [];
for (const group of groups) {
earliest -= 1000;
await ingestTabs(<any>group.tabs, groupDiv, () => earliest);
}
await Promise.all(promises);
}
}

declare global {
interface Window {
gt: Debugger;
}
}
21 changes: 21 additions & 0 deletions src/app/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DOCUMENT } from '../lib/globals';

export function makeElement(
type: string,
attrs: { [key: string]: string } = {},
children?: string | Element[],
): Element {
const elem = DOCUMENT.get().createElement(type);
for (const key in attrs) {
elem.setAttribute(key, attrs[key]);
}

if (children === undefined) return elem;

if (typeof children === 'string') {
elem.innerText = children;
} else {
children.map(c => elem.appendChild(c));
}
return elem;
}
20 changes: 20 additions & 0 deletions src/app/graytabby.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* User interface code for graytabby.
*
* Anything responsible for responding to user input or for displaying data to the user.
*/

import { DOCUMENT } from '../lib/globals';
import { Debugger } from './debugger';
import { bindOptions } from './options_ui';
import { bindTabs } from './tabs_ui';

/**
* The main entry point for GrayTabby.
*/
export async function graytabby(): Promise<void> {
DOCUMENT.get().title = 'GrayTabby';
await bindOptions();
await bindTabs();
DOCUMENT.get().defaultView.gt = new Debugger();
}
13 changes: 13 additions & 0 deletions src/app/info_ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DOCUMENT } from '../lib/globals';

let totalTabs = 0;

export function updateInfo(delta: number): void {
const infoNode = <HTMLParagraphElement>DOCUMENT.get().querySelector('#info');
totalTabs += delta;
infoNode.innerText = 'Total tabs: ' + totalTabs.toString();
}

export function getTotalTabs(): number {
return totalTabs;
}
59 changes: 59 additions & 0 deletions src/app/options_ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getOptions, setOptions } from '../lib/options';
import { DOCUMENT } from '../lib/globals';

export async function bindOptions(): Promise<void> {
const document = DOCUMENT.get();
const modal = <HTMLDivElement>document.querySelector('#optionsModal');
const logo = <HTMLImageElement>document.querySelector('#logo');
const content = <HTMLDivElement>document.querySelector('#optionsModal .content');
logo.onclick = () => (modal.style.display = 'block');
modal.onclick = event => {
if (!content.contains(<HTMLElement>event.target)) modal.style.display = 'none';
};

const checkboxes: HTMLInputElement[] = Array.from(
content.querySelectorAll('label input[type="checkbox"]'),
);
for (const checkbox of checkboxes) {
const label = <HTMLLabelElement>checkbox.parentElement;
const span = <HTMLSpanElement>label.querySelector('span');
span.onclick = () => (checkbox.checked = !checkbox.checked);
}

const optionsLimitNode = <HTMLInputElement>document.querySelector('#optionsLimit');
const optionsDupesNode = <HTMLInputElement>document.querySelector('#optionsDupes');
const optionsAtLoad = await getOptions();

optionsLimitNode.value = optionsAtLoad.tabLimit.toString();
optionsDupesNode.checked = optionsAtLoad.archiveDupes;

optionsDupesNode.onchange = async () => {
await setOptions({
archiveDupes: optionsDupesNode.checked,
});
};

// From https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input
// HTML5 validators have poor support at the moment.
optionsLimitNode.onkeydown = (e): boolean => {
return (
e.ctrlKey ||
e.altKey ||
(47 < e.keyCode && e.keyCode < 58 && e.shiftKey == false) ||
(95 < e.keyCode && e.keyCode < 106) ||
e.keyCode == 8 ||
e.keyCode == 9 ||
(e.keyCode > 34 && e.keyCode < 40) ||
e.keyCode == 46
);
};

optionsLimitNode.onkeyup = async () => {
const newLimit = Number(optionsLimitNode.value);
if (newLimit != NaN) {
await setOptions({
tabLimit: newLimit,
});
}
};
}
File renamed without changes.
153 changes: 153 additions & 0 deletions src/app/tabs_ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { BROWSER, DOCUMENT, ARCHIVAL } from '../lib/globals';
import { updateInfo, getTotalTabs } from './info_ui';
import {
keyFromDate,
dateFromKey,
GrayTabGroup,
GrayTab,
eraseTabGroup,
saveTabGroup,
keyFromGroup,
loadAllTabGroups,
} from './tabs_store';
import { makeElement } from './dom';
import { getOptions } from '../lib/options';
import { BrowserTab } from '../lib/types';

function prependInsideContainer(parent: Element, child: Element): Element {
if (parent.firstChild == null) parent.appendChild(child);
else parent.insertBefore(child, parent.firstChild);
return child;
}

function countGroups(): number {
return DOCUMENT.get().querySelectorAll('#groups > div').length;
}

function faviconLocation(url: string): string {
const domain = new URL(url).hostname;
if (domain) return `https://www.google.com/s2/favicons?domain=${domain}`;
return '';
}

function renderFavicon(url: string): HTMLImageElement {
const loc = faviconLocation(url);
return <HTMLImageElement>makeElement('img', {
src: loc,
width: '16',
height: '16',
});
}

function groupFromDiv(target: number | HTMLDivElement): GrayTabGroup {
let div: HTMLDivElement;
let date: number;
if (typeof target === 'number') {
date = target;
div = DOCUMENT.get().querySelector(`#${keyFromDate(date)}`);
} else {
div = target;
date = dateFromKey(div.id);
}
const group: GrayTabGroup = {
date: date,
tabs: [],
};
if (div == null) return group;
const lis = div.querySelectorAll('li');
lis.forEach(li => {
const a: HTMLAnchorElement = li.querySelector('a');
const tab: GrayTab = {
key: Number(a.attributes.getNamedItem('key').value),
url: a.href,
title: a.innerText,
};
group.tabs.push(tab);
});
return group;
}

/**
* Updates the backend to match the DOM for the tab group with the given date.
*/
async function syncGroupFromDOM(target: number | HTMLDivElement): Promise<void> {
const group = groupFromDiv(target);
if (group.tabs.length == 0) {
return eraseTabGroup(group.date);
}
return saveTabGroup(group);
}

async function linkRowClickhandler(event: MouseEvent): Promise<void> {
const a = <HTMLAnchorElement>event.target;
const row = <HTMLDivElement>a.parentElement;
const li = <HTMLDivElement>row.parentElement;
const ul = <HTMLUListElement>li.parentElement;
const groupDiv = <HTMLDivElement>ul.parentElement;

event.preventDefault();
await BROWSER.get().tabs.create({ url: a.href, active: false });
li.remove();
updateInfo(-1);
if (groupDiv.querySelector('li') == null) groupDiv.remove();
await syncGroupFromDOM(dateFromKey(groupDiv.id));
}

function renderLinkRow(group: GrayTabGroup, tab: GrayTab): HTMLLIElement {
const li = <HTMLLIElement>makeElement('li');
const row = <HTMLDivElement>li.appendChild(makeElement('div'));
row.appendChild(renderFavicon(tab.url));
const a = <HTMLAnchorElement>(
row.appendChild(makeElement('a', { href: tab.url, key: tab.key.toString() }, tab.title))
);
a.onclick = linkRowClickhandler;
return li;
}

function renderGroup(group: GrayTabGroup): HTMLDivElement {
const div = <HTMLDivElement>makeElement('div', { id: keyFromGroup(group) });
div.appendChild(makeElement('span', {}, new Date(group.date).toLocaleString()));
const ul = div.appendChild(makeElement('ul'));
for (const tab of group.tabs) {
ul.appendChild(renderLinkRow(group, tab));
}
return div;
}

export async function ingestTabs(
tabSummaries: BrowserTab[],
groupsNode: HTMLDivElement,
now = () => new Date().getTime(),
): Promise<void> {
if (tabSummaries.length == 0) return;
let counter = 0;
const group: GrayTabGroup = {
tabs: tabSummaries.map(ts => {
return { ...ts, key: counter++ };
}),
date: now(),
};
prependInsideContainer(groupsNode, renderGroup(group));
await syncGroupFromDOM(group.date);
updateInfo(group.tabs.length);

const tabLimit = (await getOptions()).tabLimit;
while (getTotalTabs() > tabLimit && countGroups() > 1) {
const victim = <HTMLDivElement>DOCUMENT.get().querySelector('#groups>div:last-child');
const removed = victim.querySelectorAll('li').length;
updateInfo(-removed);
await eraseTabGroup(dateFromKey(victim.id));
victim.remove();
}
}

export async function bindTabs(): Promise<void> {
const groupsNode = <HTMLDivElement>DOCUMENT.get().querySelector('#groups');
let counter = 0;
for (const group of await loadAllTabGroups()) {
counter += group.tabs.length;
groupsNode.appendChild(renderGroup(group));
}
ARCHIVAL.get().sub(summaries => ingestTabs(summaries, groupsNode));
updateInfo(counter);
}
Loading

0 comments on commit 477691a

Please sign in to comment.