-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AceEditor - codemirror replacement #9281
Merged
softwarefactory-project-zuul
merged 19 commits into
ansible:devel
from
keithjgrant:8905-codemirror-replacement
Mar 12, 2021
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
1afdd7a
Ace editor POC
keithjgrant 4e9c6a9
add code editor focus/blur keyboard controls
keithjgrant 5c38011
styling Ace CodeEditor
keithjgrant 070c67f
rename CodeMirror to CodeEditor
keithjgrant 19f4de0
add keyboard navigation help text to CodeEditor
keithjgrant 411d692
remove codemirror dependencies
keithjgrant 221021a
disable interactive elements of CodeEditor in readOnly mode
keithjgrant 2995cde
add AceEditor to changelog
keithjgrant c975f65
CodeEditor: fix hidden error message
keithjgrant 143d41f
add value assertions to code editor tests
keithjgrant 4e55c98
add more code editor tests
keithjgrant 6b2cee2
add tests ensuring forms pass correct value to CodeEditor fields
keithjgrant c2a2bf3
don't show CodeEditor control help text in readonly mode
keithjgrant 0b57522
CodeEditor: don't type newline when pressing enter to start edit mode
keithjgrant 6f7a717
hide CodeEditor touch controls menu
keithjgrant 4ea7c8a
CodeEditor bugfixes
keithjgrant 726b5dd
fix lint error
keithjgrant 05f9303
fix CodeEditor tests
keithjgrant c90dfbb
debounce CodeEditor onChange for performance improvement
keithjgrant File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import React, { useEffect, useRef, useCallback } from 'react'; | ||
import { oneOf, bool, number, string, func } from 'prop-types'; | ||
import ReactAce from 'react-ace'; | ||
import 'ace-builds/src-noconflict/mode-json'; | ||
import 'ace-builds/src-noconflict/mode-javascript'; | ||
import 'ace-builds/src-noconflict/mode-yaml'; | ||
import 'ace-builds/src-noconflict/mode-django'; | ||
import 'ace-builds/src-noconflict/theme-github'; | ||
import { withI18n } from '@lingui/react'; | ||
import { t } from '@lingui/macro'; | ||
import styled from 'styled-components'; | ||
import debounce from '../../util/debounce'; | ||
|
||
const LINE_HEIGHT = 24; | ||
const PADDING = 12; | ||
|
||
const FocusWrapper = styled.div` | ||
&& + .keyboard-help-text { | ||
opacity: 0; | ||
transition: opacity 0.1s linear; | ||
} | ||
|
||
&:focus-within + .keyboard-help-text { | ||
opacity: 1; | ||
} | ||
|
||
& .ace_hidden-cursors .ace_cursor { | ||
opacity: 0; | ||
} | ||
`; | ||
|
||
const AceEditor = styled(ReactAce)` | ||
font-family: var(--pf-global--FontFamily--monospace); | ||
max-height: 90vh; | ||
|
||
& .ace_gutter, | ||
& .ace_scroller { | ||
padding-top: 4px; | ||
padding-bottom: 4px; | ||
} | ||
|
||
& .ace_mobile-menu { | ||
display: none; | ||
} | ||
|
||
${props => | ||
props.hasErrors && | ||
` | ||
&& { | ||
--pf-c-form-control--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight); | ||
--pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor); | ||
padding-right: 24px; | ||
padding-bottom: var(--pf-c-form-control--invalid--PaddingBottom); | ||
background: var(--pf-c-form-control--invalid--Background); | ||
border-bottom-width: var(--pf-c-form-control--invalid--BorderBottomWidth); | ||
}`} | ||
|
||
${props => | ||
props.setOptions.readOnly && | ||
` | ||
&& .ace_cursor { | ||
opacity: 0; | ||
} | ||
`} | ||
`; | ||
AceEditor.displayName = 'AceEditor'; | ||
|
||
function CodeEditor({ | ||
id, | ||
value, | ||
onChange, | ||
mode, | ||
readOnly, | ||
hasErrors, | ||
rows, | ||
fullHeight, | ||
className, | ||
i18n, | ||
}) { | ||
const wrapper = useRef(null); | ||
const editor = useRef(null); | ||
|
||
useEffect( | ||
function removeTextareaTabIndex() { | ||
const editorInput = editor.current.refEditor?.querySelector('textarea'); | ||
if (editorInput && !readOnly) { | ||
editorInput.tabIndex = -1; | ||
} | ||
}, | ||
[readOnly] | ||
); | ||
|
||
const listen = useCallback(event => { | ||
if (wrapper.current === document.activeElement && event.key === 'Enter') { | ||
const editorInput = editor.current.refEditor?.querySelector('textarea'); | ||
if (!editorInput) { | ||
return; | ||
} | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
editorInput.focus(); | ||
} | ||
}, []); | ||
|
||
useEffect(function addKeyEventListeners() { | ||
const wrapperEl = wrapper.current; | ||
wrapperEl.addEventListener('keydown', listen); | ||
|
||
return () => { | ||
wrapperEl.removeEventListener('keydown', listen); | ||
}; | ||
}); | ||
|
||
const aceModes = { | ||
javascript: 'json', | ||
yaml: 'yaml', | ||
jinja2: 'django', | ||
}; | ||
|
||
const numRows = fullHeight ? value.split('\n').length : rows; | ||
|
||
return ( | ||
<> | ||
<FocusWrapper ref={wrapper} tabIndex={readOnly ? -1 : 0}> | ||
<AceEditor | ||
mode={aceModes[mode] || 'text'} | ||
className={`pf-c-form-control ${className}`} | ||
theme="github" | ||
onChange={debounce(onChange, 250)} | ||
value={value} | ||
name={id || 'code-editor'} | ||
editorProps={{ $blockScrolling: true }} | ||
fontSize={16} | ||
width="100%" | ||
height={`${numRows * LINE_HEIGHT + PADDING}px`} | ||
hasErrors={hasErrors} | ||
setOptions={{ | ||
readOnly, | ||
highlightActiveLine: !readOnly, | ||
highlightGutterLine: !readOnly, | ||
useWorker: false, | ||
showPrintMargin: false, | ||
}} | ||
commands={[ | ||
{ | ||
name: 'escape', | ||
bindKey: { win: 'Esc', mac: 'Esc' }, | ||
exec: () => { | ||
wrapper.current.focus(); | ||
}, | ||
}, | ||
{ | ||
name: 'tab escape', | ||
bindKey: { win: 'Shift-Tab', mac: 'Shift-Tab' }, | ||
exec: () => { | ||
wrapper.current.focus(); | ||
}, | ||
}, | ||
]} | ||
ref={editor} | ||
/> | ||
</FocusWrapper> | ||
{!readOnly && ( | ||
<div | ||
className="pf-c-form__helper-text keyboard-help-text" | ||
aria-live="polite" | ||
> | ||
{i18n._(t`Press Enter to edit. Press ESC to stop editing.`)} | ||
</div> | ||
)} | ||
</> | ||
); | ||
} | ||
CodeEditor.propTypes = { | ||
value: string.isRequired, | ||
onChange: func, | ||
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired, | ||
readOnly: bool, | ||
hasErrors: bool, | ||
fullHeight: bool, | ||
rows: number, | ||
className: string, | ||
}; | ||
CodeEditor.defaultProps = { | ||
readOnly: false, | ||
onChange: () => {}, | ||
rows: 6, | ||
fullHeight: false, | ||
hasErrors: false, | ||
className: '', | ||
}; | ||
|
||
export default withI18n()(CodeEditor); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import React from 'react'; | ||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; | ||
import CodeEditor from './CodeEditor'; | ||
import debounce from '../../util/debounce'; | ||
|
||
jest.mock('../../util/debounce'); | ||
|
||
describe('CodeEditor', () => { | ||
beforeEach(() => { | ||
document.body.createTextRange = jest.fn(); | ||
}); | ||
|
||
it('should pass value and mode through to ace editor', () => { | ||
const onChange = jest.fn(); | ||
const wrapper = mountWithContexts( | ||
<CodeEditor value={'---\nfoo: bar'} onChange={onChange} mode="yaml" /> | ||
); | ||
const aceEditor = wrapper.find('AceEditor'); | ||
expect(aceEditor.prop('mode')).toEqual('yaml'); | ||
expect(aceEditor.prop('setOptions').readOnly).toEqual(false); | ||
expect(aceEditor.prop('value')).toEqual('---\nfoo: bar'); | ||
}); | ||
|
||
it('should trigger onChange prop', () => { | ||
debounce.mockImplementation(fn => fn); | ||
const onChange = jest.fn(); | ||
const wrapper = mountWithContexts( | ||
<CodeEditor value="---" onChange={onChange} mode="yaml" /> | ||
); | ||
const aceEditor = wrapper.find('AceEditor'); | ||
aceEditor.prop('onChange')('newvalue'); | ||
expect(onChange).toHaveBeenCalledWith('newvalue'); | ||
}); | ||
|
||
it('should render in read only mode', () => { | ||
const onChange = jest.fn(); | ||
const wrapper = mountWithContexts( | ||
<CodeEditor value="---" onChange={onChange} mode="yaml" readOnly /> | ||
); | ||
const aceEditor = wrapper.find('AceEditor'); | ||
expect(aceEditor.prop('setOptions').readOnly).toEqual(true); | ||
expect(aceEditor.prop('value')).toEqual('---'); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😌