Initial commit: WordPress wp-content (themes, plugins, languages)
- Theme: momentry (custom theme with REST API routes) - Plugins: code-snippets (contains all API proxies) - Languages: zh_TW translations - Excludes: cache, backups, uploads, logs
This commit is contained in:
192
plugins/code-snippets/js/components/SnippetForm/SnippetForm.tsx
Normal file
192
plugins/code-snippets/js/components/SnippetForm/SnippetForm.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import React, { useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { addQueryArgs } from '@wordpress/url'
|
||||
import { WithRestAPIContext } from '../../hooks/useRestAPI'
|
||||
import { WithSnippetsListContext, useSnippetsList } from '../../hooks/useSnippetsList'
|
||||
import { SubmitSnippetAction, useSubmitSnippet } from '../../hooks/useSubmitSnippet'
|
||||
import { handleUnknownError } from '../../utils/errors'
|
||||
import { createSnippetObject, getSnippetType, isCondition, validateSnippet } from '../../utils/snippets/snippets'
|
||||
import { WithSnippetFormContext, useSnippetForm } from '../../hooks/useSnippetForm'
|
||||
import { ConfirmDialog } from '../common/ConfirmDialog'
|
||||
import { UpsellDialog } from '../common/UpsellDialog'
|
||||
import { EditorSidebar } from '../EditorSidebar'
|
||||
import { UpsellBanner } from '../common/UpsellBanner'
|
||||
import { SnippetTypeInput } from './fields/SnippetTypeInput'
|
||||
import { TagsEditor } from './fields/TagsEditor'
|
||||
import { CodeEditor } from './fields/CodeEditor'
|
||||
import { DescriptionEditor } from './fields/DescriptionEditor'
|
||||
import { NameInput } from './fields/NameInput'
|
||||
import { PageHeading } from './page/PageHeading'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { Snippet } from '../../types/Snippet'
|
||||
|
||||
const editFormClassName = ({ snippet, isReadOnly, isExpanded }: {
|
||||
snippet: Snippet,
|
||||
isReadOnly: boolean,
|
||||
isExpanded: boolean
|
||||
}) =>
|
||||
classnames(
|
||||
'snippet-form',
|
||||
isExpanded ? 'snippet-form-expanded' : 'snippet-form-collapsed',
|
||||
`${snippet.scope}-snippet`,
|
||||
`${getSnippetType(snippet)}-snippet`,
|
||||
`${snippet.id ? 'saved' : 'new'}-snippet`,
|
||||
`${snippet.active ? 'active' : 'inactive'}-snippet`,
|
||||
{
|
||||
'erroneous-snippet': !!snippet.code_error,
|
||||
'read-only-snippet': isReadOnly
|
||||
}
|
||||
)
|
||||
|
||||
interface ConfirmSubmitDialogProps {
|
||||
doSubmit: (action: SubmitSnippetAction | undefined) => void
|
||||
submitAction: SubmitSnippetAction | undefined
|
||||
setSubmitAction: (action: SubmitSnippetAction | undefined) => void
|
||||
validationWarning: string | undefined
|
||||
setValidationWarning: (warning: string | undefined) => void
|
||||
}
|
||||
|
||||
const ConfirmSubmitDialog: React.FC<ConfirmSubmitDialogProps> = ({
|
||||
doSubmit,
|
||||
submitAction,
|
||||
setSubmitAction,
|
||||
validationWarning,
|
||||
setValidationWarning
|
||||
}) =>
|
||||
<ConfirmDialog
|
||||
open={validationWarning !== undefined}
|
||||
title={__('Snippet incomplete', 'code-snippets')}
|
||||
confirmLabel={__('Continue', 'code-snippets')}
|
||||
onCancel={() => {
|
||||
setSubmitAction(undefined)
|
||||
setValidationWarning(undefined)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
doSubmit(submitAction)
|
||||
setSubmitAction(undefined)
|
||||
setValidationWarning(undefined)
|
||||
}}
|
||||
>
|
||||
<p>{`${validationWarning} ${__('Continue?', 'code-snippets')}`}</p>
|
||||
</ConfirmDialog>
|
||||
|
||||
interface EditFormProps extends PropsWithChildren {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const EditForm: React.FC<EditFormProps> = ({ children, className }) => {
|
||||
const { submitSnippet } = useSubmitSnippet()
|
||||
const { snippet } = useSnippetForm()
|
||||
const { refreshSnippetsList } = useSnippetsList()
|
||||
|
||||
const [validationWarning, setValidationWarning] = useState<string | undefined>()
|
||||
const [submitAction, setSubmitAction] = useState<SubmitSnippetAction | undefined>()
|
||||
|
||||
const doSubmit = (action?: SubmitSnippetAction) => {
|
||||
submitSnippet(action)
|
||||
.then(response => {
|
||||
if (response && 0 !== response.id && window.CODE_SNIPPETS) {
|
||||
if (window.location.href.toString().includes(window.CODE_SNIPPETS.urls.addNew)) {
|
||||
document.title = document.title
|
||||
.replace(__('Add New Snippet', 'code-snippets'), __('Edit Snippet', 'code-snippets'))
|
||||
.replace(__('Add New Condition', 'code-snippets'), __('Edit Condition', 'code-snippets'))
|
||||
|
||||
const newUrl = addQueryArgs(window.CODE_SNIPPETS.urls.edit, { id: response.id })
|
||||
window.history.pushState({}, document.title, newUrl)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(refreshSnippetsList)
|
||||
.catch(handleUnknownError)
|
||||
}
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const action = Object.values(SubmitSnippetAction).find(actionName =>
|
||||
actionName === document.activeElement?.getAttribute('name'))
|
||||
|
||||
const validationWarning = validateSnippet(snippet)
|
||||
|
||||
if (validationWarning) {
|
||||
setValidationWarning(validationWarning)
|
||||
setSubmitAction(action)
|
||||
} else {
|
||||
doSubmit(action)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="snippet-form" method="post" onSubmit={handleSubmit} className={className}>
|
||||
{children}
|
||||
</form>
|
||||
|
||||
<ConfirmSubmitDialog {...{ doSubmit, submitAction, setSubmitAction, validationWarning, setValidationWarning }} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ConditionsEditor: React.FC = () => {
|
||||
const { snippet } = useSnippetForm()
|
||||
|
||||
return isCondition(snippet)
|
||||
? <div id="snippet_conditions" className="snippet-condition-editor-container">
|
||||
<p>{__('This snippet type is not supported in this version of Code Snippets.')}</p>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
const EditFormWrap: React.FC = () => {
|
||||
const { snippet, isReadOnly } = useSnippetForm()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isUpgradeDialogOpen, setIsUpgradeDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="wrap">
|
||||
<p><small className="cs-back">
|
||||
{isCondition(snippet)
|
||||
? <a href={addQueryArgs(window.CODE_SNIPPETS?.urls.manage, { type: 'cond' })}>
|
||||
{__('Back to all conditions', 'code-snippets')}
|
||||
</a>
|
||||
: <a href={window.CODE_SNIPPETS?.urls.manage}>
|
||||
{__('Back to all snippets', 'code-snippets')}
|
||||
</a>}
|
||||
</small></p>
|
||||
|
||||
<PageHeading />
|
||||
|
||||
<EditForm className={editFormClassName({ snippet, isReadOnly, isExpanded })}>
|
||||
<main className="snippet-form-upper">
|
||||
<div className="snippet-name-wrapper">
|
||||
<NameInput />
|
||||
<SnippetTypeInput setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} />
|
||||
</div>
|
||||
|
||||
<CodeEditor {...{ isExpanded, setIsExpanded }} />
|
||||
<ConditionsEditor />
|
||||
</main>
|
||||
|
||||
<div className="snippet-form-lower">
|
||||
<UpsellBanner />
|
||||
<DescriptionEditor />
|
||||
<TagsEditor />
|
||||
</div>
|
||||
|
||||
<EditorSidebar setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} />
|
||||
</EditForm>
|
||||
|
||||
<UpsellDialog isOpen={isUpgradeDialogOpen} setIsOpen={setIsUpgradeDialogOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SnippetForm: React.FC = () =>
|
||||
<WithRestAPIContext>
|
||||
<WithSnippetsListContext>
|
||||
<WithSnippetFormContext initialSnippet={() => createSnippetObject(window.CODE_SNIPPETS_EDIT?.snippet)}>
|
||||
<EditFormWrap />
|
||||
</WithSnippetFormContext>
|
||||
</WithSnippetsListContext>
|
||||
</WithRestAPIContext>
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useSubmitSnippet } from '../../../hooks/useSubmitSnippet'
|
||||
import { handleUnknownError } from '../../../utils/errors'
|
||||
import { isMacOS } from '../../../utils/screen'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
import { Button } from '../../common/Button'
|
||||
import { ExpandIcon } from '../../common/icons/ExpandIcon'
|
||||
import { MinimiseIcon } from '../../common/icons/MinimiseIcon'
|
||||
import { CodeEditorShortcuts } from './CodeEditorShortcuts'
|
||||
import type { Dispatch, RefObject, SetStateAction } from 'react'
|
||||
|
||||
interface EditorTextareaProps {
|
||||
textareaRef: RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
|
||||
const EditorTextarea: React.FC<EditorTextareaProps> = ({ textareaRef }) => {
|
||||
const { snippet, setSnippet } = useSnippetForm()
|
||||
|
||||
return (
|
||||
<div className="snippet-editor">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
id="snippet-code"
|
||||
name="snippet_code"
|
||||
value={snippet.code}
|
||||
rows={200}
|
||||
spellCheck={false}
|
||||
onChange={event => {
|
||||
setSnippet(previous => ({ ...previous, code: event.target.value }))
|
||||
}}
|
||||
/>
|
||||
<CodeEditorShortcuts editorTheme={window.CODE_SNIPPETS_EDIT?.editorTheme ?? 'default'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface CodeEditorProps {
|
||||
isExpanded: boolean
|
||||
setIsExpanded: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const CodeEditor: React.FC<CodeEditorProps> = ({ isExpanded, setIsExpanded }) => {
|
||||
const { snippet, setSnippet, codeEditorInstance, setCodeEditorInstance } = useSnippetForm()
|
||||
const { submitSnippet } = useSubmitSnippet()
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setCodeEditorInstance(editorInstance => {
|
||||
if (textareaRef.current && !editorInstance) {
|
||||
editorInstance = window.wp.codeEditor.initialize(textareaRef.current)
|
||||
|
||||
editorInstance.codemirror.on('changes', instance => {
|
||||
setSnippet(previous => ({ ...previous, code: instance.getValue() }))
|
||||
})
|
||||
}
|
||||
|
||||
return editorInstance
|
||||
})
|
||||
}, [setCodeEditorInstance, textareaRef, setSnippet])
|
||||
|
||||
useEffect(() => {
|
||||
if (codeEditorInstance) {
|
||||
const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') ?? {}
|
||||
const controlKey = isMacOS() ? 'Cmd' : 'Ctrl'
|
||||
const onSave = () => {
|
||||
submitSnippet()
|
||||
.then(() => undefined)
|
||||
.catch(handleUnknownError)
|
||||
}
|
||||
|
||||
codeEditorInstance.codemirror.setOption('extraKeys', {
|
||||
...'object' === typeof extraKeys ? extraKeys : undefined,
|
||||
[`${controlKey}-S`]: onSave,
|
||||
[`${controlKey}-Enter`]: onSave
|
||||
})
|
||||
}
|
||||
}, [submitSnippet, codeEditorInstance, snippet])
|
||||
|
||||
return (
|
||||
<div className="snippet-code-container">
|
||||
<div className="above-snippet-code">
|
||||
<h2><label htmlFor="snippet-code">{__('Snippet Content', 'code-snippets')}</label></h2>
|
||||
|
||||
<Button small className="expand-editor-button" onClick={() => setIsExpanded(current => !current)}>
|
||||
{isExpanded ? <MinimiseIcon /> : <ExpandIcon />}
|
||||
{isExpanded ? __('Minimize', 'code-snippets') : __('Expand', 'code-snippets')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<EditorTextarea textareaRef={textareaRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { __, _x } from '@wordpress/i18n'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { KEYBOARD_KEYS } from '../../../types/KeyboardShortcut'
|
||||
import { isMacOS } from '../../../utils/screen'
|
||||
import type { KeyboardKey, KeyboardShortcut } from '../../../types/KeyboardShortcut'
|
||||
|
||||
const shortcuts: Record<string, KeyboardShortcut> = {
|
||||
saveChanges: {
|
||||
label: __('Save changes', 'code-snippets'),
|
||||
mod: 'Cmd',
|
||||
key: 'S'
|
||||
},
|
||||
selectAll: {
|
||||
label: __('Select all', 'code-snippets'),
|
||||
mod: 'Cmd',
|
||||
key: 'A'
|
||||
},
|
||||
beginSearch: {
|
||||
label: __('Begin searching', 'code-snippets'),
|
||||
mod: 'Cmd',
|
||||
key: 'F'
|
||||
},
|
||||
findNext: {
|
||||
label: __('Find next', 'code-snippets'),
|
||||
mod: 'Cmd',
|
||||
key: 'G'
|
||||
},
|
||||
findPrevious: {
|
||||
label: __('Find previous', 'code-snippets'),
|
||||
mod: ['Shift', 'Cmd'],
|
||||
key: 'G'
|
||||
},
|
||||
replace: {
|
||||
label: __('Replace', 'code-snippets'),
|
||||
mod: ['Shift', 'Cmd'],
|
||||
key: 'F'
|
||||
},
|
||||
replaceAll: {
|
||||
label: __('Replace all', 'code-snippets'),
|
||||
mod: ['Shift', 'Cmd', 'Option'],
|
||||
key: 'R'
|
||||
},
|
||||
search: {
|
||||
label: __('Persistent search', 'code-snippets'),
|
||||
mod: 'Alt',
|
||||
key: 'F'
|
||||
},
|
||||
toggleComment: {
|
||||
label: __('Toggle comment', 'code-snippets'),
|
||||
mod: 'Cmd',
|
||||
key: '/'
|
||||
},
|
||||
swapLineUp: {
|
||||
label: __('Swap line up', 'code-snippets'),
|
||||
mod: 'Option',
|
||||
key: 'Up'
|
||||
},
|
||||
swapLineDown: {
|
||||
label: __('Swap line down', 'code-snippets'),
|
||||
mod: 'Option',
|
||||
key: 'Down'
|
||||
},
|
||||
autoIndent: {
|
||||
label: __('Auto-indent current line or selection', 'code-snippets'),
|
||||
mod: 'Shift',
|
||||
key: 'Tab'
|
||||
}
|
||||
}
|
||||
|
||||
const SEP = _x('-', 'keyboard shortcut separator', 'code-snippets')
|
||||
|
||||
const ModifierKey: React.FC<{ modifier: KeyboardKey }> = ({ modifier }) => {
|
||||
switch (modifier) {
|
||||
case 'Ctrl':
|
||||
case 'Cmd':
|
||||
return (
|
||||
<>
|
||||
<kbd className="pc-key">{KEYBOARD_KEYS.Ctrl}</kbd>
|
||||
<kbd className="mac-key">{KEYBOARD_KEYS.Cmd}</kbd>
|
||||
{SEP}
|
||||
</>
|
||||
)
|
||||
|
||||
case 'Option':
|
||||
return (
|
||||
<span className="mac-key">
|
||||
<kbd className="mac-key">{KEYBOARD_KEYS.Option}</kbd>{SEP}
|
||||
</span>
|
||||
)
|
||||
|
||||
default:
|
||||
return <><kbd>{KEYBOARD_KEYS[modifier]}</kbd>{SEP}</>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeEditorShortcutsProps {
|
||||
editorTheme: string
|
||||
}
|
||||
|
||||
export const CodeEditorShortcuts: React.FC<CodeEditorShortcutsProps> = ({ editorTheme }) =>
|
||||
<div className="snippet-editor-help tooltip tooltip-inline tooltip-start">
|
||||
<span className={`dashicons dashicons-editor-help cm-s-${editorTheme}`}></span>
|
||||
|
||||
<div className={classnames('tooltip-content', { 'platform-mac': isMacOS() })}>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(shortcuts).map(([name, { label, mod, key }]) =>
|
||||
<tr key={name}>
|
||||
<td>{label}</td>
|
||||
<td>
|
||||
{(Array.isArray(mod) ? mod : [mod]).map(modifier =>
|
||||
<span key={modifier}>
|
||||
<ModifierKey modifier={modifier} />
|
||||
</span>
|
||||
)}
|
||||
<kbd>{KEYBOARD_KEYS[key]}</kbd>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import domReady from '@wordpress/dom-ready'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
|
||||
export const EDITOR_ID = 'snippet_description'
|
||||
|
||||
const TOOLBAR_BUTTONS = [
|
||||
[
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikethrough',
|
||||
'blockquote',
|
||||
'bullist',
|
||||
'numlist',
|
||||
'alignleft',
|
||||
'aligncenter',
|
||||
'alignright',
|
||||
'link',
|
||||
'wp_adv',
|
||||
'code_snippets'
|
||||
],
|
||||
[
|
||||
'formatselect',
|
||||
'forecolor',
|
||||
'pastetext',
|
||||
'removeformat',
|
||||
'charmap',
|
||||
'outdent',
|
||||
'indent',
|
||||
'undo',
|
||||
'redo',
|
||||
'spellchecker'
|
||||
]
|
||||
]
|
||||
|
||||
const initializeEditor = (onChange: (content: string) => void) => {
|
||||
window.wp.editor?.initialize(EDITOR_ID, {
|
||||
mediaButtons: window.CODE_SNIPPETS_EDIT?.descEditorOptions.mediaButtons,
|
||||
quicktags: true,
|
||||
tinymce: {
|
||||
toolbar: TOOLBAR_BUTTONS.map(line => line.join(' ')),
|
||||
setup: editor => {
|
||||
editor.on('change', () => onChange(editor.getContent()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const DescriptionEditorTextarea: React.FC = () => {
|
||||
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
|
||||
|
||||
const handleChange = useCallback(
|
||||
(desc: string) => setSnippet(previous => ({ ...previous, desc })),
|
||||
[setSnippet]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
domReady(() => initializeEditor(handleChange))
|
||||
}, [handleChange])
|
||||
|
||||
return (
|
||||
<textarea
|
||||
id={EDITOR_ID}
|
||||
className="wp-editor-area"
|
||||
onChange={event => handleChange(event.target.value)}
|
||||
autoComplete="off"
|
||||
disabled={isReadOnly}
|
||||
rows={window.CODE_SNIPPETS_EDIT?.descEditorOptions.rows}
|
||||
cols={40}
|
||||
value={snippet.desc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const DescriptionEditor: React.FC = () =>
|
||||
window.CODE_SNIPPETS_EDIT?.enableDescription
|
||||
? <div className="snippet-description-container">
|
||||
<h2>
|
||||
<label htmlFor={EDITOR_ID}>
|
||||
{__('Description', 'code-snippets')}
|
||||
</label>
|
||||
</h2>
|
||||
|
||||
<DescriptionEditorTextarea />
|
||||
</div>
|
||||
: null
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
|
||||
export const NameInput: React.FC = () => {
|
||||
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
|
||||
|
||||
return (
|
||||
<div id="titlediv">
|
||||
<div id="titlewrap">
|
||||
<label htmlFor="title" className="screen-reader-text">
|
||||
{__('Name', 'code-snippets')}
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
name="snippet_name"
|
||||
autoComplete="off"
|
||||
value={snippet.name}
|
||||
disabled={isReadOnly}
|
||||
placeholder={__('Enter snippet title', 'code-snippets')}
|
||||
onChange={event =>
|
||||
setSnippet(previous => ({ ...previous, name: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import React from 'react'
|
||||
import Select from 'react-select'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet'
|
||||
import { getSnippetType, isCondition } from '../../../utils/snippets/snippets'
|
||||
import type { SnippetCodeScope } from '../../../types/Snippet'
|
||||
import type { SelectOption } from '../../../types/SelectOption'
|
||||
|
||||
const SCOPE_ICONS: Record<SnippetCodeScope, string> = {
|
||||
'global': 'admin-site',
|
||||
'admin': 'admin-tools',
|
||||
'front-end': 'admin-appearance',
|
||||
'single-use': 'clock',
|
||||
'content': 'shortcode',
|
||||
'head-content': 'editor-code',
|
||||
'footer-content': 'editor-code',
|
||||
'admin-css': 'dashboard',
|
||||
'site-css': 'admin-customizer',
|
||||
'site-head-js': 'media-code',
|
||||
'site-footer-js': 'media-code'
|
||||
}
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<SnippetCodeScope, string> = {
|
||||
'global': __('Run everywhere', 'code-snippets'),
|
||||
'admin': __('Only run in administration area', 'code-snippets'),
|
||||
'front-end': __('Only run on site front-end', 'code-snippets'),
|
||||
'single-use': __('Only run once', 'code-snippets'),
|
||||
'content': __('Where inserted in editor', 'code-snippets'),
|
||||
'head-content': __('In site <head> section', 'code-snippets'),
|
||||
'footer-content': __('In site footer (end of <body>)', 'code-snippets'),
|
||||
'site-css': __('Site front-end', 'code-snippets'),
|
||||
'admin-css': __('Administration area', 'code-snippets'),
|
||||
'site-footer-js': __('In site footer (end of <body>)', 'code-snippets'),
|
||||
'site-head-js': __('In site <head> section', 'code-snippets')
|
||||
}
|
||||
|
||||
export const SnippetLocationInput: React.FC = () => {
|
||||
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
|
||||
|
||||
const options: SelectOption<SnippetCodeScope>[] = SNIPPET_TYPE_SCOPES[getSnippetType(snippet)]
|
||||
.filter(scope => 'condition' !== scope)
|
||||
.map(scope => ({
|
||||
key: scope,
|
||||
value: scope,
|
||||
label: SCOPE_DESCRIPTIONS[scope]
|
||||
}))
|
||||
|
||||
return isCondition(snippet)
|
||||
? null
|
||||
: <div className="block-form-field">
|
||||
<h4><label htmlFor="snippet-location">{__('Location', 'code-snippets')}</label></h4>
|
||||
<Select
|
||||
inputId="snippet-location"
|
||||
className="code-snippets-select code-snippets-select-location"
|
||||
options={options}
|
||||
isDisabled={isReadOnly}
|
||||
styles={{
|
||||
menu: provided => ({ ...provided, zIndex: 9999 }),
|
||||
input: provided => ({ ...provided, ':focus': { boxShadow: 'none' } })
|
||||
}}
|
||||
value={options.find(option => option.value === snippet.scope)}
|
||||
formatOptionLabel={({ label, value }) =>
|
||||
<>
|
||||
<span className={`dashicons dashicons-${SCOPE_ICONS[value]}`}></span>{` ${label}`}
|
||||
</>
|
||||
}
|
||||
onChange={option =>
|
||||
option?.value && setSnippet(previous => ({ ...previous, scope: option.value }))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { __, _x } from '@wordpress/i18n'
|
||||
import Select from 'react-select'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet'
|
||||
import { isLicensed } from '../../../utils/screen'
|
||||
import { getSnippetType, isProType } from '../../../utils/snippets/snippets'
|
||||
import { Badge } from '../../common/Badge'
|
||||
import type { FormatOptionLabelContext } from 'react-select'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { SnippetCodeType, SnippetType } from '../../../types/Snippet'
|
||||
import type { SelectOption } from '../../../types/SelectOption'
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
|
||||
export interface SnippetTypeInputProps {
|
||||
setIsUpgradeDialogOpen: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const EDITOR_MODES: Record<SnippetCodeType, string> = {
|
||||
css: 'text/css',
|
||||
js: 'javascript',
|
||||
php: 'text/x-php',
|
||||
html: 'application/x-httpd-php'
|
||||
}
|
||||
|
||||
const OPTIONS: SelectOption<SnippetType>[] = [
|
||||
{ value: 'php', label: __('Functions', 'code-snippets') },
|
||||
{ value: 'html', label: __('Content', 'code-snippets') },
|
||||
{ value: 'css', label: __('Styles', 'code-snippets') },
|
||||
{ value: 'js', label: __('Scripts', 'code-snippets') },
|
||||
{ value: 'cond', label: __('Conditions', 'code-snippets') }
|
||||
]
|
||||
|
||||
interface SnippetTypeOptionProps {
|
||||
option: SelectOption<SnippetType>
|
||||
context: FormatOptionLabelContext
|
||||
}
|
||||
|
||||
const SnippetTypeOption: React.FC<SnippetTypeOptionProps> = ({ option: { value, label }, context }) =>
|
||||
<div className={classnames('snippet-type-option', { 'inverted-badges': isProType(value) && !isLicensed() })}>
|
||||
{'menu' === context
|
||||
? <div>
|
||||
{label}
|
||||
{isProType(value) && !isLicensed()
|
||||
? <Badge name="pro" small>{_x('Pro', 'Upgrade to Pro', 'code-snippets')}</Badge>
|
||||
: null}
|
||||
</div>
|
||||
: null}
|
||||
<Badge name={value} />
|
||||
</div>
|
||||
|
||||
export const SnippetTypeInput: React.FC<SnippetTypeInputProps> = ({ setIsUpgradeDialogOpen }) => {
|
||||
const { snippet, setSnippet, codeEditorInstance, isReadOnly } = useSnippetForm()
|
||||
const snippetType = getSnippetType(snippet)
|
||||
|
||||
useEffect(() => {
|
||||
if (codeEditorInstance) {
|
||||
const codeEditor = codeEditorInstance.codemirror
|
||||
|
||||
codeEditor.setOption('lint' as keyof EditorConfiguration, 'php' === snippetType || 'css' === snippetType)
|
||||
|
||||
if ('cond' !== snippetType && EDITOR_MODES[snippetType]) {
|
||||
codeEditor.setOption('mode', EDITOR_MODES[snippetType])
|
||||
codeEditor.refresh()
|
||||
}
|
||||
}
|
||||
}, [codeEditorInstance, snippetType])
|
||||
|
||||
return (
|
||||
<div className="snippet-type-container">
|
||||
<label htmlFor="snippet-type-select-input" className="screen-reader-text">
|
||||
{__('Snippet Type', 'code-snippets')}
|
||||
</label>
|
||||
<Select
|
||||
inputId="snippet-type-select-input"
|
||||
className="code-snippets-select"
|
||||
isDisabled={isReadOnly}
|
||||
options={0 === snippet.id ? OPTIONS : OPTIONS.filter(option => 'cond' !== option.value)}
|
||||
menuPlacement="bottom"
|
||||
styles={{
|
||||
menu: provided => ({
|
||||
...provided,
|
||||
zIndex: 9999,
|
||||
width: 'max-content',
|
||||
minWidth: '100%'
|
||||
}),
|
||||
input: provided => ({ ...provided, boxShadow: 'none' })
|
||||
}}
|
||||
value={OPTIONS.find(option => option.value === snippetType)}
|
||||
formatOptionLabel={(data, meta) =>
|
||||
<SnippetTypeOption option={data} context={meta.context} />}
|
||||
onChange={option => {
|
||||
if (option && isProType(option.value) && !isLicensed()) {
|
||||
setIsUpgradeDialogOpen(true)
|
||||
} else if (option) {
|
||||
setSnippet(previous => ({
|
||||
...previous,
|
||||
scope: SNIPPET_TYPE_SCOPES[option.value][0]
|
||||
}))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { FormTokenField } from '@wordpress/components'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
|
||||
const options = window.CODE_SNIPPETS_EDIT?.tagOptions
|
||||
|
||||
export const TagsEditor: React.FC = () => {
|
||||
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
|
||||
|
||||
return options?.enabled
|
||||
? <div className="snippet-tags-container">
|
||||
<h3><label htmlFor="components-form-token-input-0">{__('Snippet Tags', 'code-snippets')}</label></h3>
|
||||
|
||||
<FormTokenField
|
||||
label=""
|
||||
value={snippet.tags}
|
||||
disabled={isReadOnly}
|
||||
suggestions={options.availableTags}
|
||||
tokenizeOnBlur
|
||||
tokenizeOnSpace={!options.allowSpaces}
|
||||
onChange={tokens => {
|
||||
setSnippet(previous => ({
|
||||
...previous,
|
||||
tags: tokens.map(token => 'string' === typeof token ? token : token.value)
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
1
plugins/code-snippets/js/components/SnippetForm/index.ts
Normal file
1
plugins/code-snippets/js/components/SnippetForm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './SnippetForm'
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createInterpolateElement } from '@wordpress/element'
|
||||
import React from 'react'
|
||||
import { __, sprintf } from '@wordpress/i18n'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
import { DismissibleNotice } from '../../common/DismissableNotice'
|
||||
|
||||
export const Notices: React.FC = () => {
|
||||
const { currentNotice, setCurrentNotice, snippet, setSnippet } = useSnippetForm()
|
||||
|
||||
return <>
|
||||
{currentNotice
|
||||
? <DismissibleNotice className={currentNotice[0]} onDismiss={() => setCurrentNotice(undefined)}>
|
||||
<p>{createInterpolateElement(currentNotice[1], { strong: <strong /> })}</p>
|
||||
</DismissibleNotice>
|
||||
: null}
|
||||
|
||||
{snippet.code_error
|
||||
? <DismissibleNotice
|
||||
className="notice-error"
|
||||
onDismiss={() => setSnippet(previous => ({ ...previous, code_error: null }))}
|
||||
>
|
||||
<p>
|
||||
<strong>{sprintf(
|
||||
// translators: %d: line number.
|
||||
__('Snippet automatically deactivated due to an error on line %d:', 'code-snippets'),
|
||||
snippet.code_error[1]
|
||||
)}</strong>
|
||||
|
||||
<blockquote>{snippet.code_error[0]}</blockquote>
|
||||
</p>
|
||||
</DismissibleNotice>
|
||||
: null}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { __, _x } from '@wordpress/i18n'
|
||||
import React from 'react'
|
||||
import { useSnippetForm } from '../../../hooks/useSnippetForm'
|
||||
import { createSnippetObject } from '../../../utils/snippets/snippets'
|
||||
import type { Snippet } from '../../../types/Snippet'
|
||||
|
||||
const OPTIONS = window.CODE_SNIPPETS_EDIT
|
||||
|
||||
const getAddNewHeading = (snippet: Snippet): string =>
|
||||
'condition' === snippet.scope
|
||||
? __('Add New Condition', 'code-snippets')
|
||||
: __('Add New Snippet', 'code-snippets')
|
||||
|
||||
export const PageHeading: React.FC = () => {
|
||||
const { snippet, updateSnippet, setCurrentNotice } = useSnippetForm()
|
||||
|
||||
return (
|
||||
<h1>
|
||||
{snippet.id
|
||||
? <>
|
||||
{`${'condition' === snippet.scope
|
||||
? __('Edit Condition', 'code-snippets')
|
||||
: __('Edit Snippet', 'code-snippets')} `}
|
||||
|
||||
<a
|
||||
href={window.CODE_SNIPPETS?.urls.addNew}
|
||||
className="page-title-action"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
updateSnippet(({ scope }) => createSnippetObject({ scope }))
|
||||
setCurrentNotice(undefined)
|
||||
|
||||
window.document.title = window.document.title
|
||||
.replace(__('Edit Snippet', 'code-snippets'), getAddNewHeading(snippet))
|
||||
.replace(__('Edit Condition', 'code-snippets'), getAddNewHeading(snippet))
|
||||
|
||||
window.history.pushState({}, '', window.CODE_SNIPPETS?.urls.addNew)
|
||||
}}
|
||||
>
|
||||
{_x('Add New', 'snippet', 'code-snippets')}
|
||||
</a>
|
||||
</>
|
||||
: getAddNewHeading(snippet)}
|
||||
|
||||
{OPTIONS?.pageTitleActions && Object.entries(OPTIONS.pageTitleActions).map(([label, url]) =>
|
||||
<>
|
||||
<a key={label} href={url} className="page-title-action">{label}</a>{' '}
|
||||
</>
|
||||
)}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user