PluginProbe
Code Snippets / 3.10.0-beta.1
Code Snippets v3.10.0-beta.1
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 / SnippetPreviewModal.tsx

SnippetPreviewModal.tsx in Code Snippets 3.10.0-beta.1, at js/components/common/SnippetPreviewModal.tsx

247 lines 6.7 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, getSnippetEditUrl } from '../../utils/snippets/snippets'
9 import { Badge } from './Badge'
10 import { Button } from './Button'
11 import { useDeleteSnippet } from './DeleteButton'
12 import type { EditorConfiguration, EditorFromTextArea } from 'codemirror'
13 import type { PropsWithChildren, ReactNode } from 'react'
14 import type { Snippet, SnippetType } from '../../types/Snippet'
15
16 const CODE_PREVIEW_LABEL = __('Snippet code preview', 'code-snippets')
17
18 export interface SnippetPreviewModalProps {
19 title: string
20 code: string
21 type: SnippetType
22 isOpen: boolean
23 setIsOpen: (isOpen: boolean) => void
24 snippet?: Snippet
25 footerActions?: ReactNode
26 }
27
28 const EDITOR_MODES: Record<string, string> = {
29 css: 'text/css',
30 js: 'javascript',
31 php: 'text/x-php',
32 html: 'application/x-httpd-php'
33 }
34
35 const getPreviewEditorSettings = (type: string): EditorConfiguration => ({
36 extraKeys: {
37 'Tab': false,
38 'Shift-Tab': false
39 },
40 readOnly: true,
41 lineNumbers: true,
42 theme: window.CODE_SNIPPETS_MANAGE?.editorTheme ?? 'default',
43 mode: EDITOR_MODES[type] ?? EDITOR_MODES.php,
44 screenReaderLabel: CODE_PREVIEW_LABEL
45 })
46
47 interface SnippetPreviewActionsProps {
48 snippet: Snippet
49 closeModal: () => void
50 }
51
52 interface SnippetPreviewButtonsProps extends SnippetPreviewActionsProps {
53 requestDelete: () => void
54 isWorking: boolean
55 setIsWorking: (working: boolean) => void
56 }
57
58 const usePreviewActionHandlers = ({
59 snippet,
60 closeModal,
61 setIsWorking
62 }: Omit<SnippetPreviewButtonsProps, 'requestDelete' | 'isWorking'>) => {
63 const api = useSnippetsAPI()
64 const { refreshSnippetsList } = useSnippetsList()
65 const handleClone = () => {
66 setIsWorking(true)
67
68 api.create(cloneSnippetObject(snippet))
69 .then(refreshSnippetsList)
70 .then(closeModal)
71 .catch(handleUnknownError)
72 .finally(() => setIsWorking(false))
73 }
74 const handleExport = () => {
75 setIsWorking(true)
76
77 api.export(snippet)
78 .then(response => downloadSnippetExportFile(response, snippet))
79 .catch(handleUnknownError)
80 .finally(() => setIsWorking(false))
81 }
82
83 return { handleClone, handleExport }
84 }
85
86 const SnippetPreviewButtons: React.FC<SnippetPreviewButtonsProps> = ({
87 snippet,
88 closeModal,
89 requestDelete,
90 isWorking,
91 setIsWorking
92 }) => {
93 const canModify = canModifySnippet(snippet)
94 const actionOptions = { snippet, closeModal, setIsWorking }
95 const { handleClone, handleExport } = usePreviewActionHandlers(actionOptions)
96
97 return (
98 <div className="code-snippets-preview-modal__buttons">
99 <a className="button button-primary" href={getSnippetEditUrl(snippet)}>
100 {snippet.locked || !canModify ? __('View', 'code-snippets') : __('Edit', 'code-snippets')}
101 </a>
102
103 {canModify
104 ? <Button
105 secondary
106 disabled={isWorking}
107 onClick={handleClone}
108 >
109 {__('Clone', 'code-snippets')}
110 </Button>
111 : null}
112
113 <Button
114 secondary
115 disabled={isWorking}
116 onClick={handleExport}
117 >
118 {__('Export', 'code-snippets')}
119 </Button>
120
121 {snippet.locked || !canModify
122 ? null
123 : <Button
124 link
125 className="code-snippets-preview-modal__trash"
126 disabled={isWorking}
127 onClick={requestDelete}
128 >
129 {__('Trash', 'code-snippets')}
130 </Button>}
131 </div>
132 )
133 }
134
135 /**
136 * Footer action bar for previews of local snippets. Requires the snippets API
137 * and snippets list contexts, so it is only rendered when the modal receives a
138 * full snippet object rather than bare title/code/type values.
139 */
140 const SnippetPreviewActions: React.FC<SnippetPreviewActionsProps> = ({
141 snippet,
142 closeModal
143 }) => {
144 const { refreshSnippetsList } = useSnippetsList()
145 const [isWorking, setIsWorking] = useState(false)
146 const { requestDelete, confirmDialog } = useDeleteSnippet({
147 snippet,
148 setIsWorking,
149 onSuccess: () => {
150 closeModal()
151 return refreshSnippetsList()
152 },
153 onError: handleUnknownError
154 })
155
156 return (
157 <div className="code-snippets-preview-modal__footer">
158 <SnippetPreviewButtons
159 snippet={snippet}
160 closeModal={closeModal}
161 requestDelete={requestDelete}
162 isWorking={isWorking}
163 setIsWorking={setIsWorking}
164 />
165
166 <div className="code-snippets-preview-modal__priority">
167 <span>{__('Priority', 'code-snippets')}</span>
168 <span className="code-snippets-preview-modal__priority-value">{snippet.priority}</span>
169 </div>
170
171 {confirmDialog}
172 </div>
173 )
174 }
175
176 const PreviewTypeBadge: React.FC<{ type: SnippetType }> = ({ type }) =>
177 <div className="code-snippets-preview-modal__badge">
178 <Badge name={type} />
179 </div>
180
181 const PreviewFooterActionsWrapper: React.FC<PropsWithChildren> = ({ children }) =>
182 children
183 ? <div className="code-snippets-preview-modal__footer">
184 <div className="code-snippets-preview-modal__buttons">{children}</div>
185 </div>
186 : null
187
188 /**
189 * Modal for quickly viewing a snippet's code in a read-only CodeMirror editor,
190 * without navigating to the edit page. Shared between local snippets and cloud
191 * snippet previews. Passing a full snippet object adds a footer of snippet
192 * actions, which requires the snippets API and snippets list contexts.
193 */
194 export const SnippetPreviewModal: React.FC<SnippetPreviewModalProps> = ({
195 title,
196 code,
197 type,
198 isOpen,
199 setIsOpen,
200 snippet,
201 footerActions
202 }) => {
203 const textareaRef = useRef<HTMLTextAreaElement>(null)
204
205 useEffect(() => {
206 if (!isOpen || !textareaRef.current || !window.wp.codeEditor) {
207 return undefined
208 }
209
210 const instance = window.wp.codeEditor.initialize(
211 textareaRef.current,
212 { codemirror: getPreviewEditorSettings(type) }
213 )
214
215 // CodeMirror hides the labeled source textarea and creates an unlabelled
216 // internal input. The screenReaderLabel option only exists from CodeMirror
217 // 5.59, while WordPress 5.5 ships 5.29, so label the input directly.
218 instance.codemirror.getInputField().setAttribute('aria-label', CODE_PREVIEW_LABEL)
219
220 return () => {
221 (instance.codemirror as EditorFromTextArea).toTextArea()
222 }
223 }, [isOpen, type])
224
225 return isOpen
226 ? <Modal
227 className="code-snippets-preview-modal"
228 onRequestClose={() => setIsOpen(false)}
229 title={title}
230 headerActions={<PreviewTypeBadge type={type} />}
231 >
232 <div className="code-snippets-preview-modal__editor">
233 <textarea
234 ref={textareaRef}
235 readOnly
236 aria-label={CODE_PREVIEW_LABEL}
237 defaultValue={`${'php' === type ? '<?php\n\n' : ''}${code}`}
238 />
239 </div>
240
241 {snippet
242 ? <SnippetPreviewActions snippet={snippet} closeModal={() => setIsOpen(false)} />
243 : <PreviewFooterActionsWrapper>{footerActions}</PreviewFooterActionsWrapper>}
244 </Modal>
245 : null
246 }
247