mirror of https://github.com/go-gitea/gitea.git
Fix a number of Typescript issues (#31877)
Typescript error count is reduced from 633 to 540 with this. No runtime changes except in test code.
This commit is contained in:
parent
39d2fdefaf
commit
7207d93f01
|
@ -10,22 +10,52 @@ declare module '*.css' {
|
||||||
|
|
||||||
declare let __webpack_public_path__: string;
|
declare let __webpack_public_path__: string;
|
||||||
|
|
||||||
interface Window {
|
|
||||||
config: import('./web_src/js/types.ts').Config;
|
|
||||||
$: typeof import('@types/jquery'),
|
|
||||||
jQuery: typeof import('@types/jquery'),
|
|
||||||
htmx: typeof import('htmx.org'),
|
|
||||||
_globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & {
|
|
||||||
_inited: boolean,
|
|
||||||
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'htmx.org/dist/htmx.esm.js' {
|
declare module 'htmx.org/dist/htmx.esm.js' {
|
||||||
const value = await import('htmx.org');
|
const value = await import('htmx.org');
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'uint8-to-base64' {
|
||||||
|
export function encode(arrayBuffer: ArrayBuffer): string;
|
||||||
|
export function decode(base64str: string): ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
|
||||||
|
const value = await import('swagger-ui-dist');
|
||||||
|
export default value.SwaggerUIBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JQuery {
|
||||||
|
api: any, // fomantic
|
||||||
|
areYouSure: any, // jquery.are-you-sure
|
||||||
|
dimmer: any, // fomantic
|
||||||
|
dropdown: any; // fomantic
|
||||||
|
modal: any; // fomantic
|
||||||
|
tab: any; // fomantic
|
||||||
|
transition: any, // fomantic
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JQueryStatic {
|
||||||
|
api: any, // fomantic
|
||||||
|
}
|
||||||
|
|
||||||
interface Element {
|
interface Element {
|
||||||
_tippy: import('tippy.js').Instance;
|
_tippy: import('tippy.js').Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
config: import('./web_src/js/types.ts').Config;
|
||||||
|
$: typeof import('@types/jquery'),
|
||||||
|
jQuery: typeof import('@types/jquery'),
|
||||||
|
htmx: Omit<typeof import('htmx.org/dist/htmx.esm.js').default, 'config'> & {
|
||||||
|
config?: Writable<typeof import('htmx.org').default.config>,
|
||||||
|
},
|
||||||
|
ui?: any,
|
||||||
|
_globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & {
|
||||||
|
_inited: boolean,
|
||||||
|
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
|
||||||
|
},
|
||||||
|
__webpack_public_path__: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import {showErrorToast} from './modules/toast.ts';
|
import {showErrorToast} from './modules/toast.ts';
|
||||||
|
import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx
|
||||||
|
import type {HtmxResponseInfo} from 'htmx.org';
|
||||||
|
|
||||||
// https://github.com/bigskysoftware/idiomorph#htmx
|
type HtmxEvent = Event & {detail: HtmxResponseInfo};
|
||||||
import 'idiomorph/dist/idiomorph-ext.js';
|
|
||||||
|
|
||||||
// https://htmx.org/reference/#config
|
// https://htmx.org/reference/#config
|
||||||
window.htmx.config.requestClass = 'is-loading';
|
window.htmx.config.requestClass = 'is-loading';
|
||||||
window.htmx.config.scrollIntoViewOnBoost = false;
|
window.htmx.config.scrollIntoViewOnBoost = false;
|
||||||
|
|
||||||
// https://htmx.org/events/#htmx:sendError
|
// https://htmx.org/events/#htmx:sendError
|
||||||
document.body.addEventListener('htmx:sendError', (event) => {
|
document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => {
|
||||||
// TODO: add translations
|
// TODO: add translations
|
||||||
showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
|
showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://htmx.org/events/#htmx:responseError
|
// https://htmx.org/events/#htmx:responseError
|
||||||
document.body.addEventListener('htmx:responseError', (event) => {
|
document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => {
|
||||||
// TODO: add translations
|
// TODO: add translations
|
||||||
showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
|
showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -98,12 +98,12 @@ initGiteaFomantic();
|
||||||
initDirAuto();
|
initDirAuto();
|
||||||
initSubmitEventPolyfill();
|
initSubmitEventPolyfill();
|
||||||
|
|
||||||
function callInitFunctions(functions) {
|
function callInitFunctions(functions: (() => any)[]) {
|
||||||
// Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1"
|
// Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1"
|
||||||
// It is a quick check, no side effect so no need to do slow URL parsing.
|
// It is a quick check, no side effect so no need to do slow URL parsing.
|
||||||
const initStart = performance.now();
|
const initStart = performance.now();
|
||||||
if (window.location.search.includes('_ui_performance_trace=1')) {
|
if (window.location.search.includes('_ui_performance_trace=1')) {
|
||||||
let results = [];
|
let results: {name: string, dur: number}[] = [];
|
||||||
for (const func of functions) {
|
for (const func of functions) {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
func();
|
func();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {AnsiUp} from 'ansi_up';
|
import {AnsiUp} from 'ansi_up';
|
||||||
|
|
||||||
const replacements = [
|
const replacements: Array<[RegExp, string]> = [
|
||||||
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
|
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
|
||||||
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
|
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
|
||||||
];
|
];
|
||||||
|
|
||||||
// render ANSI to HTML
|
// render ANSI to HTML
|
||||||
export function renderAnsi(line) {
|
export function renderAnsi(line: string): string {
|
||||||
// create a fresh ansi_up instance because otherwise previous renders can influence
|
// create a fresh ansi_up instance because otherwise previous renders can influence
|
||||||
// the output of future renders, because ansi_up is stateful and remembers things like
|
// the output of future renders, because ansi_up is stateful and remembers things like
|
||||||
// unclosed opening tags for colors.
|
// unclosed opening tags for colors.
|
||||||
|
|
|
@ -8,7 +8,7 @@ window.addEventListener('load', async () => {
|
||||||
|
|
||||||
// Make the page's protocol be at the top of the schemes list
|
// Make the page's protocol be at the top of the schemes list
|
||||||
const proto = window.location.protocol.slice(0, -1);
|
const proto = window.location.protocol.slice(0, -1);
|
||||||
spec.schemes.sort((a, b) => {
|
spec.schemes.sort((a: string, b: string) => {
|
||||||
if (a === proto) return -1;
|
if (a === proto) return -1;
|
||||||
if (b === proto) return 1;
|
if (b === proto) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -17,7 +17,7 @@ test('svgParseOuterInner', () => {
|
||||||
test('SvgIcon', () => {
|
test('SvgIcon', () => {
|
||||||
const root = document.createElement('div');
|
const root = document.createElement('div');
|
||||||
createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
|
createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
|
||||||
const node = root.firstChild;
|
const node = root.firstChild as Element;
|
||||||
expect(node.nodeName).toEqual('svg');
|
expect(node.nodeName).toEqual('svg');
|
||||||
expect(node.getAttribute('width')).toEqual('24');
|
expect(node.getAttribute('width')).toEqual('24');
|
||||||
expect(node.getAttribute('height')).toEqual('24');
|
expect(node.getAttribute('height')).toEqual('24');
|
||||||
|
|
|
@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams;
|
||||||
export type RequestOpts = {
|
export type RequestOpts = {
|
||||||
data?: RequestData,
|
data?: RequestData,
|
||||||
} & RequestInit;
|
} & RequestInit;
|
||||||
|
|
||||||
|
export type IssueData = {
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
type: string,
|
||||||
|
index: string,
|
||||||
|
}
|
||||||
|
|
|
@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
|
test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
|
||||||
// TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not
|
|
||||||
// structurally comparable, so we convert to array to compare. The conversion can be
|
|
||||||
// removed once https://github.com/jsdom/jsdom/issues/2524 is resolved.
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const uint8array = encoder.encode.bind(encoder);
|
const uint8array = encoder.encode.bind(encoder);
|
||||||
|
|
||||||
expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
|
expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
|
||||||
expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"
|
expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"
|
||||||
|
|
||||||
expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?')));
|
expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?'));
|
||||||
expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~')));
|
expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~'));
|
||||||
expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?')));
|
expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?'));
|
||||||
expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~')));
|
expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~'));
|
||||||
|
|
||||||
expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
|
expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
|
||||||
expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a')));
|
expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a'));
|
||||||
expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a')));
|
expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('file detection', () => {
|
test('file detection', () => {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import {encode, decode} from 'uint8-to-base64';
|
import {encode, decode} from 'uint8-to-base64';
|
||||||
|
import type {IssueData} from './types.ts';
|
||||||
|
|
||||||
// transform /path/to/file.ext to file.ext
|
// transform /path/to/file.ext to file.ext
|
||||||
export function basename(path) {
|
export function basename(path: string): string {
|
||||||
const lastSlashIndex = path.lastIndexOf('/');
|
const lastSlashIndex = path.lastIndexOf('/');
|
||||||
return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
|
return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// transform /path/to/file.ext to .ext
|
// transform /path/to/file.ext to .ext
|
||||||
export function extname(path) {
|
export function extname(path: string): string {
|
||||||
const lastSlashIndex = path.lastIndexOf('/');
|
const lastSlashIndex = path.lastIndexOf('/');
|
||||||
const lastPointIndex = path.lastIndexOf('.');
|
const lastPointIndex = path.lastIndexOf('.');
|
||||||
if (lastSlashIndex > lastPointIndex) return '';
|
if (lastSlashIndex > lastPointIndex) return '';
|
||||||
|
@ -15,54 +16,54 @@ export function extname(path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// test whether a variable is an object
|
// test whether a variable is an object
|
||||||
export function isObject(obj) {
|
export function isObject(obj: any): boolean {
|
||||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns whether a dark theme is enabled
|
// returns whether a dark theme is enabled
|
||||||
export function isDarkTheme() {
|
export function isDarkTheme(): boolean {
|
||||||
const style = window.getComputedStyle(document.documentElement);
|
const style = window.getComputedStyle(document.documentElement);
|
||||||
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
|
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip <tags> from a string
|
// strip <tags> from a string
|
||||||
export function stripTags(text) {
|
export function stripTags(text: string): string {
|
||||||
return text.replace(/<[^>]*>?/g, '');
|
return text.replace(/<[^>]*>?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIssueHref(href) {
|
export function parseIssueHref(href: string): IssueData {
|
||||||
const path = (href || '').replace(/[#?].*$/, '');
|
const path = (href || '').replace(/[#?].*$/, '');
|
||||||
const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
|
const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
|
||||||
return {owner, repo, type, index};
|
return {owner, repo, type, index};
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
|
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
|
||||||
export function parseUrl(str) {
|
export function parseUrl(str: string): URL {
|
||||||
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
|
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return current locale chosen by user
|
// return current locale chosen by user
|
||||||
export function getCurrentLocale() {
|
export function getCurrentLocale(): string {
|
||||||
return document.documentElement.lang;
|
return document.documentElement.lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a month (0-11), returns it in the documents language
|
// given a month (0-11), returns it in the documents language
|
||||||
export function translateMonth(month) {
|
export function translateMonth(month: number) {
|
||||||
return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'});
|
return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a weekday (0-6, Sunday to Saturday), returns it in the documents language
|
// given a weekday (0-6, Sunday to Saturday), returns it in the documents language
|
||||||
export function translateDay(day) {
|
export function translateDay(day: number) {
|
||||||
return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'});
|
return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert a Blob to a DataURI
|
// convert a Blob to a DataURI
|
||||||
export function blobToDataURI(blob) {
|
export function blobToDataURI(blob: Blob): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener('load', (e) => {
|
reader.addEventListener('load', (e) => {
|
||||||
resolve(e.target.result);
|
resolve(e.target.result as string);
|
||||||
});
|
});
|
||||||
reader.addEventListener('error', () => {
|
reader.addEventListener('error', () => {
|
||||||
reject(new Error('FileReader failed'));
|
reject(new Error('FileReader failed'));
|
||||||
|
@ -75,7 +76,7 @@ export function blobToDataURI(blob) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert image Blob to another mime-type format.
|
// convert image Blob to another mime-type format.
|
||||||
export function convertImage(blob, mime) {
|
export function convertImage(blob: Blob, mime: string): Promise<Blob> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
@ -104,7 +105,7 @@ export function convertImage(blob, mime) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toAbsoluteUrl(url) {
|
export function toAbsoluteUrl(url: string): string {
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode an ArrayBuffer into a URLEncoded base64 string.
|
// Encode an ArrayBuffer into a URLEncoded base64 string.
|
||||||
export function encodeURLEncodedBase64(arrayBuffer) {
|
export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string {
|
||||||
return encode(arrayBuffer)
|
return encode(arrayBuffer)
|
||||||
.replace(/\+/g, '-')
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, '_')
|
.replace(/\//g, '_')
|
||||||
.replace(/=/g, '');
|
.replace(/=/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode a URLEncoded base64 to an ArrayBuffer string.
|
// Decode a URLEncoded base64 to an ArrayBuffer.
|
||||||
export function decodeURLEncodedBase64(base64url) {
|
export function decodeURLEncodedBase64(base64url: string): ArrayBuffer {
|
||||||
return decode(base64url
|
return decode(base64url
|
||||||
.replace(/_/g, '/')
|
.replace(/_/g, '/')
|
||||||
.replace(/-/g, '+'));
|
.replace(/-/g, '+'));
|
||||||
|
@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) {
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
const xmlSerializer = new XMLSerializer();
|
const xmlSerializer = new XMLSerializer();
|
||||||
|
|
||||||
export function parseDom(text, contentType) {
|
export function parseDom(text: string, contentType: DOMParserSupportedType): Document {
|
||||||
return domParser.parseFromString(text, contentType);
|
return domParser.parseFromString(text, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeXml(node) {
|
export function serializeXml(node: Element | Node): string {
|
||||||
return xmlSerializer.serializeToString(node);
|
return xmlSerializer.serializeToString(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
export function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
export function isImageFile({name, type}) {
|
export function isImageFile({name, type}: {name: string, type?: string}): boolean {
|
||||||
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
|
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVideoFile({name, type}) {
|
export function isVideoFile({name, type}: {name: string, type?: string}): boolean {
|
||||||
return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
|
return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,23 @@ import type {ColorInput} from 'tinycolor2';
|
||||||
|
|
||||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||||
// Keep this in sync with modules/util/color.go
|
// Keep this in sync with modules/util/color.go
|
||||||
function getRelativeLuminance(color: ColorInput) {
|
function getRelativeLuminance(color: ColorInput): number {
|
||||||
const {r, g, b} = tinycolor(color).toRgb();
|
const {r, g, b} = tinycolor(color).toRgb();
|
||||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLightText(backgroundColor: ColorInput) {
|
function useLightText(backgroundColor: ColorInput): boolean {
|
||||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a background color, returns a black or white foreground color that the highest
|
// Given a background color, returns a black or white foreground color that the highest
|
||||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||||
export function contrastColor(backgroundColor: ColorInput) {
|
export function contrastColor(backgroundColor: ColorInput): string {
|
||||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveColors(obj: Record<string, string>) {
|
function resolveColors(obj: Record<string, string>): Record<string, string> {
|
||||||
const styles = window.getComputedStyle(document.documentElement);
|
const styles = window.getComputedStyle(document.documentElement);
|
||||||
const getColor = (name: string) => styles.getPropertyValue(name).trim();
|
const getColor = (name: string) => styles.getPropertyValue(name).trim();
|
||||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
|
||||||
|
|
|
@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() {
|
||||||
/**
|
/**
|
||||||
* Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
|
* Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
|
||||||
* Note: This function doesn't account for all possible visibility scenarios.
|
* Note: This function doesn't account for all possible visibility scenarios.
|
||||||
* @param {HTMLElement} element The element to check.
|
|
||||||
* @returns {boolean} True if the element is visible.
|
|
||||||
*/
|
*/
|
||||||
export function isElemVisible(element: HTMLElement) {
|
export function isElemVisible(element: HTMLElement): boolean {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
|
|
||||||
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
export async function pngChunks(blob) {
|
type PngChunk = {
|
||||||
|
name: string,
|
||||||
|
data: Uint8Array,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pngChunks(blob: Blob): Promise<PngChunk[]> {
|
||||||
const uint8arr = new Uint8Array(await blob.arrayBuffer());
|
const uint8arr = new Uint8Array(await blob.arrayBuffer());
|
||||||
const chunks = [];
|
const chunks: PngChunk[] = [];
|
||||||
if (uint8arr.length < 12) return chunks;
|
if (uint8arr.length < 12) return chunks;
|
||||||
const view = new DataView(uint8arr.buffer);
|
const view = new DataView(uint8arr.buffer);
|
||||||
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
|
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
|
||||||
|
@ -19,9 +24,14 @@ export async function pngChunks(blob) {
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageInfo = {
|
||||||
|
width?: number,
|
||||||
|
dppx?: number,
|
||||||
|
}
|
||||||
|
|
||||||
// decode a image and try to obtain width and dppx. It will never throw but instead
|
// decode a image and try to obtain width and dppx. It will never throw but instead
|
||||||
// return default values.
|
// return default values.
|
||||||
export async function imageInfo(blob) {
|
export async function imageInfo(blob: Blob): Promise<ImageInfo> {
|
||||||
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens
|
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens
|
||||||
|
|
||||||
if (blob.type === 'image/png') { // only png is supported currently
|
if (blob.type === 'image/png') { // only png is supported currently
|
||||||
|
|
|
@ -2,17 +2,17 @@ import emojis from '../../../assets/emoji.json';
|
||||||
|
|
||||||
const maxMatches = 6;
|
const maxMatches = 6;
|
||||||
|
|
||||||
function sortAndReduce(map) {
|
function sortAndReduce(map: Map<string, number>) {
|
||||||
const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
|
const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
|
||||||
return Array.from(sortedMap.keys()).slice(0, maxMatches);
|
return Array.from(sortedMap.keys()).slice(0, maxMatches);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchEmoji(queryText) {
|
export function matchEmoji(queryText: string): string[] {
|
||||||
const query = queryText.toLowerCase().replaceAll('_', ' ');
|
const query = queryText.toLowerCase().replaceAll('_', ' ');
|
||||||
if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
|
if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
|
||||||
|
|
||||||
// results is a map of weights, lower is better
|
// results is a map of weights, lower is better
|
||||||
const results = new Map();
|
const results = new Map<string, number>();
|
||||||
for (const {aliases} of emojis) {
|
for (const {aliases} of emojis) {
|
||||||
const mainAlias = aliases[0];
|
const mainAlias = aliases[0];
|
||||||
for (const [aliasIndex, alias] of aliases.entries()) {
|
for (const [aliasIndex, alias] of aliases.entries()) {
|
||||||
|
@ -27,7 +27,7 @@ export function matchEmoji(queryText) {
|
||||||
return sortAndReduce(results);
|
return sortAndReduce(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchMention(queryText) {
|
export function matchMention(queryText: string): string[] {
|
||||||
const query = queryText.toLowerCase();
|
const query = queryText.toLowerCase();
|
||||||
|
|
||||||
// results is a map of weights, lower is better
|
// results is a map of weights, lower is better
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc.js';
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
import {getCurrentLocale} from '../utils.ts';
|
import {getCurrentLocale} from '../utils.ts';
|
||||||
|
import type {ConfigType} from 'dayjs';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||||
*
|
*
|
||||||
* @param startConfig The start date. Can take any type that `Date` accepts.
|
* @param startDate The start date. Can take any type that dayjs accepts.
|
||||||
* @param endConfig The end date. Can take any type that `Date` accepts.
|
* @param endDate The end date. Can take any type that dayjs accepts.
|
||||||
*/
|
*/
|
||||||
export function startDaysBetween(startDate, endDate) {
|
export function startDaysBetween(startDate: ConfigType, endDate: ConfigType): number[] {
|
||||||
const start = dayjs.utc(startDate);
|
const start = dayjs.utc(startDate);
|
||||||
const end = dayjs.utc(endDate);
|
const end = dayjs.utc(endDate);
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ export function startDaysBetween(startDate, endDate) {
|
||||||
current = current.add(1, 'day');
|
current = current.add(1, 'day');
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDays = [];
|
const startDays: number[] = [];
|
||||||
while (current.isBefore(end)) {
|
while (current.isBefore(end)) {
|
||||||
startDays.push(current.valueOf());
|
startDays.push(current.valueOf());
|
||||||
current = current.add(1, 'week');
|
current = current.add(1, 'week');
|
||||||
|
@ -30,7 +31,7 @@ export function startDaysBetween(startDate, endDate) {
|
||||||
return startDays;
|
return startDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function firstStartDateAfterDate(inputDate) {
|
export function firstStartDateAfterDate(inputDate: Date): number {
|
||||||
if (!(inputDate instanceof Date)) {
|
if (!(inputDate instanceof Date)) {
|
||||||
throw new Error('Invalid date');
|
throw new Error('Invalid date');
|
||||||
}
|
}
|
||||||
|
@ -41,7 +42,14 @@ export function firstStartDateAfterDate(inputDate) {
|
||||||
return resultDate.valueOf();
|
return resultDate.valueOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillEmptyStartDaysWithZeroes(startDays, data) {
|
type DayData = {
|
||||||
|
week: number,
|
||||||
|
additions: number,
|
||||||
|
deletions: number,
|
||||||
|
commits: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
for (const startDay of startDays) {
|
for (const startDay of startDays) {
|
||||||
|
@ -51,11 +59,11 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) {
|
||||||
return Object.values(result);
|
return Object.values(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateFormat;
|
let dateFormat: Intl.DateTimeFormat;
|
||||||
|
|
||||||
// format a Date object to document's locale, but with 24h format from user's current locale because this
|
// format a Date object to document's locale, but with 24h format from user's current locale because this
|
||||||
// option is a personal preference of the user, not something that the document's locale should dictate.
|
// option is a personal preference of the user, not something that the document's locale should dictate.
|
||||||
export function formatDatetime(date) {
|
export function formatDatetime(date: Date | number): string {
|
||||||
if (!dateFormat) {
|
if (!dateFormat) {
|
||||||
// TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
|
// TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
|
||||||
dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
|
dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
export function pathEscapeSegments(s) {
|
export function pathEscapeSegments(s: string): string {
|
||||||
return s.split('/').map(encodeURIComponent).join('/');
|
return s.split('/').map(encodeURIComponent).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripSlash(url) {
|
function stripSlash(url: string): string {
|
||||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUrl(url) {
|
export function isUrl(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
|
return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
window.__webpack_public_path__ = '';
|
window.__webpack_public_path__ = '';
|
||||||
|
|
||||||
window.config = {
|
window.config = {
|
||||||
|
appUrl: 'http://localhost:3000/',
|
||||||
|
appSubUrl: '',
|
||||||
|
assetVersionEncoded: '',
|
||||||
|
assetUrlPrefix: '',
|
||||||
|
runModeIsProd: true,
|
||||||
|
customEmojis: {},
|
||||||
csrfToken: 'test-csrf-token-123456',
|
csrfToken: 'test-csrf-token-123456',
|
||||||
pageData: {},
|
pageData: {},
|
||||||
i18n: {},
|
notificationSettings: {},
|
||||||
appSubUrl: '',
|
enableTimeTracking: true,
|
||||||
mentionValues: [
|
mentionValues: [
|
||||||
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
|
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
|
||||||
{key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
|
{key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
|
||||||
|
@ -14,4 +20,6 @@ window.config = {
|
||||||
{key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'},
|
{key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'},
|
||||||
{key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'},
|
{key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'},
|
||||||
],
|
],
|
||||||
|
mermaidMaxSourceCharacters: 5000,
|
||||||
|
i18n: {},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue