mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(console): add line number and disable line wrapper (#3546)
--------- Co-authored-by: Charles Zhao <charleszhao@silverhand.io>
This commit is contained in:
parent
80015f22fd
commit
aee1fbfb6d
5 changed files with 98 additions and 20 deletions
packages/console/src
components
pages/SignInExperience/tabs/Branding
|
@ -1,11 +1,12 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
padding: _.unit(6);
|
||||
padding: _.unit(6) _.unit(6) _.unit(6) _.unit(4);
|
||||
border-radius: 16px;
|
||||
// Force dark theme on the code editor
|
||||
background: #34353f;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
|
@ -36,6 +37,7 @@
|
|||
|
||||
.editor {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
|
@ -46,15 +48,15 @@
|
|||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
resize: none;
|
||||
color: #fefefe;
|
||||
overflow: hidden;
|
||||
-webkit-text-fill-color: transparent;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
textarea,
|
||||
|
@ -63,6 +65,10 @@
|
|||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import classNames from 'classnames';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { a11yDark as theme } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { a11yDark as a11yDarkTheme } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
import CopyToClipboard from '../CopyToClipboard';
|
||||
import * as styles from './index.module.scss';
|
||||
import { lineNumberContainerStyle, lineNumberStyle, customStyle } from './utils';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
@ -32,8 +33,19 @@ const CodeEditor = ({
|
|||
placeholder,
|
||||
}: Props) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Update textarea width according to its scroll width
|
||||
const { current } = textareaRef;
|
||||
|
||||
if (current && current.style.width !== `${current.scrollWidth}px`) {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
current.style.width = `${current.scrollWidth}px`;
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { value } = event.currentTarget;
|
||||
onChange?.(value);
|
||||
|
@ -54,6 +66,15 @@ const CodeEditor = ({
|
|||
|
||||
onChange?.(newText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since lineNumber container could cover leftmost part of the editor,
|
||||
* when user is clicking on "Enter", should manually scroll to the leftmost.
|
||||
*/
|
||||
if (event.key === 'Enter' && editorRef.current && editorRef.current.scrollLeft !== 0) {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
editorRef.current.scrollLeft = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO @sijie temp solution for required error (the errorMessage is an empty string)
|
||||
|
@ -65,12 +86,15 @@ const CodeEditor = ({
|
|||
return t('general.required');
|
||||
}, [errorMessage, t]);
|
||||
|
||||
const maxLineNumberDigits = ((value ?? '').split('\n').length + 1).toString().length;
|
||||
const isShowingPlaceholder = !value;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{!value && <div className={styles.placeholder}>{placeholder}</div>}
|
||||
{isShowingPlaceholder && <div className={styles.placeholder}>{placeholder}</div>}
|
||||
<CopyToClipboard value={value ?? ''} variant="icon" className={styles.copy} />
|
||||
<div className={styles.editor}>
|
||||
<div ref={editorRef} className={styles.editor}>
|
||||
{/* SyntaxHighlighter is a readonly component, so a transparent <textarea> layer is needed
|
||||
in order to support user interactions, such as code editing, copy-pasting, etc. */}
|
||||
<textarea
|
||||
|
@ -82,6 +106,14 @@ const CodeEditor = ({
|
|||
readOnly={isReadonly}
|
||||
spellCheck="false"
|
||||
value={value}
|
||||
style={
|
||||
isShowingPlaceholder
|
||||
? { marginLeft: '8px', width: 'calc(100% - 8px)' }
|
||||
: {
|
||||
marginLeft: `calc(${maxLineNumberDigits}ch + 20px)`,
|
||||
width: `calc(100% - ${maxLineNumberDigits}ch - 20px)`,
|
||||
}
|
||||
}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeydown}
|
||||
/>
|
||||
|
@ -89,23 +121,19 @@ const CodeEditor = ({
|
|||
inline-styles by default. Therefore, We can only use inline styles to customize them.
|
||||
Some styles have to be applied multiple times to each of them for the sake of consistency. */}
|
||||
<SyntaxHighlighter
|
||||
wrapLongLines
|
||||
showInlineLineNumbers
|
||||
showLineNumbers={!isShowingPlaceholder}
|
||||
width={textareaRef.current?.scrollWidth ?? 0}
|
||||
lineNumberContainerStyle={lineNumberContainerStyle()}
|
||||
lineNumberStyle={lineNumberStyle(maxLineNumberDigits)}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontFamily: "'Roboto Mono', monospace", // Override default font-family of <code>
|
||||
},
|
||||
}}
|
||||
customStyle={{
|
||||
background: 'transparent',
|
||||
fontSize: '14px',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
borderRadius: '0',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: "'Roboto Mono', monospace", // Override default font-family of <pre>
|
||||
}}
|
||||
customStyle={customStyle(textareaRef.current?.scrollWidth)}
|
||||
language={language}
|
||||
style={theme}
|
||||
style={a11yDarkTheme}
|
||||
>
|
||||
{value ?? ''}
|
||||
</SyntaxHighlighter>
|
||||
|
|
44
packages/console/src/components/CodeEditor/utils.ts
Normal file
44
packages/console/src/components/CodeEditor/utils.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import type { CSSProperties } from 'react';
|
||||
|
||||
export const lineNumberContainerStyle = (): CSSProperties => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'right',
|
||||
paddingLeft: '0px',
|
||||
paddingRight: '0px',
|
||||
};
|
||||
};
|
||||
|
||||
export const lineNumberStyle = (numberOfLines: number): CSSProperties => {
|
||||
return {
|
||||
minWidth: `calc(${numberOfLines}ch + 20px)`,
|
||||
marginLeft: '0px',
|
||||
paddingRight: '20px',
|
||||
paddingLeft: '0px',
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'flex-end',
|
||||
counterIncrement: 'line',
|
||||
lineHeight: '1.5',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'Roboto Mono', monospace",
|
||||
fontSize: '14px',
|
||||
position: 'sticky',
|
||||
background: '#34353f', // Stick to code editor container
|
||||
left: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const customStyle = (width?: number): CSSProperties => {
|
||||
return {
|
||||
width: `${width ?? 0}px`,
|
||||
background: 'transparent',
|
||||
fontSize: '14px',
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
borderRadius: '0',
|
||||
wordBreak: 'break-all',
|
||||
overflow: 'unset',
|
||||
fontFamily: "'Roboto Mono', monospace", // Override default font-family of <pre>
|
||||
};
|
||||
};
|
|
@ -39,4 +39,5 @@
|
|||
|
||||
.form {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.customCssCodeEditor {
|
||||
max-height: calc(100vh - 260px);
|
||||
min-height: 132px; // min-height to show three lines of code
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue