1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
2// See the LICENCE file in the repository root for full licence text.
3
4import { padStart } from 'lodash';
5import { CSSProperties } from 'react';
6import { urlPresence } from './css';
7
8const byteSuffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
9const kilo = 1000;
10
11export function bottomPage() {
12 return bottomPageDistance() === 0;
13}
14
15export function bottomPageDistance() {
16 const page = document.documentElement;
17
18 return page.scrollHeight - page.scrollTop - page.clientHeight;
19}
20
21export function createClickCallback(target: unknown) {
22 if (target instanceof HTMLElement) {
23 // plain javascript here doesn't trigger submit events
24 // which means jquery-ujs handler won't be triggered
25 // reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
26 if (target instanceof HTMLFormElement) {
27 return () => $(target).submit();
28 }
29
30 // inversely, using jquery here won't actually click the thing
31 // reference: https://github.com/jquery/jquery/blob/f5aa89af7029ae6b9203c2d3e551a8554a0b4b89/src/event.js#L586
32 return () => target.click();
33 }
34}
35
36export function cssVar2x(url?: string | null) {
37 if (url == null) return;
38
39 return {
40 '--bg': urlPresence(url),
41 '--bg-2x': urlPresence(make2x(url)),
42 } as CSSProperties;
43}
44
45function padTimeComponent(time: number) {
46 return padStart(time.toString(), 2, '0');
47}
48
49export function formatBytes(bytes: number, decimals = 2) {
50 if (bytes < kilo) {
51 return `${bytes} B`;
52 }
53
54 const i = Math.floor(Math.log(bytes) / Math.log(kilo));
55 return `${formatNumber(bytes / Math.pow(kilo, i), decimals)} ${byteSuffixes[i]}`;
56}
57
58export function formatDuration(valueSecond: number) {
59 const s = valueSecond % 60;
60 const m = Math.floor(valueSecond / 60) % 60;
61 const h = Math.floor(valueSecond / 3600);
62
63 if (h > 0) {
64 return `${h}:${padTimeComponent(m)}:${padTimeComponent(s)}`;
65 }
66
67 return `${m}:${padTimeComponent(s)}`;
68}
69
70const defaultNumberFormatter = new Intl.NumberFormat(window.currentLocale);
71
72export function formatNumber(num: number, precision?: number, options?: Intl.NumberFormatOptions, locale?: string) {
73 if (precision == null && options == null && locale == null) {
74 return defaultNumberFormatter.format(num);
75 }
76
77 options ??= {};
78
79 if (precision != null) {
80 options.minimumFractionDigits = precision;
81 options.maximumFractionDigits = precision;
82 }
83
84 return num.toLocaleString(locale ?? window.currentLocale, options);
85}
86
87const defaultSuffixedNumberOptions = {
88 maximumFractionDigits: 1,
89 minimumFractionDigits: 0,
90 notation: 'compact',
91} as const;
92const defaultSuffixedNumberFormatter = new Intl.NumberFormat(window.currentLocale, defaultSuffixedNumberOptions);
93
94export function formatNumberSuffixed(num?: number) {
95 return num == null
96 ? undefined
97 : defaultSuffixedNumberFormatter.format(num);
98}
99
100export function htmlElementOrNull(thing: unknown) {
101 if (thing instanceof HTMLElement) {
102 return thing;
103 }
104
105 return null;
106}
107
108export function isClickable(maybeEl: unknown): boolean {
109 const el = htmlElementOrNull(maybeEl);
110
111 if (el == null) {
112 return false;
113 }
114
115 if (isInputElement(el) || ['A', 'BUTTON'].includes(el.tagName)) {
116 return true;
117 }
118
119 const parentEl = htmlElementOrNull(el.parentNode);
120 if (parentEl != null) {
121 return isClickable(parentEl);
122 }
123
124 return false;
125}
126
127export function isInputElement(el: HTMLElement) {
128 return ['INPUT', 'OPTION', 'SELECT', 'TEXTAREA'].includes(el.tagName) || el.isContentEditable;
129}
130
131export const transparentGif = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
132
133export function make2x(url?: string) {
134 if (url == null) return;
135
136 return url.replace(/(\.[^.]+)$/, '@2x$1');
137}
138
139export function stripTags(str: string) {
140 return str.replace(/<[^>]*>/g, '');
141}