Skip to content

Commit

Permalink
feat(ui-source-code-editor): add search panel
Browse files Browse the repository at this point in the history
  • Loading branch information
balzss committed Feb 7, 2024
1 parent fbe4d9e commit 991e8fa
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 17 deletions.
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/ui-source-code-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
"@instructure/ui-babel-preset": "8.52.0",
"@instructure/ui-test-queries": "8.52.0",
"@instructure/ui-test-utils": "8.52.0",
"@instructure/ui-buttons": "^8.52.0",
"@instructure/ui-icons": "^8.52.0",
"@instructure/ui-text-input": "^8.52.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0"
"@testing-library/react": "^14.0.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"@babel/runtime": "^7.23.2",
Expand Down
27 changes: 27 additions & 0 deletions packages/ui-source-code-editor/src/SourceCodeEditor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,30 @@ function exampleMethod(props: Props) {

render(<AttachmentExample />)
```

### Search

To enable the search panel use the `searchConfig` prop.

You can open the search panel in the code editor by pressing `cmd/ctrl+f` when it is in focus (otherwise the browser's search will open). The reason you would use this instead of the browser native search is because it will miss results that are far out of view in the text rendered by the editor. This is the limitation of the underlying CodeMirror component.

Hitting `Enter` jumps to the next result and `Shift+Enter` to the previous. Alternatively you can use the up and down buttons to the right of the input field.

```js
---
type: example
---
<SourceCodeEditor
label="lorem"
language="markdown"
defaultValue={`Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit
enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet.
Nisi anim cupidatat excepteur officia.
`}
searchConfig={{
placeholder: 'Find in code...',
nextResultLabel: 'Next result',
prevResultLabel: 'Previouse result',
}}
/>
```
147 changes: 147 additions & 0 deletions packages/ui-source-code-editor/src/SourceCodeEditor/SearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 - present Instructure, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import React, { useState } from 'react'
import { createRoot } from 'react-dom/client'
import {
setSearchQuery,
search,
findNext,
findPrevious,
SearchQuery,
closeSearchPanel
} from '@codemirror/search'
import { EditorView } from '@codemirror/view'
import { TextInput } from '@instructure/ui-text-input'
import { IconButton } from '@instructure/ui-buttons'
import {
IconArrowOpenDownLine,
IconArrowOpenUpLine,
IconSearchLine
} from '@instructure/ui-icons'

export type SearchConfig = {
placeholder: string
nextResultLabel: string
prevResultLabel: string
}

function SearchPanel({
view,
searchConfig
}: {
view: EditorView
searchConfig: SearchConfig
}) {
const [searchQueryStr, setSearchQueryStr] = useState('')

const handleChange = (
_e: React.ChangeEvent<HTMLInputElement>,
value: string
) => {
setSearchQueryStr(value)
handleHighlightSearch(value)
}

const handleHighlightSearch = (searchStr: string) => {
view.dispatch({
effects: setSearchQuery.of(
new SearchQuery({
search: searchStr
})
)
})
}

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key !== 'Enter') return
if (!e.shiftKey) handleFindNext()
else handleFindPrev()
}

const handleKeyUp = (e: React.KeyboardEvent) => {
if (e.key !== 'Escape') return
closeSearchPanel(view)
}

const handleFindNext = () => {
handleHighlightSearch(searchQueryStr)
findNext(view)
}

const handleFindPrev = () => {
handleHighlightSearch(searchQueryStr)
findPrevious(view)
}

return (
<TextInput
inputRef={(r) => r?.focus()}
size="small"
display="inline-block"
width="20rem"
placeholder={searchConfig.placeholder}
onChange={handleChange}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
renderBeforeInput={<IconSearchLine size="x-small" />}
renderAfterInput={
<span>
<IconButton
size="small"
withBorder={false}
withBackground={false}
onClick={handleFindNext}
screenReaderLabel={searchConfig.nextResultLabel}
>
<IconArrowOpenDownLine />
</IconButton>
<IconButton
size="small"
withBorder={false}
withBackground={false}
onClick={handleFindPrev}
screenReaderLabel={searchConfig.prevResultLabel}
>
<IconArrowOpenUpLine />
</IconButton>
</span>
}
/>
)
}

export default function customSearch(searchConfig: SearchConfig | undefined) {
return searchConfig
? search({
createPanel: (view) => {
const dom = document.createElement('div')
dom.style.padding = '8px'
const root = createRoot(dom)
root.render(<SearchPanel view={view} searchConfig={searchConfig} />)
return { dom }
}
})
: []
}
19 changes: 6 additions & 13 deletions packages/ui-source-code-editor/src/SourceCodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ import {
closeBrackets,
closeBracketsKeymap
} from '@codemirror/autocomplete'
import {
highlightSelectionMatches
// Search feature is turned off for now, see note at keymaps
// searchKeymap
} from '@codemirror/search'
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
import {
indentSelection,
defaultKeymap,
Expand Down Expand Up @@ -95,6 +91,8 @@ import { textDirectionContextConsumer } from '@instructure/ui-i18n'

import { withStyle, jsx } from '@instructure/emotion'

import customSearch from './SearchPanel'

import generateStyle from './styles'
import generateComponentTheme from './theme'

Expand Down Expand Up @@ -435,7 +433,7 @@ class SourceCodeEditor extends Component<SourceCodeEditorProps> {
crosshairCursor(),
highlightSelectionMatches(),
indentOnInput(),

customSearch(this.props.searchConfig),
keymap.of(this.keymaps)
]
}
Expand All @@ -448,13 +446,8 @@ class SourceCodeEditor extends Component<SourceCodeEditorProps> {
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap

// TODO: style and include search & replace toolbar feature
// Note: the search & replace toolbar is not styled.
// If this feature is needed in the future, we need a decision about the styling.
// For now we turned the feature off, since RCE doesn't need it.
// ...searchKeymap
...lintKeymap,
...(this.props.searchConfig ? searchKeymap : [])
]

if (this.props.indentWithTab) {
Expand Down
12 changes: 10 additions & 2 deletions packages/ui-source-code-editor/src/SourceCodeEditor/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
} from '@instructure/emotion'
import type { WithDeterministicIdProps } from '@instructure/ui-react-utils'
import type { BidirectionalProps } from '@instructure/ui-i18n'
import type { SearchConfig } from './SearchPanel'

type SourceCodeEditorOwnProps = {
/**
Expand Down Expand Up @@ -204,6 +205,11 @@ type SourceCodeEditorOwnProps = {
* provides a reference to the html element of the editor's container
*/
containerRef?: (element: HTMLDivElement | null) => void

/**
* enable search panel in editor when pressing ctrl/cmd+f
*/
searchConfig?: SearchConfig
}

type PropKeys = keyof SourceCodeEditorOwnProps
Expand Down Expand Up @@ -268,7 +274,8 @@ const propTypes: PropValidators<PropKeys> = {
width: PropTypes.string,
// darkTheme: PropTypes.bool,
elementRef: PropTypes.func,
containerRef: PropTypes.func
containerRef: PropTypes.func,
searchConfig: PropTypes.object
}

const allowedProps: AllowedPropKeys = [
Expand Down Expand Up @@ -298,7 +305,8 @@ const allowedProps: AllowedPropKeys = [
'width',
// 'darkTheme',
'elementRef',
'containerRef'
'containerRef',
'searchConfig'
]

export type { SourceCodeEditorProps, SourceCodeEditorStyle }
Expand Down
3 changes: 3 additions & 0 deletions packages/ui-source-code-editor/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"include": ["src"],
"references": [
{ "path": "../ui-babel-preset/tsconfig.build.json" },
{ "path": "../ui-buttons/tsconfig.build.json" },
{ "path": "../ui-text-input/tsconfig.build.json" },
{ "path": "../ui-icons/tsconfig.build.json" },
{ "path": "../ui-test-utils/tsconfig.build.json" },
{ "path": "../ui-themes/tsconfig.build.json" },
{ "path": "../emotion/tsconfig.build.json" },
Expand Down

0 comments on commit 991e8fa

Please sign in to comment.