mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Custom terminal fonts support (#5414)
Fixes #5132 #5133 - bundled 7 new monospaced fonts for terminal - fix: refresh terminal font after changing in the preferences / keep cluster iframe accessible in DOM while not active/focused - display terminal's custom font preview with font-name in the select-box + live-preload for current `fontSize`, fix lint * Fixes for <Input/>: - remove duplicated error messages for sync validators - don't propagate invalid values on change (uncontrolled components only) - more informative error message for numeric input with min/max info
This commit is contained in:
parent
fae6bff6fb
commit
55a977554d
@ -408,7 +408,6 @@
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typedoc": "0.22.17",
|
||||
"typedoc-plugin-markdown": "^3.11.12",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.5.5",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"webpack": "^5.72.0",
|
||||
|
||||
@ -771,6 +771,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
|
||||
>
|
||||
<input
|
||||
class="input box grow"
|
||||
max="50"
|
||||
min="10"
|
||||
spellcheck="false"
|
||||
type="number"
|
||||
@ -790,22 +791,78 @@ exports[`preferences - navigation to terminal preferences given in preferences,
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="Input theme round black"
|
||||
class="Select theme-lens css-b62m3t-container"
|
||||
>
|
||||
<label
|
||||
class="input-area flex gaps align-center"
|
||||
id=""
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
/>
|
||||
<div
|
||||
class="Select__control css-1s2u09g-control"
|
||||
>
|
||||
<div
|
||||
class="Select__value-container css-319lph-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="Select__placeholder css-14el2xx-placeholder"
|
||||
id="react-select-2-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="Select__input-container css-6j8wv5-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
class="input box grow"
|
||||
aria-autocomplete="list"
|
||||
aria-describedby="react-select-2-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="Select__input"
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="input-info flex gaps"
|
||||
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<span
|
||||
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
.notes {
|
||||
white-space: pre-line;
|
||||
font-family: "RobotoMono", monospace;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
|
||||
@ -4,18 +4,20 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { Input } from "../input";
|
||||
import { Switch } from "../switch";
|
||||
import { Select } from "../select";
|
||||
import { Select, type SelectOption } from "../select";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import { Preferences } from "./preferences";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import defaultShellInjectable from "./default-shell.injectable";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Dependencies {
|
||||
userStore: UserStore;
|
||||
@ -23,11 +25,12 @@ interface Dependencies {
|
||||
defaultShell: string;
|
||||
}
|
||||
|
||||
const NonInjectedTerminal = observer(({
|
||||
const NonInjectedTerminal = observer((
|
||||
{
|
||||
userStore,
|
||||
themeStore,
|
||||
defaultShell,
|
||||
}: Dependencies) => {
|
||||
}: Dependencies) => {
|
||||
const themeOptions = [
|
||||
{
|
||||
value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
|
||||
@ -39,6 +42,26 @@ const NonInjectedTerminal = observer(({
|
||||
})),
|
||||
];
|
||||
|
||||
// fonts must be declared in `fonts.scss` and at `template.html` (if early-preloading required)
|
||||
const supportedCustomFonts: SelectOption<string>[] = [
|
||||
"RobotoMono", "Anonymous Pro", "IBM Plex Mono", "JetBrains Mono", "Red Hat Mono",
|
||||
"Source Code Pro", "Space Mono", "Ubuntu Mono",
|
||||
].map(customFont => {
|
||||
const { fontFamily, fontSize } = userStore.terminalConfig;
|
||||
|
||||
return {
|
||||
label: <span style={{ fontFamily: customFont, fontSize }}>{customFont}</span>,
|
||||
value: customFont,
|
||||
isSelected: fontFamily === customFont,
|
||||
};
|
||||
});
|
||||
|
||||
const onFontFamilyChange = action(({ value: fontFamily }: SelectOption<string>) => {
|
||||
logger.info(`setting terminal font to ${fontFamily}`);
|
||||
|
||||
userStore.terminalConfig.fontFamily = fontFamily; // save to external storage
|
||||
});
|
||||
|
||||
return (
|
||||
<Preferences data-testid="terminal-preferences-page">
|
||||
<section>
|
||||
@ -49,7 +72,7 @@ const NonInjectedTerminal = observer(({
|
||||
<Input
|
||||
theme="round-black"
|
||||
placeholder={defaultShell}
|
||||
value={userStore.shell}
|
||||
value={userStore.shell ?? ""}
|
||||
onChange={(value) => userStore.shell = value}
|
||||
/>
|
||||
</section>
|
||||
@ -81,18 +104,19 @@ const NonInjectedTerminal = observer(({
|
||||
theme="round-black"
|
||||
type="number"
|
||||
min={10}
|
||||
validators={InputValidators.isNumber}
|
||||
value={userStore.terminalConfig.fontSize.toString()}
|
||||
max={50}
|
||||
defaultValue={userStore.terminalConfig.fontSize.toString()}
|
||||
onChange={(value) => userStore.terminalConfig.fontSize = Number(value)}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<SubTitle title="Font family" />
|
||||
<Input
|
||||
theme="round-black"
|
||||
type="text"
|
||||
<Select
|
||||
themeName="lens"
|
||||
controlShouldRenderValue
|
||||
value={userStore.terminalConfig.fontFamily}
|
||||
onChange={(value) => userStore.terminalConfig.fontFamily = value}
|
||||
options={supportedCustomFonts}
|
||||
onChange={onFontFamilyChange as any}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -12,12 +12,14 @@
|
||||
@import "./fonts";
|
||||
|
||||
:root {
|
||||
--flex-gap: #{$padding};
|
||||
--unit: 8px;
|
||||
--padding: var(--unit);
|
||||
--margin: var(--unit);
|
||||
--border-radius: 3px;
|
||||
--font-main: 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
--font-monospace: Lucida Console, Monaco, Consolas, monospace;
|
||||
--font-monospace: Lucida Console, Monaco, Consolas, monospace; // some defaults
|
||||
--font-terminal: var(--font-monospace); // overridden in terminal.ts, managed by common/user-store.ts
|
||||
--font-size-small: calc(1.5 * var(--unit));
|
||||
--font-size: calc(1.75 * var(--unit));
|
||||
--font-size-big: calc(2 * var(--unit));
|
||||
@ -63,16 +65,13 @@
|
||||
color: var(--textColorAccent);
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%; // 1 rem == 10px
|
||||
color: var(--textColorPrimary);
|
||||
background-color: var(--mainBackground);
|
||||
--flex-gap: #{$padding};
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--textColorPrimary);
|
||||
background-color: var(--mainBackground);
|
||||
font-size: var(--font-size);
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
#terminal-init {
|
||||
@ -93,10 +92,6 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font: $font-size $font-main;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
@ -227,6 +222,22 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
#fonts-preloading {
|
||||
> span {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
|
||||
&:before {
|
||||
width: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
content: "text-example"; // some text required to start applying/rendering font in document
|
||||
font-family: inherit; // font-family must be specified via style="" (see: template.html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// app's common loading indicator, displaying on the route transitions
|
||||
#loading {
|
||||
position: absolute;
|
||||
|
||||
@ -45,7 +45,6 @@ export class ClusterFrameHandler {
|
||||
|
||||
iframe.id = `cluster-frame-${cluster.id}`;
|
||||
iframe.name = cluster.contextName;
|
||||
iframe.style.display = "none";
|
||||
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
||||
iframe.addEventListener("load", action(() => {
|
||||
logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`);
|
||||
@ -95,7 +94,7 @@ export class ClusterFrameHandler {
|
||||
ipcRenderer.send(clusterVisibilityHandler);
|
||||
|
||||
for (const { frame: view } of this.views.values()) {
|
||||
view.style.display = "none";
|
||||
view.classList.add("hidden");
|
||||
}
|
||||
|
||||
const cluster = clusterId
|
||||
@ -113,9 +112,9 @@ export class ClusterFrameHandler {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
(view) => {
|
||||
(view: LensView) => {
|
||||
logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`);
|
||||
view.frame.style.display = "flex";
|
||||
view.frame.classList.remove("hidden");
|
||||
ipcRenderer.send(clusterVisibilityHandler, clusterId);
|
||||
},
|
||||
);
|
||||
|
||||
@ -35,7 +35,20 @@
|
||||
background-color: var(--mainBackground);
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
// when updating font settings in the "Preferences -> Terminal" cluster's iframe
|
||||
// must be accessible in DOM (e.g. elem.getBoundingClientRect() must work)
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,16 +64,30 @@ export class Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: TerminalDependencies, { tabId, api }: TerminalArguments) {
|
||||
get fontFamily() {
|
||||
return this.dependencies.terminalConfig.get().fontFamily;
|
||||
}
|
||||
|
||||
get fontSize() {
|
||||
return this.dependencies.terminalConfig.get().fontSize;
|
||||
}
|
||||
|
||||
get theme(): Record<string/*paramName*/, string/*color*/> {
|
||||
return this.dependencies.themeStore.xtermColors;
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: TerminalDependencies, {
|
||||
tabId,
|
||||
api,
|
||||
}: TerminalArguments) {
|
||||
this.tabId = tabId;
|
||||
this.api = api;
|
||||
const { fontSize, fontFamily } = this.dependencies.terminalConfig.get();
|
||||
|
||||
this.xterm = new XTerm({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "bar",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
fontSize: this.fontSize,
|
||||
fontFamily: this.fontFamily,
|
||||
});
|
||||
// enable terminal addons
|
||||
this.xterm.loadAddon(this.fitAddon);
|
||||
@ -95,17 +109,11 @@ export class Terminal {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
this.disposer.push(
|
||||
reaction(() => this.dependencies.themeStore.xtermColors, colors => {
|
||||
this.xterm?.setOption("theme", colors);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.dependencies.terminalConfig.get().fontSize, this.setFontSize, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.dependencies.terminalConfig.get().fontFamily, this.setFontFamily, {
|
||||
reaction(() => this.theme, colors => this.xterm.setOption("theme", colors), {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.fontSize, this.setFontSize, { fireImmediately: true }),
|
||||
reaction(() => this.fontFamily, this.setFontFamily, { fireImmediately: true }),
|
||||
() => onDataHandler.dispose(),
|
||||
() => this.fitAddon.dispose(),
|
||||
() => this.api.removeAllListeners(),
|
||||
@ -120,15 +128,14 @@ export class Terminal {
|
||||
}
|
||||
|
||||
fit = () => {
|
||||
// Since this function is debounced we need to read this value as late as possible
|
||||
if (!this.xterm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.fitAddon.fit();
|
||||
const { cols, rows } = this.xterm;
|
||||
const { cols, rows } = this.fitAddon.proposeDimensions();
|
||||
|
||||
// attempt to resize/fit terminal when it's not visible in DOM will crash with exception
|
||||
// see: https://github.com/xtermjs/xterm.js/issues/3118
|
||||
if (isNaN(cols) || isNaN(rows)) return;
|
||||
|
||||
this.fitAddon.fit();
|
||||
this.api.sendTerminalSize(cols, rows);
|
||||
} catch (error) {
|
||||
// see https://github.com/lensapp/lens/issues/1891
|
||||
@ -197,12 +204,21 @@ export class Terminal {
|
||||
}
|
||||
};
|
||||
|
||||
setFontSize = (size: number) => {
|
||||
this.xterm.options.fontSize = size;
|
||||
setFontSize = (fontSize: number) => {
|
||||
logger.info(`[TERMINAL]: set fontSize to ${fontSize}`);
|
||||
|
||||
this.xterm.options.fontSize = fontSize;
|
||||
this.fit();
|
||||
};
|
||||
|
||||
setFontFamily = (family: string) => {
|
||||
this.xterm.options.fontFamily = family;
|
||||
setFontFamily = (fontFamily: string) => {
|
||||
logger.info(`[TERMINAL]: set fontFamily to ${fontFamily}`);
|
||||
|
||||
this.xterm.options.fontFamily = fontFamily;
|
||||
this.fit();
|
||||
|
||||
// provide css-variable within `:root {}`
|
||||
document.documentElement.style.setProperty("--font-terminal", fontFamily);
|
||||
};
|
||||
|
||||
keyHandler = (evt: KeyboardEvent): boolean => {
|
||||
|
||||
@ -3,34 +3,99 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Custom fonts
|
||||
@import "~typeface-roboto/index.css";
|
||||
// App's main font
|
||||
// Downloaded from: https://fonts.google.com/specimen/Roboto
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("../fonts/Roboto-Light.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-weight: 300; // "light"
|
||||
}
|
||||
|
||||
// Material Design Icons, used primarily in icon.tsx
|
||||
// Latest: https://github.com/google/material-design-icons/tree/master/font
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("../fonts/Roboto-LightItalic.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-weight: 300; // "light" + italic
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("../fonts/Roboto-Regular.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-weight: 400; // "normal"
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("../fonts/Roboto-Bold.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-weight: 700; // "bold"
|
||||
}
|
||||
|
||||
|
||||
// Icon fonts, see: `icon.tsx`
|
||||
// Latest version for manual update: https://github.com/google/material-design-icons/tree/master/font
|
||||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("./fonts/MaterialIcons-Regular.ttf") format("truetype");
|
||||
src: url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
|
||||
// Terminal fonts (monospaced)
|
||||
// Source: https://fonts.google.com/?category=Monospace
|
||||
@font-face {
|
||||
font-family: "Anonymous Pro";
|
||||
src: local("Anonymous Pro"), url("../fonts/AnonymousPro-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: local("IBM Plex Mono"), url("../fonts/IBMPlexMono-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: local("JetBrains Mono"), url("../fonts/JetBrainsMono-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Red Hat Mono";
|
||||
src: local("Red Hat Mono"), url("../fonts/RedHatMono-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: local("Source Code Pro"), url("../fonts/SourceCodePro-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Space Mono";
|
||||
src: local("Space Mono"), url("../fonts/SpaceMono-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Ubuntu Mono";
|
||||
src: local("Ubuntu Mono"), url("../fonts/UbuntuMono-Regular.ttf") format("truetype");
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
// Patched RobotoMono font with icons
|
||||
// RobotoMono Windows Compatible for using in terminal
|
||||
// https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono
|
||||
@font-face {
|
||||
font-family: 'RobotoMono';
|
||||
font-family: "RobotoMono";
|
||||
src: local("RobotoMono"), url("../fonts/Roboto-Mono-nerd.ttf") format("truetype");
|
||||
font-display: block;
|
||||
src: local('RobotoMono'), url('./fonts/roboto-mono-nerd.ttf') format('truetype');
|
||||
}
|
||||
|
||||
#fonts-preloading {
|
||||
> .icons {
|
||||
@include font-preload("Material Icons");
|
||||
}
|
||||
|
||||
> .terminal {
|
||||
@include font-preload("RobotoMono");
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -14,7 +14,6 @@ import { Tooltip } from "../tooltip";
|
||||
import * as Validators from "./input_validators";
|
||||
import type { InputValidator } from "./input_validators";
|
||||
import isFunction from "lodash/isFunction";
|
||||
import isBoolean from "lodash/isBoolean";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
@ -24,7 +23,10 @@ export { InputValidators };
|
||||
export type { InputValidator };
|
||||
|
||||
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
type InputElementProps = InputHTMLAttributes<HTMLInputElement> & TextareaHTMLAttributes<HTMLTextAreaElement> & DOMAttributes<InputElement>;
|
||||
type InputElementProps =
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
& TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
& DOMAttributes<InputElement>;
|
||||
|
||||
export interface IconDataFnArg {
|
||||
isDirty: boolean;
|
||||
@ -173,22 +175,18 @@ export class Input extends React.Component<InputProps, State> {
|
||||
error => this.getValidatorError(value, validator) || error,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!validator.validate(value, this.props)) {
|
||||
errors.push(this.getValidatorError(value, validator));
|
||||
}
|
||||
}
|
||||
|
||||
const result = validator.validate(value, this.props);
|
||||
const isValid = validator.validate(value, this.props);
|
||||
|
||||
if (isBoolean(result) && !result) {
|
||||
if (isValid === false) {
|
||||
errors.push(this.getValidatorError(value, validator));
|
||||
} else if (result instanceof Promise) {
|
||||
} else if (isValid instanceof Promise) {
|
||||
if (!validationId) {
|
||||
this.validationId = validationId = uniqueId("validation_id_");
|
||||
}
|
||||
asyncValidators.push(
|
||||
result.then(
|
||||
isValid.then(
|
||||
() => null, // don't consider any valid result from promise since we interested in errors only
|
||||
error => this.getValidatorError(value, validator) || error,
|
||||
),
|
||||
@ -266,17 +264,25 @@ export class Input extends React.Component<InputProps, State> {
|
||||
|
||||
setDirtyOnChange = debounce(() => this.setDirty(), 500);
|
||||
|
||||
onChange(evt: React.ChangeEvent<any>) {
|
||||
this.props.onChange?.(evt.currentTarget.value, evt);
|
||||
this.validate();
|
||||
async onChange(evt: React.ChangeEvent<any>) {
|
||||
const newValue = evt.currentTarget.value;
|
||||
const eventCopy = { ...evt };
|
||||
|
||||
this.autoFitHeight();
|
||||
this.setDirtyOnChange();
|
||||
|
||||
// re-render component when used as uncontrolled input
|
||||
// when used @defaultValue instead of @value changing real input.value doesn't call render()
|
||||
if (this.isUncontrolled && this.showMaxLenIndicator) {
|
||||
this.forceUpdate();
|
||||
// Handle uncontrolled components (`props.defaultValue` must be used instead `value`)
|
||||
if (this.isUncontrolled) {
|
||||
// update DOM since render() is not called on input's changes with uncontrolled inputs
|
||||
if (this.showMaxLenIndicator) this.forceUpdate();
|
||||
|
||||
// don't propagate changes for invalid values
|
||||
await this.validate();
|
||||
if (!this.state.valid) return; // skip
|
||||
}
|
||||
|
||||
// emit new value update
|
||||
this.props.onChange?.(newValue, eventCopy);
|
||||
}
|
||||
|
||||
onKeyDown(evt: React.KeyboardEvent<InputElement>) {
|
||||
@ -299,7 +305,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
this.setDirty();
|
||||
}
|
||||
|
||||
if(this.props.blurOnEnter){
|
||||
if (this.props.blurOnEnter) {
|
||||
//pressing enter indicates that the edit is complete, we can unfocus now
|
||||
this.blur();
|
||||
}
|
||||
@ -379,7 +385,6 @@ export class Input extends React.Component<InputProps, State> {
|
||||
multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip,
|
||||
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
|
||||
dirty: _dirty, // excluded from passing to input-element
|
||||
defaultValue,
|
||||
trim,
|
||||
blurOnEnter,
|
||||
...inputProps
|
||||
|
||||
@ -8,7 +8,8 @@ import type { ReactNode } from "react";
|
||||
import fse from "fs-extra";
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
|
||||
export class AsyncInputValidationError extends Error {}
|
||||
export class AsyncInputValidationError extends Error {
|
||||
}
|
||||
|
||||
export type InputValidator<IsAsync extends boolean> = {
|
||||
/**
|
||||
@ -32,7 +33,7 @@ export type InputValidator<IsAsync extends boolean> = {
|
||||
message?: undefined;
|
||||
debounce: number;
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
export function inputValidator<IsAsync extends boolean = false>(validator: InputValidator<IsAsync>): InputValidator<IsAsync> {
|
||||
return validator;
|
||||
@ -52,7 +53,14 @@ export const isEmail = inputValidator({
|
||||
|
||||
export const isNumber = inputValidator({
|
||||
condition: ({ type }) => type === "number",
|
||||
message: () => `Invalid number`,
|
||||
message(value, { min, max }) {
|
||||
const minMax: string = [
|
||||
typeof min === "number" ? `min: ${min}` : undefined,
|
||||
typeof max === "number" ? `max: ${max}` : undefined,
|
||||
].filter(Boolean).join(", ");
|
||||
|
||||
return `Invalid number${minMax ? ` (${minMax})` : ""}`;
|
||||
},
|
||||
validate: (value, { min, max }) => {
|
||||
const numVal = +value;
|
||||
|
||||
|
||||
@ -59,20 +59,3 @@
|
||||
@content; // css-modules (*.module.scss)
|
||||
}
|
||||
}
|
||||
|
||||
// Makes custom @font-family available at earlier stages.
|
||||
// Element must exist in DOM as soon as possible to initiate preloading.
|
||||
@mixin font-preload($fontFamily) {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
|
||||
&:before {
|
||||
width: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
content: "x"; // some text required to start applying font in document
|
||||
font-family: $fontFamily; // imported name in @font-face declaration
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ const { Menu } = components;
|
||||
|
||||
export interface SelectOption<Value> {
|
||||
value: Value;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
BIN
src/renderer/fonts/AnonymousPro-Regular.ttf
Normal file
BIN
src/renderer/fonts/AnonymousPro-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/IBMPlexMono-Regular.ttf
Normal file
BIN
src/renderer/fonts/IBMPlexMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
src/renderer/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/RedHatMono-Regular.ttf
Normal file
BIN
src/renderer/fonts/RedHatMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/Roboto-Bold.ttf
Normal file
BIN
src/renderer/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/Roboto-Light.ttf
Normal file
BIN
src/renderer/fonts/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/Roboto-LightItalic.ttf
Normal file
BIN
src/renderer/fonts/Roboto-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/Roboto-Regular.ttf
Normal file
BIN
src/renderer/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/SourceCodePro-Regular.ttf
Normal file
BIN
src/renderer/fonts/SourceCodePro-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/SpaceMono-Regular.ttf
Normal file
BIN
src/renderer/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/fonts/UbuntuMono-Regular.ttf
Normal file
BIN
src/renderer/fonts/UbuntuMono-Regular.ttf
Normal file
Binary file not shown.
@ -1,17 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app"></div>
|
||||
<div id="terminal-init"></div>
|
||||
|
||||
<div id="fonts-preloading">
|
||||
<i class="icons"><!--material icons--></i>
|
||||
<i class="terminal"><!--roboto mono--></i>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="fonts-preloading">
|
||||
<span style="font-family: 'Material Icons'" />
|
||||
<span style="font-family: 'RobotoMono'" />
|
||||
<span style="font-family: 'Anonymous Pro'" />
|
||||
<span style="font-family: 'IBM Plex Mono'" />
|
||||
<span style="font-family: 'JetBrains Mono'" />
|
||||
<span style="font-family: 'Red Hat Mono'" />
|
||||
<span style="font-family: 'Source Code Pro'" />
|
||||
<span style="font-family: 'Space Mono'" />
|
||||
<span style="font-family: 'Ubuntu Mono'" />
|
||||
</div>
|
||||
<div id="terminal-init"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -13012,11 +13012,6 @@ typedoc@0.22.17:
|
||||
minimatch "^5.1.0"
|
||||
shiki "^0.10.1"
|
||||
|
||||
typeface-roboto@^1.1.13:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5"
|
||||
integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==
|
||||
|
||||
typescript-plugin-css-modules@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-plugin-css-modules/-/typescript-plugin-css-modules-3.4.0.tgz#4ff6905d88028684d1608c05c62cb6346e5548cc"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user