mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #21259 from overleaf/td-bs5-editor-search
[BS5] Migrate Editor search panel GitOrigin-RevId: 37605845d4efc27d6c0c3a11de12387e8e3262f4
This commit is contained in:
parent
2646fefce4
commit
1b2385dfce
6 changed files with 224 additions and 148 deletions
|
@ -15,9 +15,14 @@ import {
|
|||
getSearchQuery,
|
||||
SearchCursor,
|
||||
} from '@codemirror/search'
|
||||
import { Button, ButtonGroup, FormControl, InputGroup } from 'react-bootstrap'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButtonGroup from '@/features/ui/components/ol/ol-button-group'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLCloseButton from '@/features/ui/components/ol/ol-close-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import classnames from 'classnames'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
|
@ -226,14 +231,14 @@ const CodeMirrorSearchForm: FC = () => {
|
|||
role="search"
|
||||
>
|
||||
<div className="ol-cm-search-controls">
|
||||
<InputGroup
|
||||
bsSize="small"
|
||||
<span
|
||||
className={classnames('ol-cm-search-input-group', {
|
||||
'ol-cm-search-input-error':
|
||||
query.regexp && isInvalidRegExp(query.search),
|
||||
})}
|
||||
>
|
||||
<FormControl
|
||||
<OLFormControl
|
||||
ref={handleInputRef}
|
||||
type="text"
|
||||
name="search"
|
||||
// IMPORTANT: CodeMirror uses this attribute to focus the input
|
||||
|
@ -245,99 +250,90 @@ const CodeMirrorSearchForm: FC = () => {
|
|||
onChange={handleChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
className="ol-cm-search-form-input"
|
||||
bsSize="small"
|
||||
inputRef={handleInputRef}
|
||||
size="sm"
|
||||
aria-label={t('search_command_find')}
|
||||
/>
|
||||
|
||||
<InputGroup.Button>
|
||||
<Tooltip
|
||||
id="search-match-case"
|
||||
description={t('search_match_case')}
|
||||
<OLTooltip
|
||||
id="search-match-case"
|
||||
description={t('search_match_case')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.caseSensitive,
|
||||
focused: activeSearchOption === 'caseSensitive',
|
||||
}
|
||||
)}
|
||||
htmlFor={caseSensitiveId}
|
||||
aria-label={t('search_match_case')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.caseSensitive,
|
||||
focused: activeSearchOption === 'caseSensitive',
|
||||
}
|
||||
)}
|
||||
htmlFor={caseSensitiveId}
|
||||
aria-label={t('search_match_case')}
|
||||
>
|
||||
Aa
|
||||
</label>
|
||||
</Tooltip>
|
||||
</InputGroup.Button>
|
||||
Aa
|
||||
</label>
|
||||
</OLTooltip>
|
||||
|
||||
<InputGroup.Button>
|
||||
<Tooltip id="search-regexp" description={t('search_regexp')}>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.regexp,
|
||||
focused: activeSearchOption === 'regexp',
|
||||
}
|
||||
)}
|
||||
htmlFor={regexpId}
|
||||
aria-label={t('search_regexp')}
|
||||
>
|
||||
[.*]
|
||||
</label>
|
||||
</Tooltip>
|
||||
</InputGroup.Button>
|
||||
|
||||
<InputGroup.Button>
|
||||
<Tooltip
|
||||
id="search-whole-word"
|
||||
description={t('search_whole_word')}
|
||||
<OLTooltip id="search-regexp" description={t('search_regexp')}>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.regexp,
|
||||
focused: activeSearchOption === 'regexp',
|
||||
}
|
||||
)}
|
||||
htmlFor={regexpId}
|
||||
aria-label={t('search_regexp')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.wholeWord,
|
||||
focused: activeSearchOption === 'wholeWord',
|
||||
}
|
||||
)}
|
||||
htmlFor={wholeWordId}
|
||||
aria-label={t('search_whole_word')}
|
||||
>
|
||||
W
|
||||
</label>
|
||||
</Tooltip>
|
||||
</InputGroup.Button>
|
||||
[.*]
|
||||
</label>
|
||||
</OLTooltip>
|
||||
|
||||
<InputGroup.Button>
|
||||
<Tooltip
|
||||
id="search-within-selection"
|
||||
description={t('search_within_selection')}
|
||||
<OLTooltip
|
||||
id="search-whole-word"
|
||||
description={t('search_whole_word')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: query.wholeWord,
|
||||
focused: activeSearchOption === 'wholeWord',
|
||||
}
|
||||
)}
|
||||
htmlFor={wholeWordId}
|
||||
aria-label={t('search_whole_word')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: !!query.scope,
|
||||
focused: activeSearchOption === 'withinSelection',
|
||||
}
|
||||
)}
|
||||
htmlFor={withinSelectionId}
|
||||
aria-label={t('search_within_selection')}
|
||||
>
|
||||
<Icon type="align-left" fw />
|
||||
</label>
|
||||
</Tooltip>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
W
|
||||
</label>
|
||||
</OLTooltip>
|
||||
<OLTooltip
|
||||
id="search-within-selection"
|
||||
description={t('search_within_selection')}
|
||||
>
|
||||
<label
|
||||
className={classnames(
|
||||
'btn btn-sm btn-default ol-cm-search-input-button',
|
||||
{
|
||||
checked: !!query.scope,
|
||||
focused: activeSearchOption === 'withinSelection',
|
||||
}
|
||||
)}
|
||||
htmlFor={withinSelectionId}
|
||||
aria-label={t('search_within_selection')}
|
||||
>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="align-left" fw />}
|
||||
bs5={<MaterialIcon type="format_align_left" />}
|
||||
/>
|
||||
</label>
|
||||
</OLTooltip>
|
||||
</span>
|
||||
|
||||
{showReplace && (
|
||||
<InputGroup
|
||||
bsSize="small"
|
||||
className="ol-cm-search-input-group ol-cm-search-replace-input"
|
||||
>
|
||||
<FormControl
|
||||
<span className="ol-cm-search-input-group ol-cm-search-replace-input">
|
||||
<OLFormControl
|
||||
ref={handleReplaceRef}
|
||||
type="text"
|
||||
name="replace"
|
||||
placeholder={t('search_replace_with')}
|
||||
|
@ -346,11 +342,10 @@ const CodeMirrorSearchForm: FC = () => {
|
|||
onChange={handleChange}
|
||||
onKeyDown={handleReplaceKeyDown}
|
||||
className="ol-cm-search-form-input"
|
||||
bsSize="small"
|
||||
inputRef={handleReplaceRef}
|
||||
size="sm"
|
||||
aria-label={t('search_command_replace')}
|
||||
/>
|
||||
</InputGroup>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="ol-cm-search-hidden-inputs">
|
||||
|
@ -404,27 +399,51 @@ const CodeMirrorSearchForm: FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="ol-cm-search-form-group ol-cm-search-next-previous">
|
||||
<ButtonGroup className="ol-cm-search-form-button-group">
|
||||
<Button
|
||||
type="button"
|
||||
bsSize="small"
|
||||
<OLButtonGroup className="ol-cm-search-form-button-group">
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => findPrevious(view)}
|
||||
>
|
||||
<Icon
|
||||
type="chevron-up"
|
||||
fw
|
||||
accessibilityLabel={t('search_previous')}
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon
|
||||
type="chevron-up"
|
||||
fw
|
||||
accessibilityLabel={t('search_previous')}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="keyboard_arrow_up"
|
||||
accessibilityLabel={t('search_previous')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</OLButton>
|
||||
|
||||
<Button type="button" bsSize="small" onClick={() => findNext(view)}>
|
||||
<Icon
|
||||
type="chevron-down"
|
||||
fw
|
||||
accessibilityLabel={t('search_next')}
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => findNext(view)}
|
||||
>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<Icon
|
||||
type="chevron-down"
|
||||
fw
|
||||
accessibilityLabel={t('search_next')}
|
||||
/>
|
||||
}
|
||||
bs5={
|
||||
<MaterialIcon
|
||||
type="keyboard_arrow_down"
|
||||
accessibilityLabel={t('search_next')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</OLButton>
|
||||
</OLButtonGroup>
|
||||
|
||||
{position !== null && (
|
||||
<div className="ol-cm-search-form-position">
|
||||
|
@ -437,36 +456,29 @@ const CodeMirrorSearchForm: FC = () => {
|
|||
|
||||
{showReplace && (
|
||||
<div className="ol-cm-search-form-group ol-cm-search-replace-buttons">
|
||||
<Button
|
||||
type="button"
|
||||
bsSize="small"
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => replaceNext(view)}
|
||||
>
|
||||
{t('search_replace')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
bsSize="small"
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => replaceAll(view)}
|
||||
>
|
||||
{t('search_replace_all')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ol-cm-search-form-close">
|
||||
<Tooltip id="search-close" description={<>{t('close')} (Esc)</>}>
|
||||
<button
|
||||
className="close"
|
||||
onClick={() => closeSearchPanel(view)}
|
||||
type="button"
|
||||
aria-label={t('close')}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<OLTooltip id="search-close" description={<>{t('close')} (Esc)</>}>
|
||||
<OLCloseButton onClick={() => closeSearchPanel(view)} />
|
||||
</OLTooltip>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
|
|
@ -257,21 +257,29 @@ export const search = () => {
|
|||
|
||||
const searchFormTheme = EditorView.theme({
|
||||
'.ol-cm-search-form': {
|
||||
padding: '10px',
|
||||
'--ol-cm-search-form-gap': '10px',
|
||||
'--ol-cm-search-form-button-margin': '3px',
|
||||
padding: 'var(--ol-cm-search-form-gap)',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
background: 'var(--ol-blue-gray-1)',
|
||||
gap: 'var(--ol-cm-search-form-gap)',
|
||||
background: 'var(--neutral-20)',
|
||||
'--ol-cm-search-form-focus-shadow':
|
||||
'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)',
|
||||
'--ol-cm-search-form-error-shadow':
|
||||
'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--input-shadow-danger-color)',
|
||||
'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px var(--red-50)',
|
||||
containerType: 'inline-size',
|
||||
},
|
||||
'&.bootstrap-5 .ol-cm-search-form': {
|
||||
'--ol-cm-search-form-gap': 'var(--spacing-05)',
|
||||
'--ol-cm-search-form-button-margin': 'var(--spacing-02)',
|
||||
'--input-border': 'var(--border-primary)',
|
||||
'--input-border-focus': 'var(--border-active)',
|
||||
},
|
||||
'.ol-cm-search-controls': {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto auto',
|
||||
gridTemplateRows: 'auto auto',
|
||||
gap: '10px',
|
||||
gap: 'var(--ol-cm-search-form-gap)',
|
||||
},
|
||||
'@container (max-width: 450px)': {
|
||||
'.ol-cm-search-controls': {
|
||||
|
@ -280,12 +288,12 @@ const searchFormTheme = EditorView.theme({
|
|||
},
|
||||
'.ol-cm-search-form-row': {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
gap: 'var(--ol-cm-search-form-gap)',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
'.ol-cm-search-form-group': {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
gap: 'var(--ol-cm-search-form-gap)',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'.ol-cm-search-input-group': {
|
||||
|
@ -294,6 +302,8 @@ const searchFormTheme = EditorView.theme({
|
|||
background: 'white',
|
||||
width: '100%',
|
||||
maxWidth: '25em',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
'& input[type="text"]': {
|
||||
background: 'none',
|
||||
boxShadow: 'none',
|
||||
|
@ -303,18 +313,18 @@ const searchFormTheme = EditorView.theme({
|
|||
boxShadow: 'none',
|
||||
},
|
||||
'& .btn.btn': {
|
||||
background: 'var(--ol-blue-gray-0)',
|
||||
color: 'var(--ol-blue-gray-3)',
|
||||
background: 'var(--neutral-10)',
|
||||
color: 'var(--neutral-60)',
|
||||
borderRadius: '50%',
|
||||
height: '2em',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '2em',
|
||||
marginRight: '3px',
|
||||
marginRight: 'var(--ol-cm-search-form-button-margin)',
|
||||
'&.checked': {
|
||||
color: '#fff',
|
||||
backgroundColor: 'var(--ol-blue)',
|
||||
color: 'var(--white)',
|
||||
backgroundColor: 'var(--blue-50)',
|
||||
},
|
||||
'&:active': {
|
||||
boxShadow: 'none',
|
||||
|
@ -331,7 +341,7 @@ const searchFormTheme = EditorView.theme({
|
|||
boxShadow: 'var(--ol-cm-search-form-error-shadow)',
|
||||
},
|
||||
},
|
||||
'.input-group .ol-cm-search-form-input': {
|
||||
'.ol-cm-search-form-input': {
|
||||
border: 'none',
|
||||
},
|
||||
'.ol-cm-search-input-button': {
|
||||
|
@ -355,7 +365,9 @@ const searchFormTheme = EditorView.theme({
|
|||
left: '-10000px',
|
||||
},
|
||||
'.ol-cm-search-form-close': {
|
||||
flex: 1,
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
},
|
||||
'.ol-cm-search-replace-input': {
|
||||
order: 3,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { EditorView } from '@codemirror/view'
|
|||
import { Annotation, Compartment, TransactionSpec } from '@codemirror/state'
|
||||
import { syntaxHighlighting } from '@codemirror/language'
|
||||
import { classHighlighter } from './class-highlighter'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const optionsThemeConf = new Compartment()
|
||||
const selectedThemeConf = new Compartment()
|
||||
|
@ -16,6 +17,7 @@ type Options = {
|
|||
fontFamily: FontFamily
|
||||
lineHeight: LineHeight
|
||||
overallTheme: OverallTheme
|
||||
bootstrapVersion: 3 | 5
|
||||
}
|
||||
|
||||
export const theme = (options: Options) => [
|
||||
|
@ -67,13 +69,17 @@ const createThemeFromOptions = ({
|
|||
fontFamily = 'monaco',
|
||||
lineHeight = 'normal',
|
||||
overallTheme = '',
|
||||
bootstrapVersion = 3,
|
||||
}: Options) => {
|
||||
/**
|
||||
* Theme styles that depend on settings.
|
||||
*/
|
||||
return [
|
||||
EditorView.editorAttributes.of({
|
||||
class: overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light',
|
||||
class: classNames(
|
||||
overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light',
|
||||
'bootstrap-' + bootstrapVersion
|
||||
),
|
||||
style: Object.entries({
|
||||
'--font-size': `${fontSize}px`,
|
||||
'--source-font-family': fontFamilies[fontFamily]?.join(', '),
|
||||
|
|
|
@ -66,6 +66,7 @@ import { useRangesContext } from '@/features/review-panel-new/context/ranges-con
|
|||
import { updateRanges } from '@/features/source-editor/extensions/ranges'
|
||||
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
|
||||
import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
|
||||
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||
|
||||
function useCodeMirrorScope(view: EditorView) {
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
@ -141,6 +142,7 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
lineHeight,
|
||||
overallTheme,
|
||||
editorTheme,
|
||||
bootstrapVersion: 3 as 3 | 5,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -150,6 +152,7 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
lineHeight,
|
||||
overallTheme,
|
||||
editorTheme,
|
||||
bootstrapVersion: isBootstrap5() ? 5 : 3,
|
||||
}
|
||||
|
||||
view.dispatch(
|
||||
|
@ -158,6 +161,7 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
fontSize,
|
||||
lineHeight,
|
||||
overallTheme,
|
||||
bootstrapVersion: themeRef.current.bootstrapVersion,
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||
import { CloseButton, CloseButtonProps } from 'react-bootstrap-5'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
const OLCloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const bs3CloseButtonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
|
||||
className: classNames('close', props.className),
|
||||
onClick: props.onClick,
|
||||
onMouseOver: props.onMouseOver,
|
||||
onMouseOut: props.onMouseOut,
|
||||
|
||||
'aria-label': t('close'),
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<button {...bs3CloseButtonProps}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
}
|
||||
bs5={<CloseButton ref={ref} {...props} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
OLCloseButton.displayName = 'OLCloseButton'
|
||||
|
||||
export default OLCloseButton
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef, ComponentProps } from 'react'
|
||||
import { forwardRef, ComponentProps, useCallback } from 'react'
|
||||
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
|
||||
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
|
||||
import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control'
|
||||
|
@ -15,8 +15,22 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
|
|||
(props, ref) => {
|
||||
const { bs3Props, ...rest } = props
|
||||
|
||||
// Use a callback so that the ref passed to the BS3 FormControl is stable
|
||||
const bs3InputRef = useCallback(
|
||||
(inputElement: HTMLInputElement) => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(inputElement)
|
||||
} else if (ref) {
|
||||
ref.current = inputElement
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
)
|
||||
|
||||
let bs3FormControlProps: BS3FormControlProps = {
|
||||
inputRef: bs3InputRef,
|
||||
componentClass: rest.as,
|
||||
bsSize: rest.size,
|
||||
id: rest.id,
|
||||
name: rest.name,
|
||||
className: rest.className,
|
||||
|
@ -37,13 +51,6 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
|
|||
onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
|
||||
onBlur: rest.onBlur as BS3FormControlProps['onBlur'],
|
||||
onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
|
||||
inputRef: (inputElement: HTMLInputElement) => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(inputElement)
|
||||
} else if (ref) {
|
||||
ref.current = inputElement
|
||||
}
|
||||
},
|
||||
prepend: rest.prepend,
|
||||
append: rest.append,
|
||||
...bs3Props,
|
||||
|
|
Loading…
Reference in a new issue