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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user