search box overhaul

This commit is contained in:
Labhansh Agrawal 2022-12-31 13:47:19 +05:30
parent 502cdbb3b2
commit 9ab2ba9f08
7 changed files with 351 additions and 93 deletions

View file

@ -1,29 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="left-arrow" viewBox="0 0 10 10">
<title>left arrow</title>
<g stroke-linecap="round">
<line x1="0.5" y1="5" x2="8.5" y2="5" stroke="#000" />
<line x1="0.5" y1="5" x2="3.5" y2="2" stroke="#000" />
<line x1="0.5" y1="5" x2="3.5" y2="8" stroke="#000" />
</g>
</symbol>
<symbol id="right-arrow" viewBox="0 0 10 10">
<title>right arrow</title>
<g stroke-linecap="round">
<line x1="1.5" y1="5" x2="9.5" y2="5" stroke="#000" />
<line x1="9.5" y1="5" x2="6.5" y2="2" stroke="#000" />
<line x1="9.5" y1="5" x2="6.5" y2="8" stroke="#000" />
</g>
</symbol>
<symbol id="cancel" viewBox="0 0 10 10">
<title>cancel</title>
<g stroke-linecap="round">
<line x1="5" y1="5" x2="8" y2="8" stroke="#000" />
<line x1="5" y1="5" x2="8" y2="2" stroke="#000" />
<line x1="5" y1="5" x2="2" y2="2" stroke="#000" />
<line x1="5" y1="5" x2="2" y2="8" stroke="#000" />
</g>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,76 +1,254 @@
import React from 'react';
import React, {useCallback} from 'react';
import {SearchBoxProps} from '../hyper';
import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp';
import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown';
import {VscClose} from '@react-icons/all-files/vsc/VscClose';
import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive';
import {VscRegex} from '@react-icons/all-files/vsc/VscRegex';
import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord';
import clsx from 'clsx';
const searchBoxStyling: React.CSSProperties = {
float: 'right',
height: '28px',
backgroundColor: 'white',
position: 'absolute',
right: '10px',
top: '0px',
width: '224px',
zIndex: 9999
type SearchButtonColors = {
foregroundColor: string;
selectionColor: string;
backgroundColor: string;
};
const enterKey = 13;
type SearchButtonProps = React.PropsWithChildren<
{
onClick: () => void;
active: boolean;
title: string;
} & SearchButtonColors
>;
export default class SearchBox extends React.PureComponent<SearchBoxProps> {
const SearchButton = ({
onClick,
active,
title,
foregroundColor,
backgroundColor,
selectionColor,
children
}: SearchButtonProps) => {
const handleKeyUp = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
onClick();
}
},
[onClick]
);
return (
<div
onClick={onClick}
className={clsx('search-button', {'search-button-active': active})}
tabIndex={0}
onKeyUp={handleKeyUp}
title={title}
>
{children}
<style jsx>
{`
.search-button {
cursor: pointer;
color: ${foregroundColor};
padding: 2px;
margin: 4px 0px;
height: 18px;
width: 18px;
border-radius: 2px;
}
.search-button:focus {
outline: ${selectionColor} solid 2px;
}
.search-button:hover {
background-color: ${backgroundColor};
}
.search-button-active {
background-color: ${selectionColor};
}
.search-button-active:hover {
background-color: ${selectionColor};
}
`}
</style>
</div>
);
};
class SearchBox extends React.PureComponent<SearchBoxProps> {
searchTerm: string;
input: HTMLInputElement | null = null;
searchButtonColors: SearchButtonColors;
constructor(props: SearchBoxProps) {
super(props);
this.searchTerm = '';
this.searchButtonColors = {
backgroundColor: this.props.borderColor,
selectionColor: this.props.selectionColor,
foregroundColor: this.props.foregroundColor
};
}
handleChange = (event: React.KeyboardEvent<HTMLInputElement>) => {
this.searchTerm = event.currentTarget.value;
if (event.keyCode === enterKey) {
this.props.search(event.currentTarget.value);
if (event.shiftKey && event.key === 'Enter') {
this.props.prev(this.searchTerm);
} else if (event.key === 'Enter') {
this.props.next(this.searchTerm);
}
};
componentDidMount(): void {
this.input?.focus();
}
render() {
const {
caseSensitive,
wholeWord,
regex,
results,
toggleCaseSensitive,
toggleWholeWord,
toggleRegex,
next,
prev,
close,
backgroundColor,
foregroundColor,
borderColor,
selectionColor,
font
} = this.props;
return (
<div style={searchBoxStyling}>
<input type="text" className="search-box" onKeyUp={this.handleChange} ref={(input) => input?.focus()} />
<svg className="search-button" onClick={() => this.props.prev(this.searchTerm)}>
<use xlinkHref="./renderer/assets/search-icons.svg#left-arrow" />
</svg>
<svg className="search-button" onClick={() => this.props.next(this.searchTerm)}>
<use xlinkHref="./renderer/assets/search-icons.svg#right-arrow" />
</svg>
<svg className="search-button" onClick={() => this.props.close()}>
<use xlinkHref="./renderer/assets/search-icons.svg#cancel" />
</svg>
<div className="flex-row search-container">
<div className="flex-row search-box">
<input
className="search-input"
type="text"
onKeyDown={this.handleChange}
ref={(input) => {
this.input = input;
}}
placeholder="Search"
></input>
<SearchButton
onClick={toggleCaseSensitive}
active={caseSensitive}
title="Match Case"
{...this.searchButtonColors}
>
<VscCaseSensitive size="14px" />
</SearchButton>
<SearchButton
onClick={toggleWholeWord}
active={wholeWord}
title="Match Whole Word"
{...this.searchButtonColors}
>
<VscWholeWord size="14px" />
</SearchButton>
<SearchButton
onClick={toggleRegex}
active={regex}
title="Use Regular Expression"
{...this.searchButtonColors}
>
<VscRegex size="14px" />
</SearchButton>
</div>
<span style={{minWidth: '60px', marginLeft: '4px'}}>
{results === undefined
? ''
: results.resultCount === 0
? 'No results'
: `${results.resultIndex + 1} of ${results.resultCount}`}
</span>
<div className="flex-row">
<SearchButton
onClick={() => prev(this.searchTerm)}
active={false}
title="Previous Match"
{...this.searchButtonColors}
>
<VscArrowUp size="14px" />
</SearchButton>
<SearchButton
onClick={() => next(this.searchTerm)}
active={false}
title="Next Match"
{...this.searchButtonColors}
>
<VscArrowDown size="14px" />
</SearchButton>
<SearchButton onClick={() => close()} active={false} title="Close" {...this.searchButtonColors}>
<VscClose size="14px" />
</SearchButton>
</div>
<style jsx>
{`
.search-box {
font-size: 18px;
padding: 3px 6px;
width: 152px;
border: none;
float: left;
.search-container {
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
border-radius: 2px;
position: absolute;
right: 13px;
top: 4px;
z-index: 10;
padding: 4px;
font-family: ${font};
font-size: 12px;
}
.search-box:focus {
.search-input {
outline: none;
background-color: transparent;
border: none;
color: ${foregroundColor};
align-self: stretch;
width: 100px;
}
.search-button {
background-color: #ffffff;
color: black;
padding: 7px 5.5px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
cursor: pointer;
height: 27px;
width: 24px;
float: left;
.flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 4px;
}
.search-button:hover {
background-color: #e7e7e7;
.search-box {
border: none;
border-radius: 2px;
outline: ${borderColor} solid 1px;
background-color: ${backgroundColor};
color: ${foregroundColor};
padding: 0px 4px;
}
.search-input::placeholder {
color: ${foregroundColor};
}
.search-box:focus-within {
outline: ${selectionColor} solid 2px;
}
`}
</style>
@ -78,3 +256,5 @@ export default class SearchBox extends React.PureComponent<SearchBoxProps> {
);
}
}
export default SearchBox;

View file

@ -137,6 +137,15 @@ export default class StyleSheet extends React.PureComponent<StyleSheetProps> {
overflow: hidden;
}
.xterm .xterm-decoration-overview-ruler {
position: absolute;
z-index: 10;
right: 0px;
top: unset;
left: unset;
pointer-events: none;
}
::-webkit-scrollbar {
width: 5px;
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import {Terminal, ITerminalOptions, IDisposable} from 'xterm';
import {FitAddon} from 'xterm-addon-fit';
import {WebLinksAddon} from 'xterm-addon-web-links';
import {SearchAddon} from 'xterm-addon-search';
import {SearchAddon, ISearchDecorationOptions} from 'xterm-addon-search';
import {WebglAddon} from 'xterm-addon-webgl';
import {LigaturesAddon} from 'xterm-addon-ligatures';
import {Unicode11Addon} from 'xterm-addon-unicode11';
@ -10,9 +10,12 @@ import {clipboard, shell} from 'electron';
import Color from 'color';
import terms from '../terms';
import processClipboard from '../utils/paste';
import SearchBox from './searchBox';
import _SearchBox from './searchBox';
import {TermProps} from '../hyper';
import {ObjectTypedKeys} from '../utils/object';
import {decorate} from '../utils/plugins';
const SearchBox = decorate(_SearchBox, 'SearchBox');
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform);
@ -78,11 +81,27 @@ const getTermOptions = (props: TermProps): ITerminalOptions => {
brightCyan: props.colors.lightCyan,
brightWhite: props.colors.lightWhite
},
screenReaderMode: props.screenReaderMode
screenReaderMode: props.screenReaderMode,
overviewRulerWidth: 20
};
};
export default class Term extends React.PureComponent<TermProps> {
export default class Term extends React.PureComponent<
TermProps,
{
searchOptions: {
caseSensitive: boolean;
wholeWord: boolean;
regex: boolean;
};
searchResults:
| {
resultIndex: number;
resultCount: number;
}
| undefined;
}
> {
termRef: HTMLElement | null;
termWrapperRef: HTMLElement | null;
termOptions: ITerminalOptions;
@ -94,6 +113,16 @@ export default class Term extends React.PureComponent<TermProps> {
term!: Terminal;
resizeObserver!: ResizeObserver;
resizeTimeout!: NodeJS.Timeout;
searchDecorations: ISearchDecorationOptions;
state = {
searchOptions: {
caseSensitive: false,
wholeWord: false,
regex: false
},
searchResults: undefined
};
constructor(props: TermProps) {
super(props);
props.ref_(props.uid, this);
@ -104,6 +133,11 @@ export default class Term extends React.PureComponent<TermProps> {
this.termDefaultBellSound = null;
this.fitAddon = new FitAddon();
this.searchAddon = new SearchAddon();
this.searchDecorations = {
activeMatchColorOverviewRuler: Color(this.props.cursorColor).hex(),
matchOverviewRuler: Color(this.props.borderColor).hex(),
activeMatchBackground: Color(this.props.cursorColor).hex()
};
}
// The main process shows this in the About dialog
@ -242,6 +276,15 @@ export default class Term extends React.PureComponent<TermProps> {
);
}
this.disposableListeners.push(
this.searchAddon.onDidChangeResults((results) => {
this.setState((state) => ({
...state,
searchResults: results
}));
})
);
window.addEventListener('paste', this.onWindowPaste, {
capture: true
});
@ -302,20 +345,28 @@ export default class Term extends React.PureComponent<TermProps> {
this.term.reset();
}
search = (searchTerm = '') => {
this.searchAddon.findNext(searchTerm);
};
searchNext = (searchTerm: string) => {
this.searchAddon.findNext(searchTerm);
this.searchAddon.findNext(searchTerm, {
...this.state.searchOptions,
decorations: this.searchDecorations
});
};
searchPrevious = (searchTerm: string) => {
this.searchAddon.findPrevious(searchTerm);
this.searchAddon.findPrevious(searchTerm, {
...this.state.searchOptions,
decorations: this.searchDecorations
});
};
closeSearchBox = () => {
this.props.onCloseSearch();
this.searchAddon.clearDecorations();
this.searchAddon.clearActiveDecoration();
this.setState((state) => ({
...state,
searchResults: undefined
}));
this.term.focus();
};
@ -350,8 +401,8 @@ export default class Term extends React.PureComponent<TermProps> {
// otherwise use the default sound found in xterm.
nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound!;
if (!prevProps.search && this.props.search) {
this.search();
if (prevProps.search && !this.props.search) {
this.closeSearchBox();
}
// Update only options that have changed.
@ -445,14 +496,38 @@ export default class Term extends React.PureComponent<TermProps> {
{this.props.customChildren}
{this.props.search ? (
<SearchBox
search={this.search}
next={this.searchNext}
prev={this.searchPrevious}
close={this.closeSearchBox}
caseSensitive={this.state.searchOptions.caseSensitive}
wholeWord={this.state.searchOptions.wholeWord}
regex={this.state.searchOptions.regex}
results={this.state.searchResults}
toggleCaseSensitive={() =>
this.setState({
...this.state,
searchOptions: {...this.state.searchOptions, caseSensitive: !this.state.searchOptions.caseSensitive}
})
}
toggleWholeWord={() =>
this.setState({
...this.state,
searchOptions: {...this.state.searchOptions, wholeWord: !this.state.searchOptions.wholeWord}
})
}
toggleRegex={() =>
this.setState({
...this.state,
searchOptions: {...this.state.searchOptions, regex: !this.state.searchOptions.regex}
})
}
selectionColor={this.props.selectionColor}
backgroundColor={this.props.backgroundColor}
foregroundColor={this.props.foregroundColor}
borderColor={this.props.borderColor}
font={this.props.uiFontFamily}
/>
) : (
''
)}
) : null}
<style jsx global>{`
.term_fit {

13
lib/hyper.d.ts vendored
View file

@ -314,10 +314,21 @@ import {TermGroupConnectedProps} from './components/term-group';
export type TermGroupProps = TermGroupConnectedProps & TermGroupOwnProps;
export type SearchBoxProps = {
search: (searchTerm: string) => void;
caseSensitive: boolean;
wholeWord: boolean;
regex: boolean;
results: {resultIndex: number; resultCount: number} | undefined;
toggleCaseSensitive: () => void;
toggleWholeWord: () => void;
toggleRegex: () => void;
next: (searchTerm: string) => void;
prev: (searchTerm: string) => void;
close: () => void;
backgroundColor: string;
foregroundColor: string;
borderColor: string;
selectionColor: string;
font: string;
};
import {FitAddon} from 'xterm-addon-fit';

View file

@ -29,8 +29,10 @@
},
"dependencies": {
"@electron/remote": "2.0.9",
"@react-icons/all-files": "4.1.0",
"args": "5.0.3",
"chalk": "5.2.0",
"clsx": "1.2.1",
"color": "4.2.3",
"columnify": "1.6.0",
"css-loader": "6.7.3",

View file

@ -797,6 +797,11 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"
"@react-icons/all-files@4.1.0":
version "4.1.0"
resolved "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz#477284873a0821928224b6fc84c62d2534d6650b"
integrity sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@ -2411,6 +2416,11 @@ clone@^1.0.2:
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clsx@1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
code-excerpt@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz#2de7d46e98514385cb01f7b3b741320115f4c95e"