PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / js / components / EditMenu / SnippetForm / fields / CodeEditor.tsx

CodeEditor.tsx in Code Snippets 4.0.0-beta.2, at js/components/EditMenu/SnippetForm/fields/CodeEditor.tsx

154 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useId, useRef } from 'react'
2 import { __, sprintf } from '@wordpress/i18n'
3 import { useSubmitSnippet } from '../../../../hooks/useSubmitSnippet'
4 import { handleUnknownError } from '../../../../utils/errors'
5 import { isMacOS } from '../../../../utils/screen'
6 import { getSnippetType } from '../../../../utils/snippets/snippets'
7 import { stripWrapperTags } from '../../../../utils/snippets/tags'
8 import { useSnippetForm } from '../WithSnippetFormContext'
9 import { Button } from '../../../common/Button'
10 import { ExpandIcon } from '../../../common/icons/ExpandIcon'
11 import { MinimiseIcon } from '../../../common/icons/MinimiseIcon'
12 import { CodeEditorShortcuts } from './CodeEditorShortcuts'
13 import type { Dispatch, RefObject, SetStateAction } from 'react'
14 import type { ScreenNotice } from '../../../../types/ScreenNotice'
15 import type { Snippet } from '../../../../types/Snippet'
16
17 interface EditorTextareaProps {
18 textareaRef: RefObject<HTMLTextAreaElement>
19 snippetCodeId: string
20 }
21
22 const EditorTextarea: React.FC<EditorTextareaProps> = ({ textareaRef, snippetCodeId }) => {
23 const descriptionId = useId()
24 const { snippet, setSnippet } = useSnippetForm()
25
26 return (
27 <div
28 className="snippet-editor"
29 role="application"
30 aria-label={__('Code editor', 'code-snippets')}
31 aria-describedby={descriptionId}
32 >
33 <p id={descriptionId} className="screen-reader-text">
34 {__('In the editing area, the Tab key enters a tab character. To exit the code editor, press the Escape key and then the Tab key.', 'code-snippets')}
35 </p>
36 <textarea
37 ref={textareaRef}
38 id={snippetCodeId}
39 name="snippet_code"
40 value={snippet.code}
41 aria-label={__('Snippet code', 'code-snippets')}
42 rows={200}
43 spellCheck={false}
44 onChange={event => {
45 setSnippet(previous => ({ ...previous, code: event.target.value }))
46 }}
47 />
48 <CodeEditorShortcuts editorTheme={window.CODE_SNIPPETS_EDIT?.editorTheme ?? 'default'} />
49 </div>
50 )
51 }
52
53 export interface CodeEditorProps {
54 isExpanded: boolean
55 setIsExpanded: Dispatch<SetStateAction<boolean>>
56 }
57
58 /**
59 * Keep the editor's contents in step with the snippet being edited.
60 *
61 * Code pasted from a chat window or a file usually arrives wrapped in the tags
62 * for its language. Those are removed here rather than silently on save, so the
63 * editor shows what will actually be stored and does not flag an error for
64 * markup we were going to strip anyway.
65 */
66 const handleEditorChanges = (
67 instance: CodeMirror.Editor,
68 changes: readonly CodeMirror.EditorChange[],
69 setSnippet: Dispatch<SetStateAction<Snippet>>,
70 setCurrentNotice: Dispatch<SetStateAction<ScreenNotice | undefined>>
71 ) => {
72 const pasted = changes.some(change => 'paste' === change.origin)
73
74 setSnippet(previous => {
75 const value = instance.getValue()
76
77 if (!pasted) {
78 return { ...previous, code: value }
79 }
80
81 const { code, removed } = stripWrapperTags(value, getSnippetType(previous))
82
83 if (removed) {
84 instance.setValue(code)
85 setCurrentNotice(['updated', sprintf(
86 /* translators: %s: markup that was removed, such as "opening PHP tag". */
87 __('Removed the %s from the pasted code. Snippets do not need them.', 'code-snippets'),
88 removed
89 )])
90 }
91
92 return { ...previous, code }
93 })
94 }
95
96 export const CodeEditor: React.FC<CodeEditorProps> = ({ isExpanded, setIsExpanded }) => {
97 const { snippet, setSnippet, codeEditorInstance, setCodeEditorInstance, setCurrentNotice } = useSnippetForm()
98 const { submitSnippet } = useSubmitSnippet()
99 const textareaRef = useRef<HTMLTextAreaElement>(null)
100 const snippetCodeId = useId()
101
102 useEffect(() => {
103 setCodeEditorInstance(editorInstance => {
104 if (textareaRef.current && !editorInstance && window.wp.codeEditor) {
105 editorInstance = window.wp.codeEditor.initialize(textareaRef.current)
106
107 // CodeMirror hides the labelled textarea and types into an unlabelled one
108 // of its own, so the name has to be put on that input directly.
109 editorInstance.codemirror.getInputField().setAttribute('aria-label', __('Snippet code', 'code-snippets'))
110
111 editorInstance.codemirror.on('changes', (instance, changes) =>
112 handleEditorChanges(instance, changes, setSnippet, setCurrentNotice))
113 }
114
115 return editorInstance
116 })
117 }, [setCodeEditorInstance, textareaRef, setSnippet, setCurrentNotice])
118
119 useEffect(() => {
120 if (codeEditorInstance) {
121 const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') ?? {}
122 const controlKey = isMacOS() ? 'Cmd' : 'Ctrl'
123 const onSave = () => {
124 submitSnippet(snippet)
125 .then(() => undefined)
126 .catch(handleUnknownError)
127 }
128
129 codeEditorInstance.codemirror.setOption('extraKeys', {
130 ...'object' === typeof extraKeys ? extraKeys : undefined,
131 [`${controlKey}-S`]: onSave,
132 [`${controlKey}-Enter`]: onSave
133 })
134 }
135 }, [submitSnippet, codeEditorInstance, snippet])
136
137 return (
138 <div className="snippet-code-container">
139 <div className="above-snippet-code">
140 <label htmlFor={snippetCodeId}>
141 {__('Snippet Content', 'code-snippets')}
142 </label>
143
144 <Button small className="expand-editor-button" onClick={() => setIsExpanded(current => !current)}>
145 {isExpanded ? <MinimiseIcon aria-hidden="true" /> : <ExpandIcon aria-hidden="true" />}
146 {isExpanded ? __('Minimize', 'code-snippets') : __('Expand', 'code-snippets')}
147 </Button>
148 </div>
149
150 <EditorTextarea textareaRef={textareaRef} snippetCodeId={snippetCodeId} />
151 </div>
152 )
153 }
154