mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-04 18:59:08 +01:00
save work
This commit is contained in:
parent
57bfa6b8bd
commit
4dd4acfa8c
@ -251,7 +251,6 @@ const EmojiPalette = memo(({ scopeRef, className }: EmojiPaletteProps) => {
|
||||
{isPaletteVisible && (
|
||||
<Palette anchorRef={anchorRef} scopeRef={scopeRef} className="emoji-palette-content">
|
||||
<Input placeholder="Search emojis..." value={searchTerm} onChange={handleSearchChange} />
|
||||
|
||||
<div className="emoji-grid">
|
||||
{filteredEmojis.length > 0 ? (
|
||||
filteredEmojis.map((item, index) => (
|
||||
|
96
frontend/app/element/inputmultiline.less
Normal file
96
frontend/app/element/inputmultiline.less
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
min-width: 50px;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
border: 2px solid var(--form-element-border-color);
|
||||
background: var(--form-element-bg-color);
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: var(--form-element-primary-color);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--form-element-error-color);
|
||||
}
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
--inner-padding: 5px 0 5px 16px;
|
||||
|
||||
&-label {
|
||||
padding: var(--inner-padding);
|
||||
margin-bottom: -10px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: var(--form-element-label-color);
|
||||
line-height: 10px;
|
||||
user-select: none;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.offset-left {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
padding: var(--inner-padding);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: var(--form-element-text-color);
|
||||
line-height: 20px;
|
||||
resize: none; // Disable manual resizing by the user
|
||||
|
||||
&.offset-left {
|
||||
padding: 5px 16px 5px 0;
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-label {
|
||||
height: auto;
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-Line Input Customization for textarea
|
||||
textarea.input-inner-input {
|
||||
overflow: hidden; // Hide native scrollbars, and let height expand automatically
|
||||
min-height: 32px; // Minimum height of textarea
|
||||
max-height: 150px; // Set a maximum height before scrolling is triggered
|
||||
line-height: 1.5em; // Control line height for better readability
|
||||
}
|
52
frontend/app/element/inputmultiline.stories.tsx
Normal file
52
frontend/app/element/inputmultiline.stories.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { InputMultiLine } from "./intputmultiline";
|
||||
|
||||
const meta: Meta<typeof InputMultiLine> = {
|
||||
title: "Elements/InputMultiLine",
|
||||
component: InputMultiLine,
|
||||
args: {
|
||||
label: "Message",
|
||||
placeholder: "Type your message here...",
|
||||
className: "custom-input-class",
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
description: "Label for the input field",
|
||||
},
|
||||
placeholder: {
|
||||
description: "Placeholder text for the input",
|
||||
},
|
||||
className: {
|
||||
description: "Custom class for input styling",
|
||||
},
|
||||
decoration: {
|
||||
description: "Input decorations for start or end positions",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InputMultiLine>;
|
||||
|
||||
export const DefaultInput: Story = {
|
||||
render: (args) => {
|
||||
return <InputMultiLine {...args} />;
|
||||
},
|
||||
args: {
|
||||
label: "Message",
|
||||
placeholder: "Type your message...",
|
||||
},
|
||||
};
|
||||
|
||||
export const InputWithErrorState: Story = {
|
||||
render: (args) => {
|
||||
return <InputMultiLine {...args} required={true} />;
|
||||
},
|
||||
args: {
|
||||
label: "Required Message",
|
||||
placeholder: "This field is required...",
|
||||
},
|
||||
};
|
184
frontend/app/element/intputmultiline.tsx
Normal file
184
frontend/app/element/intputmultiline.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import { clsx } from "clsx";
|
||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
||||
|
||||
import "./inputmultiline.less";
|
||||
|
||||
interface InputDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface InputMultiLineProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
decoration?: InputDecorationProps;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
inputRef?: React.MutableRefObject<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
const InputMultiLine = forwardRef<HTMLDivElement, InputMultiLineProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
placeholder,
|
||||
defaultValue = "",
|
||||
decoration,
|
||||
required,
|
||||
maxLength,
|
||||
autoFocus,
|
||||
disabled,
|
||||
inputRef,
|
||||
}: InputMultiLineProps,
|
||||
ref
|
||||
) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||
const [error, setError] = useState(false);
|
||||
const [hasContent, setHasContent] = useState(Boolean(value || defaultValue));
|
||||
const internalInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined) {
|
||||
setFocused(Boolean(value));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleComponentFocus = () => {
|
||||
if (internalInputRef.current && !internalInputRef.current.contains(document.activeElement)) {
|
||||
internalInputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleComponentBlur = () => {
|
||||
if (internalInputRef.current?.contains(document.activeElement)) {
|
||||
internalInputRef.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetInputRef = (elem: HTMLTextAreaElement) => {
|
||||
if (inputRef) {
|
||||
inputRef.current = elem;
|
||||
}
|
||||
internalInputRef.current = elem;
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setFocused(true);
|
||||
onFocus && onFocus();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (internalInputRef.current) {
|
||||
const inputValue = internalInputRef.current.value;
|
||||
if (required && !inputValue) {
|
||||
setError(true);
|
||||
setFocused(false);
|
||||
} else {
|
||||
setError(false);
|
||||
setFocused(false);
|
||||
}
|
||||
}
|
||||
onBlur && onBlur();
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
if (required && !inputValue) {
|
||||
setError(true);
|
||||
setHasContent(false);
|
||||
} else {
|
||||
setError(false);
|
||||
setHasContent(Boolean(inputValue));
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
setInternalValue(inputValue);
|
||||
}
|
||||
|
||||
onChange && onChange(inputValue);
|
||||
handleAutoResize(e.target);
|
||||
};
|
||||
|
||||
const handleAutoResize = (textarea: HTMLTextAreaElement) => {
|
||||
textarea.style.height = "auto"; // Reset height to calculate new height
|
||||
textarea.style.height = `${textarea.scrollHeight}px`; // Set new height based on content
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (internalInputRef.current) {
|
||||
handleAutoResize(internalInputRef.current);
|
||||
}
|
||||
}, [internalValue]);
|
||||
|
||||
const inputValue = value ?? internalValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx("input", className, {
|
||||
focused: focused,
|
||||
error: error,
|
||||
disabled: disabled,
|
||||
"no-label": !label,
|
||||
})}
|
||||
onFocus={handleComponentFocus}
|
||||
onBlur={handleComponentBlur}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="input-inner">
|
||||
{label && (
|
||||
<label
|
||||
className={clsx("input-inner-label", {
|
||||
float: hasContent || focused || placeholder,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
className={clsx("input-inner-input", {
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
ref={handleSetInputRef}
|
||||
id={label}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
onInput={(e) => handleAutoResize(e.currentTarget)}
|
||||
/>
|
||||
</div>
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { InputMultiLine };
|
||||
export type { InputDecorationProps, InputMultiLineProps };
|
Loading…
Reference in New Issue
Block a user