PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
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
← All changes | js/components/EditMenu/SnippetForm/fields/CodeEditor.tsx +57 -35 3.10.0-beta.1 → trunk View file →
@@ -1,9 +1,11 @@
1 1 import React, { useEffect, useId, useRef } from 'react'
2 -import { __ } from '@wordpress/i18n'
2 +import { __, sprintf } from '@wordpress/i18n'
3 3 import { useSubmitSnippet } from '../../../../hooks/useSubmitSnippet'
4 4 import { handleUnknownError } from '../../../../utils/errors'
5 5 import { isMacOS } from '../../../../utils/screen'
6 +import { getSnippetType } from '../../../../utils/snippets/snippets'
7 +import { stripWrapperTags } from '../../../../utils/snippets/tags'
6 8 import { useSnippetForm } from '../WithSnippetFormContext'
7 9 import { Button } from '../../../common/Button'
8 10 import { ExpandIcon } from '../../../common/icons/ExpandIcon'
9 11 import { MinimiseIcon } from '../../../common/icons/MinimiseIcon'
@@ -8,37 +10,17 @@
8 10 import { ExpandIcon } from '../../../common/icons/ExpandIcon'
9 11 import { MinimiseIcon } from '../../../common/icons/MinimiseIcon'
10 12 import { CodeEditorShortcuts } from './CodeEditorShortcuts'
11 13 import type { Dispatch, RefObject, SetStateAction } from 'react'
14 +import type { ScreenNotice } from '../../../../types/ScreenNotice'
15 +import type { Snippet } from '../../../../types/Snippet'
12 16
13 17 interface EditorTextareaProps {
14 18 textareaRef: RefObject<HTMLTextAreaElement>
19 + snippetCodeId: string
15 20 }
16 21
17 -const useFocusEditorShortcut = (
18 - textareaRef: RefObject<HTMLTextAreaElement>
19 -) => {
20 - const { codeEditorInstance } = useSnippetForm()
21 -
22 - useEffect(() => {
23 - const focusEditor = () => {
24 - if (codeEditorInstance) {
25 - codeEditorInstance.codemirror.focus()
26 - return
27 - }
28 -
29 - textareaRef.current?.focus()
30 - }
31 -
32 - window.addEventListener('code_snippets_focus_editor', focusEditor)
33 -
34 - return () => {
35 - window.removeEventListener('code_snippets_focus_editor', focusEditor)
36 - }
37 - }, [codeEditorInstance, textareaRef])
38 -}
39 -
40 -const EditorTextarea: React.FC<EditorTextareaProps> = ({ textareaRef }) => {
22 +const EditorTextarea: React.FC<EditorTextareaProps> = ({ textareaRef, snippetCodeId }) => {
41 23 const descriptionId = useId()
42 24 const { snippet, setSnippet } = useSnippetForm()
43 25
44 26 return (
@@ -52,9 +34,9 @@
52 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')}
53 35 </p>
54 36 <textarea
55 37 ref={textareaRef}
56 - id="snippet-code"
38 + id={snippetCodeId}
57 39 name="snippet_code"
58 40 value={snippet.code}
59 41 aria-label={__('Snippet code', 'code-snippets')}
60 42 rows={200}
@@ -72,12 +54,51 @@
72 54 isExpanded: boolean
73 55 setIsExpanded: Dispatch<SetStateAction<boolean>>
74 56 }
75 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 +
76 96 export const CodeEditor: React.FC<CodeEditorProps> = ({ isExpanded, setIsExpanded }) => {
77 - const { snippet, setSnippet, codeEditorInstance, setCodeEditorInstance } = useSnippetForm()
97 + const { snippet, setSnippet, codeEditorInstance, setCodeEditorInstance, setCurrentNotice } = useSnippetForm()
78 98 const { submitSnippet } = useSubmitSnippet()
79 99 const textareaRef = useRef<HTMLTextAreaElement>(null)
100 + const snippetCodeId = useId()
80 101
81 102 useEffect(() => {
82 103 setCodeEditorInstance(editorInstance => {
83 104 if (textareaRef.current && !editorInstance && window.wp.codeEditor) {
@@ -82,16 +103,19 @@
82 103 setCodeEditorInstance(editorInstance => {
83 104 if (textareaRef.current && !editorInstance && window.wp.codeEditor) {
84 105 editorInstance = window.wp.codeEditor.initialize(textareaRef.current)
85 106
86 - editorInstance.codemirror.on('changes', instance => {
87 - setSnippet(previous => ({ ...previous, code: instance.getValue() }))
88 - })
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))
89 113 }
90 114
91 115 return editorInstance
92 116 })
93 - }, [setCodeEditorInstance, textareaRef, setSnippet])
117 + }, [setCodeEditorInstance, textareaRef, setSnippet, setCurrentNotice])
94 118
95 119 useEffect(() => {
96 120 if (codeEditorInstance) {
97 121 const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') ?? {}
@@ -109,14 +133,12 @@
109 133 })
110 134 }
111 135 }, [submitSnippet, codeEditorInstance, snippet])
112 136
113 - useFocusEditorShortcut(textareaRef)
114 -
115 137 return (
116 138 <div className="snippet-code-container">
117 139 <div className="above-snippet-code">
118 - <label htmlFor="snippet-code">
140 + <label htmlFor={snippetCodeId}>
119 141 {__('Snippet Content', 'code-snippets')}
120 142 </label>
121 143
122 144 <Button small className="expand-editor-button" onClick={() => setIsExpanded(current => !current)}>
@@ -124,8 +146,8 @@
124 146 {isExpanded ? __('Minimize', 'code-snippets') : __('Expand', 'code-snippets')}
125 147 </Button>
126 148 </div>
127 149
128 - <EditorTextarea textareaRef={textareaRef} />
150 + <EditorTextarea textareaRef={textareaRef} snippetCodeId={snippetCodeId} />
129 151 </div>
130 152 )
131 153 }