summaryrefslogtreecommitdiff
path: root/source/features/toggle-files-button.tsx
blob: 947bac75bf7771b37018a96086847e62cc148f9b (plain) (blame)
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
import './toggle-files-button.css';
import select from 'select-dom';
import {CachedValue} from 'webext-storage-cache';
import React from 'dom-chef';
import delegate, {DelegateEvent} from 'delegate-it';
import * as pageDetect from 'github-url-detection';
import {ChevronDownIcon} from '@primer/octicons-react';

import features from '../feature-manager.js';
import observe from '../helpers/selector-observer.js';
import {isHasSelectorSupported} from '../helpers/select-has.js';

const wereFilesHidden = new CachedValue<boolean>('files-hidden');
const toggleButtonClass = 'rgh-toggle-files';

function addButton(filesBox: HTMLElement): void {
	select('ul:has(.octicon-history)', filesBox)?.append(
		<button
			type="button"
			className={`btn-octicon ${toggleButtonClass}`}
			aria-label="Hide files"
		>
			<ChevronDownIcon/>
		</button>,
	);
}

type Targets = {
	fileList: HTMLElement;
	buttonWrapper: Element;
};

function getTargets(): Targets {
	const fileList = select('[aria-labelledby="files"]')!;
	const buttonWrapper = fileList.nextElementSibling!;
	return {fileList, buttonWrapper};
}

function firstCollapseOnDesktop(targets = getTargets()): void {
	targets.fileList.classList.remove('d-md-block');
	targets.buttonWrapper.classList.remove('d-md-none');
}

async function toggleList(): Promise<void> {
	const targets = getTargets();
	const button = targets.buttonWrapper.firstElementChild as HTMLButtonElement;

	if (targets.fileList.classList.contains('d-md-block')) {
		// On the first click, collapse the list and enable native toggling on desktop
		firstCollapseOnDesktop(targets);
		if (window.matchMedia('(min-width: 768px)').matches) {
			// We just hid the file list, no further action is necessary on desktop
			void wereFilesHidden.set(true);
			return;
		}

		// On mobile nothing visually happened because by default the list is already hidden, so it continues to actually hide the list via the native button.
	}

	// Toggle file list via native button, open or close
	button.click();
}

async function updateView(anchor: HTMLHeadingElement): Promise<void> {
	const filesBox = anchor.parentElement!;
	addButton(filesBox);
	if (await wereFilesHidden.get()) {
		// This only applies on desktop; Mobile already always starts collapsed and we're not changing that
		firstCollapseOnDesktop();
	}
}

async function recordToggle({detail}: DelegateEvent<CustomEvent>): Promise<void> {
	await wereFilesHidden.set(!detail.open);
}

async function init(signal: AbortSignal): Promise<void> {
	observe('.Box h2#files', updateView, {signal});
	delegate(`.${toggleButtonClass}`, 'click', toggleList, {signal});
	delegate('#files ~ .Details', 'details:toggled', recordToggle, {signal});
}

void features.add(import.meta.url, {
	asLongAs: [
		isHasSelectorSupported,
	],
	include: [
		pageDetect.isRepoTree,
	],
	init,
});

/*

Test URLs

https://github.com/refined-github/refined-github
https://github.com/refined-github/sandbox/tree/other-branch

*/