import { Modal } from '@wordpress/components' import { __ } from '@wordpress/i18n' import React, { useEffect, useRef, useState } from 'react' import { useSnippetsAPI } from '../../hooks/useSnippetsAPI' import { useSnippetsList } from '../../hooks/useSnippetsList' import { handleUnknownError } from '../../utils/errors' import { downloadSnippetExportFile } from '../../utils/files' import { canModifySnippet, cloneSnippetObject, getSnippetEditUrl } from '../../utils/snippets/snippets' import { Badge } from './Badge' import { Button } from './Button' import { useDeleteSnippet } from './DeleteButton' import type { EditorConfiguration, EditorFromTextArea } from 'codemirror' import type { PropsWithChildren, ReactNode } from 'react' import type { Snippet, SnippetType } from '../../types/Snippet' const CODE_PREVIEW_LABEL = __('Snippet code preview', 'code-snippets') export interface SnippetPreviewModalProps { title: string code: string type: SnippetType isOpen: boolean setIsOpen: (isOpen: boolean) => void snippet?: Snippet footerActions?: ReactNode } const EDITOR_MODES: Record = { css: 'text/css', js: 'javascript', php: 'text/x-php', html: 'application/x-httpd-php' } const getPreviewEditorSettings = (type: string): EditorConfiguration => ({ extraKeys: { 'Tab': false, 'Shift-Tab': false }, readOnly: true, lineNumbers: true, theme: window.CODE_SNIPPETS_MANAGE?.editorTheme ?? 'default', mode: EDITOR_MODES[type] ?? EDITOR_MODES.php, screenReaderLabel: CODE_PREVIEW_LABEL }) interface SnippetPreviewActionsProps { snippet: Snippet closeModal: () => void } interface SnippetPreviewButtonsProps extends SnippetPreviewActionsProps { requestDelete: () => void isWorking: boolean setIsWorking: (working: boolean) => void } const usePreviewActionHandlers = ({ snippet, closeModal, setIsWorking }: Omit) => { const api = useSnippetsAPI() const { refreshSnippetsList } = useSnippetsList() const handleClone = () => { setIsWorking(true) api.create(cloneSnippetObject(snippet)) .then(refreshSnippetsList) .then(closeModal) .catch(handleUnknownError) .finally(() => setIsWorking(false)) } const handleExport = () => { setIsWorking(true) api.export(snippet) .then(response => downloadSnippetExportFile(response, snippet)) .catch(handleUnknownError) .finally(() => setIsWorking(false)) } return { handleClone, handleExport } } const SnippetPreviewButtons: React.FC = ({ snippet, closeModal, requestDelete, isWorking, setIsWorking }) => { const canModify = canModifySnippet(snippet) const actionOptions = { snippet, closeModal, setIsWorking } const { handleClone, handleExport } = usePreviewActionHandlers(actionOptions) return (
{snippet.locked || !canModify ? __('View', 'code-snippets') : __('Edit', 'code-snippets')} {canModify ? : null} {snippet.locked || !canModify ? null : }
) } /** * Footer action bar for previews of local snippets. Requires the snippets API * and snippets list contexts, so it is only rendered when the modal receives a * full snippet object rather than bare title/code/type values. */ const SnippetPreviewActions: React.FC = ({ snippet, closeModal }) => { const { refreshSnippetsList } = useSnippetsList() const [isWorking, setIsWorking] = useState(false) const { requestDelete, confirmDialog } = useDeleteSnippet({ snippet, setIsWorking, onSuccess: () => { closeModal() return refreshSnippetsList() }, onError: handleUnknownError }) return (
{__('Priority', 'code-snippets')} {snippet.priority}
{confirmDialog}
) } const PreviewTypeBadge: React.FC<{ type: SnippetType }> = ({ type }) =>
const PreviewFooterActionsWrapper: React.FC = ({ children }) => children ?
{children}
: null /** * Modal for quickly viewing a snippet's code in a read-only CodeMirror editor, * without navigating to the edit page. Shared between local snippets and cloud * snippet previews. Passing a full snippet object adds a footer of snippet * actions, which requires the snippets API and snippets list contexts. */ export const SnippetPreviewModal: React.FC = ({ title, code, type, isOpen, setIsOpen, snippet, footerActions }) => { const textareaRef = useRef(null) useEffect(() => { if (!isOpen || !textareaRef.current || !window.wp.codeEditor) { return undefined } const instance = window.wp.codeEditor.initialize( textareaRef.current, { codemirror: getPreviewEditorSettings(type) } ) // CodeMirror hides the labeled source textarea and creates an unlabelled // internal input. The screenReaderLabel option only exists from CodeMirror // 5.59, while WordPress 5.5 ships 5.29, so label the input directly. instance.codemirror.getInputField().setAttribute('aria-label', CODE_PREVIEW_LABEL) return () => { (instance.codemirror as EditorFromTextArea).toTextArea() } }, [isOpen, type]) return isOpen ? setIsOpen(false)} title={title} headerActions={} >