PluginProbe
Code Snippets / 3.10.2
Code Snippets v3.10.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 / common / snippets / SnippetPreviewModal.tsx

SnippetPreviewModal.tsx in Code Snippets 3.10.2, at js/components/common/snippets/SnippetPreviewModal.tsx

289 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { Modal } from '@wordpress/components'
2 import { __ } from '@wordpress/i18n'
3 import React, { useEffect, useRef, useState } from 'react'
4 import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI'
5 import { useSnippetsList } from '../../../hooks/useSnippetsList'
6 import { handleUnknownError } from '../../../utils/errors'
7 import { downloadSnippetExportFile } from '../../../utils/files'
8 import { canModifySnippet, cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets'
9 import { Badge } from '../Badge'
10 import { Button } from '../Button'
11 import { CloudSnippetDownloadButton } from '../cloud/CloudSnippetDownloadButton'
12 import { ConfirmDeleteDialog, useDeleteSnippet } from './ConfirmDeleteDialog'
13 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
14 import type { EditorConfiguration, EditorFromTextArea } from 'codemirror'
15 import type { ReactNode } from 'react'
16 import type { Snippet, SnippetType } from '../../../types/Snippet'
17
18 const EDITOR_MODES: Record<string, string> = {
19 css: 'text/css',
20 js: 'javascript',
21 php: 'text/x-php',
22 html: 'application/x-httpd-php'
23 }
24
25 const getClipboard = (): Clipboard | undefined =>
26 window.isSecureContext ? navigator.clipboard as Clipboard | undefined : undefined
27
28 const getPreviewEditorSettings = (type: string): EditorConfiguration => ({
29 extraKeys: {
30 'Tab': false,
31 'Shift-Tab': false
32 },
33 readOnly: true,
34 lineNumbers: true,
35 theme: window.CODE_SNIPPETS_MANAGE?.editorTheme ?? 'default',
36 mode: EDITOR_MODES[type] ?? EDITOR_MODES.php,
37 screenReaderLabel: __('Snippet code preview', 'code-snippets')
38 })
39
40 /**
41 * Tracks whether a footer action is in flight. The ref mirrors the state so
42 * `beginWorking` can reject re-entry within the same tick, before React
43 * re-renders with the disabled buttons.
44 */
45 const useWorkingState = () => {
46 const [isWorking, setIsWorking] = useState(false)
47 const isWorkingRef = useRef(false)
48 const updateWorking = (value: boolean) => {
49 isWorkingRef.current = value
50 setIsWorking(value)
51 }
52
53 return { isWorking, setIsWorking: updateWorking }
54 }
55
56 enum CopyStatus { Ready, Copied, Failed}
57
58 const CopyCodeButton: React.FC<{ code: string }> = ({ code }) => {
59 const [copyStatus, setCopyStatus] = useState(CopyStatus.Ready)
60
61 const handleCopy = () => {
62 const clipboard = getClipboard()
63
64 if (!clipboard) {
65 setCopyStatus(CopyStatus.Failed)
66 return
67 }
68
69 void clipboard.writeText(code)
70 .then(() => setCopyStatus(CopyStatus.Copied))
71 .catch(() => setCopyStatus(CopyStatus.Failed))
72 }
73
74 return (
75 <Button secondary onClick={handleCopy}>
76 {(() => {
77 switch (copyStatus) {
78 case CopyStatus.Copied:
79 return __('Copied', 'code-snippets')
80 case CopyStatus.Failed:
81 return __('Copy unavailable', 'code-snippets')
82 case CopyStatus.Ready:
83 return __('Copy code', 'code-snippets')
84 }
85 })()}
86 </Button>
87 )
88 }
89
90 interface PreviewModalProps {
91 onRequestClose: VoidFunction
92 title: string
93 type: SnippetType
94 code: string
95 children: ReactNode
96 }
97
98 const PreviewModal: React.FC<PreviewModalProps> = ({ onRequestClose, title, type, code, children }) => {
99 const textareaRef = useRef<HTMLTextAreaElement>(null)
100
101 useEffect(() => {
102 if (!textareaRef.current || !window.wp.codeEditor) {
103 return undefined
104 }
105
106 const instance = window.wp.codeEditor.initialize(
107 textareaRef.current,
108 { codemirror: getPreviewEditorSettings(type) }
109 )
110
111 // CodeMirror hides the labeled source textarea and creates an unlabelled
112 // internal input. The screenReaderLabel option only exists from CodeMirror
113 // 5.59, while WordPress 5.5 ships 5.29, so label the input directly.
114 instance.codemirror.getInputField().setAttribute('aria-label', __('Snippet code preview', 'code-snippets'))
115
116 return () => {
117 (instance.codemirror as EditorFromTextArea).toTextArea()
118 }
119 }, [type])
120
121 return (
122 <Modal
123 className="code-snippets-preview-modal"
124 onRequestClose={onRequestClose}
125 title={title}
126 headerActions={
127 <div className="code-snippets-preview-modal__badge">
128 <Badge name={type} />
129 </div>
130 }
131 >
132 <div className="code-snippets-preview-modal__editor">
133 <textarea
134 ref={textareaRef}
135 readOnly
136 aria-label={__('Snippet code preview', 'code-snippets')}
137 defaultValue={`${'php' === type ? '<?php\n\n' : ''}${code}`}
138 />
139 </div>
140 {children}
141 </Modal>
142 )
143 }
144
145 export interface SnippetCodePreviewModalProps {
146 snippet: CloudSnippetSchema
147 setIsOpen: (isOpen: boolean) => void
148 onDownloaded: VoidFunction
149 }
150
151 export const CloudSnippetPreviewModal: React.FC<SnippetCodePreviewModalProps> = ({
152 snippet,
153 setIsOpen,
154 onDownloaded
155 }) => {
156 return (
157 <PreviewModal
158 code={snippet.code}
159 type={getSnippetType(snippet)}
160 title={snippet.name}
161 onRequestClose={() => setIsOpen(false)}
162 >
163 <div className="code-snippets-preview-modal__footer">
164 <div className="code-snippets-preview-modal__buttons">
165 <CloudSnippetDownloadButton snippet={snippet} onDownloaded={onDownloaded} />
166 {getClipboard() && <CopyCodeButton code={snippet.code} />}
167 </div>
168 </div>
169 </PreviewModal>
170 )
171 }
172
173 interface ActionButtonProps {
174 snippet: Snippet
175 isWorking: boolean
176 setIsWorking: (isWorking: boolean) => void
177 }
178
179 interface CloneButtonProps extends ActionButtonProps {
180 setIsOpen: (isOpen: boolean) => void
181 }
182
183 const CloneButton: React.FC<CloneButtonProps> = ({ snippet, isWorking, setIsWorking, setIsOpen }) => {
184 const api = useSnippetsAPI()
185 const { refreshSnippetsList } = useSnippetsList()
186
187 const handleClone = () => {
188 setIsWorking(true)
189
190 api.create(cloneSnippetObject(snippet))
191 .then(refreshSnippetsList)
192 .then(() => setIsOpen(false))
193 .catch(handleUnknownError)
194 .finally(() => setIsWorking(false))
195 }
196
197 return (
198 <Button secondary disabled={isWorking} onClick={handleClone}>
199 {__('Clone', 'code-snippets')}
200 </Button>
201 )
202 }
203
204 const ExportButton: React.FC<ActionButtonProps> = ({ snippet, isWorking, setIsWorking }) => {
205 const api = useSnippetsAPI()
206
207 const handleExport = () => {
208 setIsWorking(true)
209
210 api.export(snippet)
211 .then(response => downloadSnippetExportFile(response, snippet))
212 .catch(handleUnknownError)
213 .finally(() => setIsWorking(false))
214 }
215
216 return (
217 <Button
218 secondary
219 disabled={isWorking}
220 onClick={handleExport}
221 >
222 {__('Export', 'code-snippets')}
223 </Button>
224 )
225 }
226
227 export interface SnippetPreviewModalProps {
228 snippet: Snippet
229 setIsOpen: (open: boolean) => void
230 }
231
232 export const SnippetPreviewModal: React.FC<SnippetPreviewModalProps> = ({ snippet, setIsOpen }) => {
233 const { refreshSnippetsList } = useSnippetsList()
234 const { isWorking, setIsWorking } = useWorkingState()
235
236 const { requestDelete, deleteDialogProps } = useDeleteSnippet({
237 snippet,
238 setIsWorking,
239 onSuccess: () => {
240 setIsOpen(false)
241 return refreshSnippetsList()
242 },
243 onError: handleUnknownError
244 })
245
246 const canModify = canModifySnippet(snippet)
247
248 return (
249 <PreviewModal
250 code={snippet.code}
251 type={getSnippetType(snippet)}
252 title={getSnippetDisplayName(snippet)}
253 onRequestClose={() => setIsOpen(false)}
254 >
255 <div className="code-snippets-preview-modal__footer">
256 <div className="code-snippets-preview-modal__buttons">
257 <a className="button button-primary" href={getSnippetEditUrl(snippet)}>
258 {snippet.locked || !canModify
259 ? __('View', 'code-snippets')
260 : __('Edit', 'code-snippets')}
261 </a>
262
263 {canModify && <CloneButton snippet={snippet} isWorking={isWorking} setIsWorking={setIsWorking} setIsOpen={setIsOpen} />}
264
265 <ExportButton snippet={snippet} isWorking={isWorking} setIsWorking={setIsWorking} />
266 <CopyCodeButton code={snippet.code} />
267
268 {!snippet.locked && canModify && (
269 <Button
270 link
271 className="code-snippets-preview-modal__trash"
272 disabled={isWorking}
273 onClick={() => void requestDelete()}
274 >
275 {__('Trash', 'code-snippets')}
276 </Button>)}
277 </div>
278
279 <div className="code-snippets-preview-modal__priority">
280 <span>{__('Priority', 'code-snippets')}</span>
281 <span className="code-snippets-preview-modal__priority-value">{snippet.priority}</span>
282 </div>
283
284 <ConfirmDeleteDialog {...deleteDialogProps} />
285 </div>
286 </PreviewModal>
287 )
288 }
289