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
|
import 'webext-base-css/webext-base.css';
import './options.css';
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import domify from 'doma';
import select from 'select-dom';
import delegate from 'delegate-it';
import fitTextarea from 'fit-textarea';
import * as indentTextarea from 'indent-textarea';
import {perDomainOptions} from './options-storage';
function moveDisabledFeaturesToTop(): void {
const container = select('.js-features')!;
for (const unchecked of select.all('.feature [type=checkbox]:not(:checked)', container).reverse()) {
// .reverse() needed to preserve alphabetical order while prepending
container.prepend(unchecked.closest('.feature')!);
}
}
function buildFeatureCheckbox({id, description, screenshot}: FeatureMeta): HTMLElement {
const descriptionElement = domify.one(description)!;
descriptionElement.className = 'description';
return (
<div className="feature" data-text={`${id} ${description}`.toLowerCase()}>
<input type="checkbox" name={`feature:${id}`} id={id}/>
<div className="info">
<label htmlFor={id}>
<span className="feature-name">{id}</span>
{' '}
<a href={`https://github.com/sindresorhus/refined-github/blob/master/source/features/${id}.tsx`}>
source
</a>
{screenshot && <>, <a href={screenshot}>screenshot</a></>}
{descriptionElement}
</label>
</div>
</div>
);
}
async function clearCacheHandler(event: Event): Promise<void> {
await cache.clear();
const button = event.target as HTMLButtonElement;
const initialText = button.textContent;
button.textContent = 'Cache cleared!';
button.disabled = true;
setTimeout(() => {
button.textContent = initialText;
button.disabled = false;
}, 2000);
}
function featuresFilterHandler(event: Event): void {
const keywords = (event.currentTarget as HTMLInputElement).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));
}
}
async function highlightNewFeatures(): Promise<void> {
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');
}
}
void browser.storage.local.set({featuresAlreadySeen});
}
async function generateDom(): Promise<void> {
// Generate list
select('.js-features')!.append(...__featuresMeta__.map(buildFeatureCheckbox));
// Update list from saved options
await perDomainOptions.syncForm('form');
// Decorate list
moveDisabledFeaturesToTop();
void highlightNewFeatures();
// Move debugging tools higher when side-loaded
if (process.env.NODE_ENV === 'development') {
select('#debugging-position')!.replaceWith(select('#debugging')!);
}
}
function addEventListeners(): void {
// Update domain-dependent page content when the domain is changed
select('.js-options-sync-selector')?.addEventListener('change', ({currentTarget: dropdown}) => {
select<HTMLAnchorElement>('#personal-token-link')!.host = (dropdown as HTMLSelectElement).value;
});
// Refresh page when permissions are changed (because the dropdown selector needs to be regenerated)
browser.permissions.onRemoved.addListener(() => location.reload());
browser.permissions.onAdded.addListener(() => location.reload());
// Improve textareas editing
fitTextarea.watch('textarea');
indentTextarea.watch('textarea');
// Filter feature list
select('#filter-features')!.addEventListener('input', featuresFilterHandler);
// Add cache clearer
select('#clear-cache')!.addEventListener('click', clearCacheHandler);
// Ensure all links open in a new tab #3181
delegate(document, '[href^="http"]', 'click', (event: delegate.Event<MouseEvent, HTMLAnchorElement>) => {
event.preventDefault();
window.open(event.delegateTarget.href);
});
}
async function init(): Promise<void> {
await generateDom();
addEventListeners();
}
void init();
|