1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
import 'webext-base-css/webext-base.css';
import './options.css';
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import select from 'select-dom';
import fitTextarea from 'fit-textarea';
import {applyToLink} from 'shorten-repo-url';
import * as indentTextarea from 'indent-textarea';
import {getAllOptions} from './options-storage';
import * as domFormatters from './libs/dom-formatters';
function parseDescription(description: string): DocumentFragment {
const descriptionElement = <span>{description}</span>;
domFormatters.linkifyIssues(descriptionElement, {
baseUrl: 'https://github.com',
user: 'sindresorhus',
repository: 'refined-github'
});
domFormatters.linkifyURLs(descriptionElement);
domFormatters.parseBackticks(descriptionElement);
for (const a of select.all('a', descriptionElement)) {
applyToLink(a);
}
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{[...descriptionElement.childNodes]}</>;
}
function buildFeatureCheckbox({id, description, screenshot, disabled}: FeatureMeta): HTMLElement {
// `undefined` disconnects it from the options
const key = disabled ? undefined : `feature:${id}`;
return (
<div className={`feature feature--${disabled ? 'disabled' : 'enabled'}`} data-text={`${id} ${description}`.toLowerCase()}>
<input type="checkbox" name={key} id={id} disabled={Boolean(disabled)}/>
<div className="info">
<label for={id}>
<span className="feature-name">{id}</span>
{' '}
{disabled && <small>{parseDescription(`(Disabled because of ${disabled}) `)}</small>}
<a href={`https://github.com/sindresorhus/refined-github/blob/master/source/features/${id}.tsx`}>
source
</a>
{screenshot && <>, <a href={screenshot}>screenshot</a></>}
<p className="description">{parseDescription(description)}</p>
</label>
</div>
</div>
);
}
async function init(): Promise<void> {
// Generate list
const container = select('.js-features')!;
container.append(...__featuresMeta__.map(buildFeatureCheckbox));
// Update list from saved options
const form = select('form')!;
const optionsByDomain = await getAllOptions();
await optionsByDomain.get('github.com')!.syncForm(form);
// Move disabled features first
for (const unchecked of select.all('.feature--enabled [type=checkbox]:not(:checked)', container).reverse()) {
// .reverse() needed to preserve alphabetical order while prepending
container.prepend(unchecked.closest('.feature')!);
}
// Highlight new features
const {featuresAlreadySeen} = await browser.storage.local.get({featuresAlreadySeen: {}});
const isFirstVisit = Object.keys(featuresAlreadySeen).length === 0;
const tenDaysAgo = Date.now() - (10 * 24 * 60 * 60 * 1000);
for (const feature of select.all('.feature [type=checkbox]')) {
if (!(feature.id in featuresAlreadySeen)) {
featuresAlreadySeen[feature.id] = isFirstVisit ? tenDaysAgo : Date.now();
}
if (featuresAlreadySeen[feature.id] > tenDaysAgo) {
feature.parentElement!.classList.add('feature-new');
}
}
browser.storage.local.set({featuresAlreadySeen});
// Improve textareas editing
fitTextarea.watch('textarea');
indentTextarea.watch('textarea');
// Filter feature options
const filterField = select<HTMLInputElement>('#filter-features')!;
filterField.addEventListener('input', () => {
const keywords = filterField.value.toLowerCase()
.replace(/\W/g, ' ')
.split(/\s+/)
.filter(Boolean); // Ignore empty strings
for (const feature of select.all('.feature')) {
feature.hidden = !keywords.every(word => feature.dataset.text!.includes(word));
}
});
const button = select<HTMLButtonElement>('#clear-cache')!;
button.addEventListener('click', async () => {
await cache.clear();
const initialText = button.textContent;
button.textContent = 'Cache cleared!';
button.disabled = true;
setTimeout(() => {
button.textContent = initialText;
button.disabled = false;
}, 2000);
});
// GitHub Enterprise domain picker
if (optionsByDomain.size > 1) {
const dropdown = (
<select>
{[...optionsByDomain.keys()].map(domain => <option value={domain}>{domain}</option>)}
</select>
) as unknown as HTMLSelectElement;
form.before(<p>Domain selector: {dropdown}</p>, <hr/>);
dropdown.addEventListener('change', () => {
for (const [domain, options] of optionsByDomain) {
if (dropdown.value === domain) {
options.syncForm(form);
} else {
options.stopSyncForm();
}
}
select<HTMLAnchorElement>('#personal-token-link')!.host = dropdown.value;
});
}
// Move debugging tools higher when side-loaded
if (process.env.NODE_ENV === 'development') {
select('#debugging-position')!.replaceWith(select('#debugging')!);
}
}
init();
|