Файловый менеджер - Редактировать - /home/freeclou/app.optimyar.com/front-web/build/assets/resources/agGrid/js.tar
Назад
components/ConditionModal/ConditionModalButton.tsx 0000755 00000002525 15110437570 0016505 0 ustar 00 import React from 'react' import classnames from 'classnames' import { __ } from '@wordpress/i18n' import { isLicensed } from '../../utils/screen' import { isCondition } from '../../utils/snippets/snippets' import { Badge } from '../common/Badge' import { Button } from '../common/Button' import { useSnippetForm } from '../../hooks/useSnippetForm' import type { Dispatch, SetStateAction } from 'react' export interface ConditionModalButtonProps { setIsDialogOpen: Dispatch<SetStateAction<boolean>> } export const ConditionModalButton: React.FC<ConditionModalButtonProps> = ({ setIsDialogOpen }) => { const { snippet, isReadOnly } = useSnippetForm() const hasCondition = 0 !== snippet.conditionId return ( <div className={classnames('conditions-editor-open block-form-field', hasCondition ? 'has-condition' : 'no-condition')}> {isCondition(snippet) ? null : <> <h4> {__('Conditions', 'code-snippets')} <Badge name="beta" small>{__('beta', 'code-snippets')}</Badge> {!isLicensed() && <Badge name="pro" small>{__('Pro', 'code-snippets')}</Badge>} </h4> <Button large disabled={isReadOnly} onClick={() => setIsDialogOpen(true)}> <Badge name="cond" small /> {hasCondition ? __('Edit Conditions', 'code-snippets') : __('Add Conditions', 'code-snippets')} </Button> </>} </div> ) } components/EditorSidebar/actions/DeleteButton.tsx 0000755 00000003073 15110437570 0016260 0 ustar 00 import { addQueryArgs } from '@wordpress/url' import React, { useState } from 'react' import { __ } from '@wordpress/i18n' import { useRestAPI } from '../../../hooks/useRestAPI' import { Button } from '../../common/Button' import { ConfirmDialog } from '../../common/ConfirmDialog' import { useSnippetForm } from '../../../hooks/useSnippetForm' export const DeleteButton: React.FC = () => { const { snippetsAPI } = useRestAPI() const { snippet, setIsWorking, isWorking, handleRequestError } = useSnippetForm() const [isDialogOpen, setIsDialogOpen] = useState(false) return ( <> <Button id="delete-snippet" className="delete-button" disabled={isWorking} onClick={() => { setIsDialogOpen(true) }} > {__('Delete', 'code-snippets')} </Button> <ConfirmDialog open={isDialogOpen} title={__('Delete?', 'code-snippets')} confirmLabel={__('Delete', 'code-snippets')} confirmButtonClassName="is-destructive" onCancel={() => setIsDialogOpen(false)} onConfirm={() => { setIsDialogOpen(false) setIsWorking(true) snippetsAPI.delete(snippet) .then(() => { setIsWorking(false) window.location.replace(addQueryArgs(window.CODE_SNIPPETS?.urls.manage, { result: 'deleted' })) }) .catch((error: unknown) => handleRequestError(error, __('Could not delete snippet.', 'code-snippets'))) }} > <p style={{ marginBlockStart: 0 }}> {__('You are about to delete this snippet.', 'code-snippets')}{' '} {__('Are you sure?', 'code-snippets')} </p> </ConfirmDialog> </> ) } components/EditorSidebar/actions/ExportButtons.tsx 0000755 00000003164 15110437570 0016523 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { useRestAPI } from '../../../hooks/useRestAPI' import { Button } from '../../common/Button' import { downloadSnippetExportFile } from '../../../utils/files' import { useSnippetForm } from '../../../hooks/useSnippetForm' import type { Snippet } from '../../../types/Snippet' import type { SnippetsExport } from '../../../types/schema/SnippetsExport' interface ExportButtonProps { name: string label: string makeRequest: (snippet: Snippet) => Promise<SnippetsExport | string> } const ExportButton: React.FC<ExportButtonProps> = ({ name, label, makeRequest }) => { const { snippet, isWorking, setIsWorking, handleRequestError } = useSnippetForm() const handleClick = () => { setIsWorking(true) makeRequest(snippet) .then(response => downloadSnippetExportFile(response, snippet)) // translators: %s: error message. .catch((error: unknown) => handleRequestError(error, __('Could not download export file.', 'code-snippets'))) .finally(() => setIsWorking(false)) } return ( <Button name={name} onClick={handleClick} disabled={isWorking}> {label} </Button> ) } export const ExportButtons: React.FC = () => { const { snippetsAPI } = useRestAPI() return ( <div className="snippet-export-buttons"> <ExportButton name="export_snippet" label={__('Export', 'code-snippets')} makeRequest={snippetsAPI.export} /> {window.CODE_SNIPPETS_EDIT?.enableDownloads ? <ExportButton name="export_snippet_code" label={__('Export Code', 'code-snippets')} makeRequest={snippetsAPI.exportCode} /> : null} </div> ) } components/EditorSidebar/actions/ShortcodeInfo.tsx 0000755 00000010560 15110437570 0016427 0 ustar 00 import React, { useState } from 'react' import { CheckboxControl, ExternalLink, Modal } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' import { Button } from '../../common/Button' import { CopyToClipboardButton } from '../../common/CopyToClipboardButton' import type { Dispatch, SetStateAction } from 'react' type ShortcodeAtts = Record<string, unknown> const buildShortcodeTag = (tag: string, atts: ShortcodeAtts): string => `[${[ tag, ...Object.entries(atts) .filter(([, value]) => Boolean(value)) .map(([att, value]) => 'boolean' === typeof value ? att : `${att}=${JSON.stringify(value)}`) ].filter(Boolean).join(' ')}]` const SHORTCODE_TAG = 'code_snippet' interface ShortcodeOptions { php: boolean format: boolean shortcodes: boolean } interface CheckboxListProps<T extends string> { options: T[] checked: Record<T, boolean> disabled: boolean setChecked: Dispatch<SetStateAction<Record<T, boolean>>> optionLabels: Partial<Record<T, string>> optionDescriptions: Partial<Record<T, string>> } const CheckboxList = <T extends string>({ options, checked, disabled, setChecked, optionLabels, optionDescriptions }: CheckboxListProps<T>) => <ul> {options.map(option => <li key={option}> <CheckboxControl label={optionLabels[option]} help={optionDescriptions[option]} checked={checked[option]} disabled={disabled} onChange={value => setChecked(previous => ({ ...previous, [option]: value }))} /> </li>)} </ul> const ShortcodeDescription = () => <p className="description"> {__('Copy the below shortcode to insert this snippet into a post, page, or other content.', 'code-snippets')}{'\n'} {__('You can also use the Classic Editor button, Block editor (Pro) or Elementor widget (Pro).', 'code-snippets')}{'\n'} <ExternalLink href={__('https://codesnippets.pro/doc/inserting-content-snippets/', 'code-snippets')} > {__('Learn more', 'code-snippets')} </ExternalLink> </p> const OPTION_LABELS: Record<keyof ShortcodeOptions, string> = { php: __('Evaluate PHP code', 'code-snippets'), format: __('Add paragraphs and formatting', 'code-snippets'), shortcodes: __('Evaluate additional shortcode tags', 'code-snippets') } const OPTION_DESCRIPTIONS: Record<keyof ShortcodeOptions, string> = { php: __('Run code within <?php ?> tags.', 'code-snippets'), format: __('Wrap output in paragraphs and apply formatting.', 'code-snippets'), shortcodes: __('Replace [shortcodes] embedded within the snippet.', 'code-snippets') } const ModalContent = () => { const { snippet, isReadOnly } = useSnippetForm() const [options, setOptions] = useState<ShortcodeOptions>(() => ({ php: snippet.code.includes('<?'), format: true, shortcodes: false })) const shortcodeAtts: ShortcodeAtts = { id: snippet.id, network: snippet.network, ...options, name: snippet.name } const shortcodeTag = buildShortcodeTag(SHORTCODE_TAG, shortcodeAtts) return ( <> <ShortcodeDescription /> <p className="shortcode-tag-wrapper"> <code className="shortcode-tag">{shortcodeTag}</code> <CopyToClipboardButton primary text={shortcodeTag} /> </p> <p> <h4>{__('Shortcode Options', 'code-snippets')}</h4> <CheckboxList options={['php', 'format', 'shortcodes']} checked={options} disabled={isReadOnly} setChecked={setOptions} optionLabels={OPTION_LABELS} optionDescriptions={OPTION_DESCRIPTIONS} /> </p> </> ) } export const ShortcodeInfo: React.FC = () => { const { snippet, isReadOnly } = useSnippetForm() const [isModalOpen, setIsModalOpen] = useState(false) return 'content' === snippet.scope && snippet.id ? <div className="inline-form-field"> <h4>{__('Shortcode', 'code-snippets')}</h4> <Button onClick={() => setIsModalOpen(true)} disabled={isReadOnly}> {__('See options', 'code-snippets')} </Button> {isModalOpen ? <Modal size="medium" className="code-snippets-modal" title={__('Embed Snippet with Shortcode', 'code-snippets')} onRequestClose={() => setIsModalOpen(false)} > <div className="modal-content"> <ModalContent /> </div> <div className="modal-footer"> <Button link large onClick={() => setIsModalOpen(false)}> {__('Close Popup', 'code-snippets')} </Button> </div> </Modal> : null} </div> : null } components/EditorSidebar/actions/SubmitButtons.tsx 0000755 00000004301 15110437570 0016477 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { SubmitSnippetAction } from '../../../hooks/useSubmitSnippet' import { isCondition } from '../../../utils/snippets/snippets' import { isNetworkAdmin } from '../../../utils/screen' import { useSnippetForm } from '../../../hooks/useSnippetForm' import { SubmitButton } from '../../common/SubmitButton' import type { SubmitButtonProps } from '../../common/SubmitButton' const SaveButton = (props: SubmitButtonProps) => { const { snippet } = useSnippetForm() return ( <SubmitButton large name={SubmitSnippetAction.SAVE} text={isCondition(snippet) ? __('Save Condition', 'code-snippets') : __('Save Snippet', 'code-snippets')} {...props} /> ) } interface ActivateOrDeactivateButtonProps { primaryActivate: boolean } const ActivateOrDeactivateButton: React.FC<ActivateOrDeactivateButtonProps> = ({ primaryActivate }) => { const { snippet, isWorking } = useSnippetForm() switch (true) { case isCondition(snippet) || snippet.shared_network && isNetworkAdmin(): return null case 'single-use' === snippet.scope: return ( <SubmitButton large name={SubmitSnippetAction.SAVE_AND_EXECUTE} disabled={isWorking} text={__('Save and Execute Once', 'code-snippets')} /> ) case snippet.active: return ( <SubmitButton name={SubmitSnippetAction.SAVE_AND_DEACTIVATE} disabled={isWorking} large text={__('Save and Deactivate', 'code-snippets')} /> ) default: case !snippet.active: return ( <SubmitButton name={SubmitSnippetAction.SAVE_AND_ACTIVATE} primary={primaryActivate} disabled={isWorking} large text={__('Save and Activate', 'code-snippets')} /> ) } } export const SubmitButtons: React.FC = () => { const { snippet } = useSnippetForm() const activateByDefault = !!window.CODE_SNIPPETS_EDIT?.activateByDefault && !snippet.active && 'single-use' !== snippet.scope && (!snippet.shared_network || !isNetworkAdmin()) return <> {activateByDefault && <SaveButton primary={!activateByDefault} />} <ActivateOrDeactivateButton primaryActivate={activateByDefault} /> {!activateByDefault && <SaveButton primary />} </> } components/EditorSidebar/controls/ActivationSwitch.tsx 0000755 00000002033 15110437570 0017343 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' import { SubmitSnippetAction, useSubmitSnippet } from '../../../hooks/useSubmitSnippet' import { handleUnknownError } from '../../../utils/errors' export const ActivationSwitch = () => { const { snippet, isWorking } = useSnippetForm() const { submitSnippet } = useSubmitSnippet() return ( <div className="inline-form-field activation-switch-container"> <h4>{__('Status')}</h4> <label> {snippet.active ? __('Active', 'code-snippets') : __('Inactive', 'code-snippets')} <input id="activation-switch" type="checkbox" checked={snippet.active} disabled={isWorking || !!snippet.shared_network} className="switch" onChange={() => { submitSnippet(snippet.active ? SubmitSnippetAction.SAVE_AND_DEACTIVATE : SubmitSnippetAction.SAVE_AND_ACTIVATE) .then(() => undefined) .catch(handleUnknownError) }} /> </label> </div> ) } components/EditorSidebar/controls/MultisiteSharingSettings.tsx 0000755 00000002106 15110437570 0021075 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' import { Tooltip } from '../../common/Tooltip' export const MultisiteSharingSettings: React.FC = () => { const { snippet, setSnippet, isReadOnly } = useSnippetForm() return ( <div className="inline-form-field activation-switch-container"> <h4> {__('Share with Subsites', 'code-snippets')} </h4> <Tooltip inline start> {__('Instead of running on every site, allow this snippet to be activated on individual sites on the network.', 'code-snippets')} </Tooltip> <label> {snippet.shared_network ? __('Enabled', 'code-snippets') : __('Disabled', 'code-snippets')} <input id="snippet_sharing" name="snippet_sharing" type="checkbox" className="switch" checked={!!snippet.shared_network} disabled={isReadOnly} onChange={event => setSnippet(previous => ({ ...previous, active: false, shared_network: event.target.checked }))} /> </label> </div> ) } components/EditorSidebar/controls/PriorityInput.tsx 0000755 00000001556 15110437570 0016732 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' import { Tooltip } from '../../common/Tooltip' export const PriorityInput = () => { const { snippet, isReadOnly, setSnippet } = useSnippetForm() return ( <div className="snippet-priority inline-form-field"> <h4> <label htmlFor="snippet-priority"> {__('Priority', 'code-snippets')} </label> </h4> <Tooltip block end> {__('Snippets with a lower priority number will run before those with a higher number.', 'code-snippets')} </Tooltip> <input type="number" id="snippet-priority" name="snippet_priority" value={snippet.priority} disabled={isReadOnly} onChange={event => setSnippet(previous => ({ ...previous, priority: parseInt(event.target.value, 10) }))} /> </div> ) } components/EditorSidebar/controls/RTLControl.tsx 0000755 00000001305 15110437570 0016063 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' export const RTLControl: React.FC = () => { const { codeEditorInstance } = useSnippetForm() return ( <div className="inline-form-field"> <h4> <label htmlFor="snippet-code-direction"> {__('Code Direction', 'code-snippets')} </label> </h4> <select id="snippet-code-direction" onChange={event => codeEditorInstance?.codemirror.setOption('direction', 'rtl' === event.target.value ? 'rtl' : 'ltr') }> <option value="ltr">{__('LTR', 'code-snippets')}</option> <option value="rtl">{__('RTL', 'code-snippets')}</option> </select> </div> ) } components/EditorSidebar/EditorSidebar.tsx 0000755 00000003630 15110437570 0014741 0 ustar 00 import React from 'react' import { Spinner } from '@wordpress/components' import { isRTL } from '@wordpress/i18n' import { useSnippetForm } from '../../hooks/useSnippetForm' import { isNetworkAdmin } from '../../utils/screen' import { isCondition } from '../../utils/snippets/snippets' import { ConditionModalButton } from '../ConditionModal/ConditionModalButton' import { SnippetLocationInput } from '../SnippetForm/fields/SnippetLocationInput' import { Notices } from '../SnippetForm/page/Notices' import { ShortcodeInfo } from './actions/ShortcodeInfo' import { MultisiteSharingSettings } from './controls/MultisiteSharingSettings' import { ExportButtons } from './actions/ExportButtons' import { SubmitButtons } from './actions/SubmitButtons' import { ActivationSwitch } from './controls/ActivationSwitch' import { DeleteButton } from './actions/DeleteButton' import { PriorityInput } from './controls/PriorityInput' import { RTLControl } from './controls/RTLControl' import type { Dispatch, SetStateAction } from 'react' export interface EditorSidebarProps { setIsUpgradeDialogOpen: Dispatch<SetStateAction<boolean>> } export const EditorSidebar: React.FC<EditorSidebarProps> = ({ setIsUpgradeDialogOpen }) => { const { snippet, isWorking } = useSnippetForm() return ( <div className="snippet-editor-sidebar"> <div className="box"> {snippet.id && !isCondition(snippet) ? <ActivationSwitch /> : null} {isNetworkAdmin() ? <MultisiteSharingSettings /> : null} {isRTL() ? <RTLControl /> : null} <ConditionModalButton setIsDialogOpen={setIsUpgradeDialogOpen} /> <SnippetLocationInput /> <ShortcodeInfo /> <PriorityInput /> {snippet.id ? <div className="row-actions visible inline-form-field"> <ExportButtons /> <DeleteButton /> </div> : null} </div> <p className="submit"> <SubmitButtons /> {isWorking ? <Spinner /> : ''} </p> <Notices /> </div> ) } components/EditorSidebar/index.ts 0000755 00000000040 15110437570 0013130 0 ustar 00 export * from './EditorSidebar' components/SnippetForm/fields/CodeEditorShortcuts.tsx 0000755 00000005540 15110437570 0017157 0 ustar 00 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> components/SnippetForm/fields/CodeEditor.tsx 0000755 00000006027 15110437570 0015241 0 ustar 00 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> ) } components/SnippetForm/fields/DescriptionEditor.tsx 0000755 00000003555 15110437570 0016655 0 ustar 00 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 components/SnippetForm/fields/NameInput.tsx 0000755 00000001336 15110437570 0015116 0 ustar 00 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> ) } components/SnippetForm/fields/SnippetLocationInput.tsx 0000755 00000005253 15110437570 0017353 0 ustar 00 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> } components/SnippetForm/fields/SnippetTypeInput.tsx 0000755 00000007067 15110437570 0016531 0 ustar 00 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> ) } components/SnippetForm/fields/TagsEditor.tsx 0000755 00000001610 15110437570 0015256 0 ustar 00 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 } components/SnippetForm/page/Notices.tsx 0000755 00000002125 15110437570 0014265 0 ustar 00 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} </> } components/SnippetForm/page/PageHeading.tsx 0000755 00000003144 15110437570 0015017 0 ustar 00 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> ) } components/SnippetForm/index.ts 0000755 00000000036 15110437570 0012663 0 ustar 00 export * from './SnippetForm' components/SnippetForm/SnippetForm.tsx 0000755 00000014414 15110437570 0014217 0 ustar 00 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> {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> components/common/icons/CopyIcon.tsx 0000755 00000001514 15110437570 0013626 0 ustar 00 import React from 'react' export const CopyIcon = () => <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 6V4.5C12 4.10218 11.842 3.72064 11.5607 3.43934C11.2794 3.15804 10.8978 3 10.5 3H4.5C4.10218 3 3.72064 3.15804 3.43934 3.43934C3.15804 3.72064 3 4.10218 3 4.5V10.5C3 10.8978 3.15804 11.2794 3.43934 11.5607C3.72064 11.842 4.10218 12 4.5 12H6M6 7.5C6 7.10218 6.15804 6.72065 6.43934 6.43934C6.72065 6.15804 7.10218 6 7.5 6H13.5C13.8978 6 14.2794 6.15804 14.5607 6.43934C14.842 6.72065 15 7.10218 15 7.5V13.5C15 13.8978 14.842 14.2794 14.5607 14.5607C14.2794 14.842 13.8978 15 13.5 15H7.5C7.10218 15 6.72065 14.842 6.43934 14.5607C6.15804 14.2794 6 13.8978 6 13.5V7.5Z" stroke="#F0F0F0" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> components/common/icons/ExpandIcon.tsx 0000755 00000001216 15110437570 0014132 0 ustar 00 import React from 'react' export const ExpandIcon = () => <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 3H15V6" /> <path d="M10.5 7.5L15 3L10.5 7.5Z" /> <path d="M6 15H3V12" /> <path d="M3 15L7.5 10.5L3 15Z" /> <path d="M12 15H15V12" /> <path d="M10.5 10.5L15 15L10.5 10.5Z" /> <path d="M6 3H3V6" /> <path d="M3 3L7.5 7.5L3 3Z" /> <path d="M12 3H15M15 3V6M15 3L10.5 7.5M6 15H3M3 15V12M3 15L7.5 10.5M12 15H15M15 15V12M15 15L10.5 10.5M6 3H3M3 3V6M3 3L7.5 7.5" stroke="currentcolor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> components/common/icons/MinimiseIcon.tsx 0000755 00000001530 15110437570 0014464 0 ustar 00 import React from 'react' export const MinimiseIcon = () => <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M3.75 6.75H6.75V3.75" /> <path d="M2.25 2.25L6.75 6.75L2.25 2.25Z" /> <path d="M3.75 11.25H6.75V14.25" /> <path d="M2.25 15.75L6.75 11.25L2.25 15.75Z" /> <path d="M14.25 6.75H11.25V3.75" /> <path d="M11.25 6.75L15.75 2.25L11.25 6.75Z" /> <path d="M14.25 11.25H11.25V14.25" /> <path d="M11.25 11.25L15.75 15.75L11.25 11.25Z" /> <path d="M3.75 6.75H6.75M6.75 6.75V3.75M6.75 6.75L2.25 2.25M3.75 11.25H6.75M6.75 11.25V14.25M6.75 11.25L2.25 15.75M14.25 6.75H11.25M11.25 6.75V3.75M11.25 6.75L15.75 2.25M14.25 11.25H11.25M11.25 11.25V14.25M11.25 11.25L15.75 15.75" stroke="currentcolor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> components/common/Badge.tsx 0000755 00000001506 15110437570 0011773 0 ustar 00 import React from 'react' import classnames from 'classnames' import type { ReactNode } from 'react' import type { SnippetType } from '../../types/Snippet' export type BadgeName = SnippetType | 'core' | 'pro' | 'ai' | 'cloud' | 'bundles' | 'cloud_search' | 'beta' const badgeIcons: Partial<Record<BadgeName, string>> = { cond: 'randomize', cloud: 'cloud', bundles: 'screenoptions', cloud_search: 'search' } export interface BadgeProps { name: BadgeName small?: boolean inverted?: boolean children?: ReactNode } export const Badge: React.FC<BadgeProps> = ({ name, small, inverted, children }) => <span className={classnames('badge', `${name}-badge`, { 'small-badge': small, 'inverted-badge': inverted })}> {badgeIcons[name] ? <span className={`dashicons dashicons-${badgeIcons[name]}`} /> : children ?? name} </span> components/common/Button.tsx 0000755 00000001641 15110437570 0012244 0 ustar 00 import React from 'react' import classnames from 'classnames' import type { ButtonHTMLAttributes } from 'react' export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'id' | 'name'> { id?: string name?: string primary?: boolean secondary?: boolean small?: boolean large?: boolean link?: boolean } export const Button: React.FC<ButtonProps> = ({ id, children, className, name, primary = false, secondary = false, small = false, large = false, link = false, type = 'button', onClick, ...props }) => <button id={id ?? name} name={name} type={type} {...props} onClick={event => { if (onClick) { event.preventDefault() onClick(event) } }} className={classnames('button', className, { 'button-primary': primary, 'button-secondary': secondary, 'button-large': large, 'button-small': small, 'button-link': link })} > {children} </button> components/common/ConfirmDialog.tsx 0000755 00000002144 15110437570 0013505 0 ustar 00 import React from 'react' import { __ } from '@wordpress/i18n' import { Button, Flex, Modal } from '@wordpress/components' import type { ReactNode } from 'react' export interface ConfirmDialogProps { open?: boolean title: string onConfirm?: VoidFunction onCancel: VoidFunction confirmLabel?: string cancelLabel?: string children?: ReactNode, confirmButtonClassName?: string } export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ open, title, onConfirm, onCancel, children, confirmLabel = __('OK', 'code-snippets'), cancelLabel = __('Cancel', 'code-snippets'), confirmButtonClassName }) => open ? <Modal title={title} onRequestClose={onCancel} closeButtonLabel={cancelLabel} isDismissible={true} onKeyDown={event => { if ('Enter' === event.key) { onConfirm?.() } }} > {children} <Flex direction="row" justify="flex-end"> <Button variant="tertiary" onClick={onCancel}> {cancelLabel} </Button> <Button variant="primary" onClick={onConfirm} className={confirmButtonClassName}> {confirmLabel} </Button> </Flex> </Modal> : null components/common/CopyToClipboardButton.tsx 0000755 00000003217 15110437570 0015223 0 ustar 00 import { Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' import React, { useState } from 'react' import { Button } from './Button' import { CopyIcon } from './icons/CopyIcon' import type { ButtonProps } from './Button' const TIMEOUT = 1500 enum Status { INITIAL, PROGRESSING, SUCCESS, ERROR } interface StatusIconProps { status: Status } const StatusIcon: React.FC<StatusIconProps> = ({ status }) => { switch (status) { case Status.INITIAL: return <CopyIcon /> case Status.PROGRESSING: return <span className="spinner-wrapper"><Spinner /></span> case Status.SUCCESS: return <span className="dashicons dashicons-yes"></span> case Status.ERROR: return <span className="dashicons dashicons-warning"></span> } } export interface CopyToClipboardButtonProps extends ButtonProps { text: string timeout?: number } export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({ text, timeout = TIMEOUT, ...props }) => { const [status, setStatus] = useState(Status.INITIAL) const clipboard = window.navigator.clipboard as Clipboard | undefined const handleClick = () => { setStatus(Status.PROGRESSING) clipboard?.writeText(text) .then(() => { setStatus(Status.SUCCESS) setTimeout(() => setStatus(Status.INITIAL), timeout) }) .catch((error: unknown) => { console.error('Failed to copy text to clipboard.', error) setStatus(Status.ERROR) }) } return clipboard && window.isSecureContext ? <Button className="code-snippets-copy-text" onClick={handleClick} {...props} > <StatusIcon status={status} /> {__('Copy', 'code-snippets')} </Button> : null } components/common/DismissableNotice.tsx 0000755 00000001274 15110437570 0014374 0 ustar 00 import { __ } from '@wordpress/i18n' import classnames from 'classnames' import React from 'react' import type { ReactNode } from 'react' export interface DismissibleNoticeProps { className?: classnames.Argument onDismiss: VoidFunction children?: ReactNode } export const DismissibleNotice: React.FC<DismissibleNoticeProps> = ({ className, onDismiss, children }) => <div id="message" className={classnames('notice fade is-dismissible', className)}> <>{children}</> <button type="button" className="notice-dismiss" onClick={event => { event.preventDefault() onDismiss() }}> <span className="screen-reader-text">{__('Dismiss notice.', 'code-snippets')}</span> </button> </div> components/common/SubmitButton.tsx 0000755 00000001621 15110437570 0013426 0 ustar 00 import React from 'react' import classnames from 'classnames' import { __ } from '@wordpress/i18n' import type { InputHTMLAttributes } from 'react' export interface SubmitButtonProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'name' | 'value'> { id?: string name?: string primary?: boolean small?: boolean large?: boolean wrap?: boolean text?: string } export const SubmitButton: React.FC<SubmitButtonProps> = ({ id, text, name = 'submit', primary, small, large, wrap, className, ...inputProps }) => { const button = <input id={id ?? name} type="submit" name={name} value={text ?? __('Save Changes', 'code-snippets')} className={classnames( 'button', { 'button-primary': primary, 'button-small': small, 'button-large': large }, className )} {...inputProps} /> return wrap ? <p className="submit">{button}</p> : button } components/common/Tooltip.tsx 0000755 00000001251 15110437570 0012420 0 ustar 00 import React from 'react' import classnames from 'classnames' import type { ReactNode } from 'react' export interface TooltipProps { block?: boolean inline?: boolean start?: boolean end?: boolean icon?: ReactNode children: ReactNode className?: classnames.Argument } export const Tooltip: React.FC<TooltipProps> = ({ block, inline, start, end, icon, className, children }) => <div className={classnames( 'tooltip', { 'tooltip-block': block, 'tooltip-inline': inline, 'tooltip-start': start, 'tooltip-end': end }, className )}> {icon ?? <span className="dashicons dashicons-editor-help"></span>} <div className="tooltip-content"> {children} </div> </div> components/common/UpsellBanner.tsx 0000755 00000002243 15110437570 0013362 0 ustar 00 import { ExternalLink } from '@wordpress/components' import { createInterpolateElement } from '@wordpress/element' import { __ } from '@wordpress/i18n' import React, { useState } from 'react' import { isLicensed } from '../../utils/screen' import { Button } from './Button' export const UpsellBanner = () => { const [isDismissed, setIsDismissed] = useState(false) return isDismissed || isLicensed() || window.CODE_SNIPPETS_EDIT?.hideUpsell ? null : <div className="code-snippets-upsell-banner"> <img src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`} alt={__('Code Snippets logo', 'code-snippets')} height="34" /> <p> {createInterpolateElement( __('Unlock <strong>cloud sync, snippet conditions, AI features</strong> and much more with Code Snippets Pro.', 'code-snippets'), { strong: <strong /> } )} </p> <ExternalLink className="button button-primary button-large" href="https://codesnippets.pro/pricing/" > {__('Get Started', 'code-snippets')} </ExternalLink> <Button small link onClick={() => setIsDismissed(true)}> <span className="dashicons dashicons-no-alt"></span> </Button> </div> } components/common/UpsellDialog.tsx 0000755 00000004165 15110437570 0013361 0 ustar 00 import { createInterpolateElement } from '@wordpress/element' import React from 'react' import { __ } from '@wordpress/i18n' import { Modal } from '@wordpress/components' import type { Dispatch, SetStateAction } from 'react' export interface UpsellDialogProps { isOpen: boolean setIsOpen: Dispatch<SetStateAction<boolean>> } export const UpsellDialog: React.FC<UpsellDialogProps> = ({ isOpen, setIsOpen }) => isOpen ? <Modal title="" className="code-snippets-upsell-dialog" onRequestClose={() => { setIsOpen(false) }} > <img src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`} alt={__('Code Snippets logo', 'code-snippets')} /> <h1> {createInterpolateElement( __('Unlock all cloud sync features and many more, with <span>Code Snippets Pro</span>', 'code-snippets'), { span: <span /> } )} </h1> <p> {createInterpolateElement( __('With Code Snippets Pro you can connect your WordPress sites to the code snippets cloud platform and be able to <strong>backup, synchronise, collaborate, and deploy</strong> your snippets from one central location.', 'code-snippets'), { strong: <strong /> } )} </p> <a href="https://codesnippets.pro/pricing/" className="button button-primary button-large" rel="noreferrer" target="_blank" > {__('Explore Code Snippets Pro', 'code-snippets')} </a> <h2>{__("Here's what else you get with Pro:", 'code-snippets')}</h2> <ul> <li>{__('Create, explain and verify snippets with AI', 'code-snippets')}</li> <li>{__('Control when snippets run with Conditions', 'code-snippets')}</li> <li>{__('CSS stylesheet snippets', 'code-snippets')}</li> <li>{__('Minified JavaScript snippets', 'code-snippets')}</li> <li>{__('Editor blocks and Elementor widgets', 'code-snippets')}</li> <li>{__('Cloud sync and backup', 'code-snippets')}</li> <li>{__('Cloud share and deploy', 'code-snippets')}</li> <li>{__('Cloud bundles and teams', 'code-snippets')}</li> <li>{__('WP-CLI commands', 'code-snippets')}</li> <li>{__('And much more!', 'code-snippets')}</li> </ul> </Modal> : null hooks/useRestAPI.tsx 0000755 00000004310 15110437570 0010417 0 ustar 00 import React, { useMemo } from 'react' import axios from 'axios' import { createContextHook } from '../utils/hooks' import { REST_API_AXIOS_CONFIG } from '../utils/restAPI' import { buildSnippetsAPI } from '../utils/snippets/api' import type { SnippetsAPI } from '../utils/snippets/api' import type { PropsWithChildren } from 'react' import type { AxiosInstance, AxiosResponse } from 'axios' export interface RestAPIContext { api: RestAPI snippetsAPI: SnippetsAPI axiosInstance: AxiosInstance } export interface RestAPI { get: <T>(url: string) => Promise<T> post: <T>(url: string, data?: object) => Promise<T> put: <T>(url: string, data?: object) => Promise<T> del: <T>(url: string) => Promise<T> } const debugRequest = async <T, D = never>( method: 'GET' | 'POST' | 'PUT' | 'DELETE', url: string, doRequest: Promise<AxiosResponse<T, D>>, data?: D ): Promise<T> => { console.debug(`${method} ${url}`, ...data ? [data] : []) const response = await doRequest console.debug('Response', response) return response.data } const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({ get: <T, >(url: string): Promise<T> => debugRequest('GET', url, axiosInstance.get<T, AxiosResponse<T, never>, never>(url)), post: <T, >(url: string, data?: object): Promise<T> => debugRequest('POST', url, axiosInstance.post<T, AxiosResponse<T, typeof data>, typeof data>(url, data), data), del: <T, >(url: string): Promise<T> => debugRequest('DELETE', url, axiosInstance.delete<T, AxiosResponse<T, never>, never>(url)), put: <T, >(url: string, data?: object): Promise<T> => debugRequest('PUT', url, axiosInstance.put<T, AxiosResponse<T, typeof data>, typeof data>(url, data), data) }) export const [RestAPIContext, useRestAPI] = createContextHook<RestAPIContext>('RestAPI') export const WithRestAPIContext: React.FC<PropsWithChildren> = ({ children }) => { const axiosInstance = useMemo(() => axios.create(REST_API_AXIOS_CONFIG), []) const api = useMemo(() => buildRestAPI(axiosInstance), [axiosInstance]) const snippetsAPI = useMemo(() => buildSnippetsAPI(api), [api]) const value: RestAPIContext = { api, snippetsAPI, axiosInstance } return <RestAPIContext.Provider value={value}>{children}</RestAPIContext.Provider> } hooks/useSnippetForm.tsx 0000755 00000005310 15110437570 0011417 0 ustar 00 import { isAxiosError } from 'axios' import React, { useCallback, useMemo, useState } from 'react' import { createContextHook } from '../utils/hooks' import { isLicensed } from '../utils/screen' import { isProSnippet } from '../utils/snippets/snippets' import type { Dispatch, PropsWithChildren, SetStateAction } from 'react' import type { ScreenNotice } from '../types/ScreenNotice' import type { Snippet } from '../types/Snippet' import type { CodeEditorInstance } from '../types/WordPressCodeEditor' export interface SnippetFormContext { snippet: Snippet isWorking: boolean isReadOnly: boolean setSnippet: Dispatch<SetStateAction<Snippet>> updateSnippet: Dispatch<SetStateAction<Snippet>> setIsWorking: Dispatch<SetStateAction<boolean>> currentNotice: ScreenNotice | undefined setCurrentNotice: Dispatch<SetStateAction<ScreenNotice | undefined>> codeEditorInstance: CodeEditorInstance | undefined handleRequestError: (error: unknown, message?: string) => void setCodeEditorInstance: Dispatch<SetStateAction<CodeEditorInstance | undefined>> } export const [SnippetFormContext, useSnippetForm] = createContextHook<SnippetFormContext>('SnippetForm') export interface WithSnippetFormContextProps extends PropsWithChildren { initialSnippet: () => Snippet } export const WithSnippetFormContext: React.FC<WithSnippetFormContextProps> = ({ children, initialSnippet }) => { const [snippet, setSnippet] = useState<Snippet>(initialSnippet) const [isWorking, setIsWorking] = useState(false) const [currentNotice, setCurrentNotice] = useState<ScreenNotice>() const [codeEditorInstance, setCodeEditorInstance] = useState<CodeEditorInstance>() const isReadOnly = useMemo(() => !isLicensed() && isProSnippet({ scope: snippet.scope }), [snippet.scope]) const handleRequestError = useCallback((error: unknown, message?: string) => { console.error('Request failed', error) setIsWorking(false) setCurrentNotice(['error', [message, isAxiosError(error) ? error.message : ''].filter(Boolean).join(' ')]) }, [setIsWorking, setCurrentNotice]) const updateSnippet: Dispatch<SetStateAction<Snippet>> = useCallback((value: SetStateAction<Snippet>) => { setSnippet(previous => { const updated = 'object' === typeof value ? value : value(previous) codeEditorInstance?.codemirror.setValue(updated.code) window.tinymce?.activeEditor.setContent(updated.desc) return updated }) }, [codeEditorInstance?.codemirror]) const value: SnippetFormContext = { snippet, isWorking, isReadOnly, setSnippet, setIsWorking, updateSnippet, currentNotice, setCurrentNotice, codeEditorInstance, handleRequestError, setCodeEditorInstance } return <SnippetFormContext.Provider value={value}>{children}</SnippetFormContext.Provider> } hooks/useSnippetsList.tsx 0000755 00000002505 15110437570 0011615 0 ustar 00 import React, { useCallback, useEffect, useState } from 'react' import { createContextHook } from '../utils/hooks' import { isNetworkAdmin } from '../utils/screen' import { useRestAPI } from './useRestAPI' import type { PropsWithChildren } from 'react' import type { Snippet } from '../types/Snippet' export interface SnippetsListContext { snippetsList: readonly Snippet[] | undefined refreshSnippetsList: () => Promise<void> } const [SnippetsListContext, useSnippetsList] = createContextHook<SnippetsListContext>('SnippetsList') export const WithSnippetsListContext: React.FC<PropsWithChildren> = ({ children }) => { const { snippetsAPI: { fetchAll } } = useRestAPI() const [snippetsList, setSnippetsList] = useState<Snippet[]>() const refreshSnippetsList = useCallback(async (): Promise<void> => { try { console.info('Fetching snippets list') const response = await fetchAll(isNetworkAdmin()) setSnippetsList(response) } catch (error: unknown) { console.error('Error fetching snippets list', error) } }, [fetchAll]) useEffect(() => { refreshSnippetsList() .catch(() => undefined) }, [refreshSnippetsList]) const value: SnippetsListContext = { snippetsList, refreshSnippetsList } return <SnippetsListContext.Provider value={value}>{children}</SnippetsListContext.Provider> } export { useSnippetsList } hooks/useSubmitSnippet.ts 0000755 00000011374 15110437570 0011576 0 ustar 00 import { __ } from '@wordpress/i18n' import { addQueryArgs } from '@wordpress/url' import { isAxiosError } from 'axios' import { useCallback } from 'react' import { createSnippetObject, isCondition } from '../utils/snippets/snippets' import { useRestAPI } from './useRestAPI' import { useSnippetForm } from './useSnippetForm' import type { Snippet } from '../types/Snippet' const snippetMessages = <const> { addNew: __('Add New Snippet', 'code-snippets'), edit: __('Edit Snippet', 'code-snippets'), created: __('Snippet <strong>created</strong>.', 'code-snippets'), updated: __('Snippet <strong>updated</strong>.', 'code-snippets'), createdActivated: __('Snippet <strong>created</strong> and <strong>activated</strong>.', 'code-snippets'), updatedActivated: __('Snippet <strong>updated</strong> and <strong>activated</strong>.', 'code-snippets'), updatedDeactivated: __('Snippet <strong>updated</strong> and <strong>deactivated</strong>'), updatedExecuted: __('Snippet <strong>updated</strong> and <strong>executed</strong>.', 'code-snippets'), failedCreate: __('Could not create snippet.', 'code-snippets'), failedUpdate: __('Could not update snippet.', 'code-snippets') } const conditionCreated = __('Condition <strong>created</strong>.', 'code-snippets') const conditionUpdated = __('Condition <strong>updated</strong>.', 'code-snippets') const conditionMessages: typeof snippetMessages = { addNew: __('Add New Condition', 'code-snippets'), edit: __('Edit Condition', 'code-snippets'), created: conditionCreated, updated: conditionUpdated, createdActivated: conditionCreated, updatedActivated: conditionUpdated, updatedDeactivated: conditionUpdated, updatedExecuted: conditionUpdated, failedCreate: __('Could not create condition.', 'code-snippets'), failedUpdate: __('Could not update condition.', 'code-snippets') } export enum SubmitSnippetAction { SAVE = 'save_snippet', SAVE_AND_ACTIVATE = 'save_snippet_activate', SAVE_AND_EXECUTE = 'save_snippet_execute', SAVE_AND_DEACTIVATE = 'save_snippet_deactivate' } const getSuccessNotice = (request: Snippet, response: Snippet, action: SubmitSnippetAction): string => { const messages = 'condition' === request.scope ? conditionMessages : snippetMessages const wasCreated = 0 === request.id switch (action) { case SubmitSnippetAction.SAVE: return wasCreated ? messages.created : messages.updated case SubmitSnippetAction.SAVE_AND_EXECUTE: return messages.updatedExecuted case SubmitSnippetAction.SAVE_AND_ACTIVATE: if ('single-use' === response.scope) { return messages.updatedExecuted } else { return wasCreated ? messages.createdActivated : messages.updatedActivated } case SubmitSnippetAction.SAVE_AND_DEACTIVATE: return messages.updatedDeactivated } } const SUBMIT_ACTION_DELTA: Record<SubmitSnippetAction, Partial<Snippet>> = { [SubmitSnippetAction.SAVE]: {}, [SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true }, [SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false }, [SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true } } export interface UseSubmitSnippet { submitSnippet: (action?: SubmitSnippetAction) => Promise<Snippet | undefined> } export const useSubmitSnippet = (): UseSubmitSnippet => { const { snippetsAPI } = useRestAPI() const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm() const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => { setCurrentNotice(undefined) const result = await (async (): Promise<Snippet | string | undefined> => { try { const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] } const response = await (0 === request.id ? snippetsAPI.create(request) : snippetsAPI.update(request)) setIsWorking(false) return response.id ? response : undefined } catch (error) { setIsWorking(false) return isAxiosError(error) ? error.message : undefined } })() const messages = isCondition(snippet) ? conditionMessages : snippetMessages if (undefined === result || 'string' === typeof result) { const message = [ snippet.id ? messages.failedUpdate : messages.failedCreate, result ?? __('The server did not send a valid response.', 'code-snippets') ] setCurrentNotice(['error', message.filter(Boolean).join(' ')]) return undefined } else { setSnippet(createSnippetObject(result)) setCurrentNotice(['updated', getSuccessNotice(snippet, result, action)]) if (snippet.id && result.id) { window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit) window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id })) } return result } }, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet]) return { submitSnippet } } services/manage/activation.ts 0000755 00000004474 15110437570 0012347 0 ustar 00 import { __ } from '@wordpress/i18n' import { updateSnippet } from './requests' import type { Snippet } from '../../types/Snippet' /** * Update the snippet count of a specific view * @param element * @param increment */ const updateViewCount = (element: HTMLElement | null, increment: boolean) => { if (element?.textContent) { let count = parseInt(element.textContent.replace(/\((?<count>\d+)\)/, '$1'), 10) count += increment ? 1 : -1 element.textContent = `(${count})` } else { console.error('Could not update view count.', element) } } /** * Activate an inactive snippet, or deactivate an active snippet * @param link * @param event */ export const toggleSnippetActive = (link: HTMLAnchorElement, event: Event) => { const row = link.parentElement?.parentElement // Switch < cell < row if (!row) { console.error('Could not toggle snippet active status.', row) return } const match = /\b(?:in)?active-snippet\b/.exec(row.className) if (!match) { return } event.preventDefault() const activating = 'inactive-snippet' === match[0] const snippet: Partial<Snippet> = { active: activating } updateSnippet('active', row, snippet, response => { const button: HTMLAnchorElement | null = row.querySelector('.snippet-activation-switch') if (response.success) { row.className = activating ? row.className.replace(/\binactive-snippet\b/, 'active-snippet') : row.className.replace(/\bactive-snippet\b/, 'inactive-snippet') const views = document.querySelector('.subsubsub') const activeCount = views?.querySelector<HTMLElement>('.active .count') const inactiveCount = views?.querySelector<HTMLElement>('.inactive .count') if (activeCount) { updateViewCount(activeCount, activating) } if (inactiveCount) { updateViewCount(inactiveCount, activating) } if (button) { button.title = activating ? __('Deactivate', 'code-snippets') : __('Activate', 'code-snippets') } } else { row.className += ' erroneous-snippet' if (button) { button.title = __('An error occurred when attempting to activate', 'code-snippets') } } }) } export const handleSnippetActivationSwitches = () => { for (const link of document.getElementsByClassName('snippet-activation-switch')) { link.addEventListener('click', event => toggleSnippetActive(<HTMLAnchorElement> link, event)) } } services/manage/cloud.ts 0000755 00000003002 15110437570 0011276 0 ustar 00 import Prism from 'prismjs' import 'prismjs/components/prism-clike' import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-css' import 'prismjs/components/prism-php' import 'prismjs/components/prism-markup' import 'prismjs/plugins/keep-markup/prism-keep-markup' /** * Handle clicks on snippet preview button. */ export const handleShowCloudPreview = () => { const previewButtons = document.querySelectorAll('.cloud-snippet-preview') previewButtons.forEach(button => { button.addEventListener('click', () => { const snippetId = button.getAttribute('data-snippet') const snippetLanguage = button.getAttribute('data-lang') const snippetCodeInput = <HTMLInputElement | null> document.getElementById(`cloud-snippet-code-${snippetId}`) const snippetCodeModalTag = document.getElementById('snippet-code-thickbox') if (!snippetCodeModalTag || !snippetCodeInput) { return } snippetCodeModalTag.classList.remove(...snippetCodeModalTag.classList) snippetCodeModalTag.classList.add(`language-${snippetLanguage}`) snippetCodeModalTag.textContent = snippetCodeInput.value if ('markup' === snippetLanguage) { snippetCodeModalTag.innerHTML = `<xmp>${snippetCodeInput.value}</xmp>` } if ('php' === snippetLanguage) { // Check if there is an opening php tag if not add it. if (!snippetCodeInput.value.startsWith('<?php')) { snippetCodeModalTag.textContent = `<?php\n${snippetCodeInput.value}` } } Prism.highlightElement(snippetCodeModalTag) }) }) } services/manage/index.ts 0000755 00000000252 15110437570 0011303 0 ustar 00 export { handleSnippetActivationSwitches } from './activation' export { handleSnippetPriorityChanges } from './priority' export { handleShowCloudPreview } from './cloud' services/manage/priority.ts 0000755 00000001344 15110437570 0012060 0 ustar 00 import { updateSnippet } from './requests' import type { Snippet } from '../../types/Snippet' /** * Update the priority of a snippet */ export const updateSnippetPriority = (element: HTMLInputElement) => { const row = element.parentElement?.parentElement const snippet: Partial<Snippet> = { priority: parseFloat(element.value) } if (row) { updateSnippet('priority', row, snippet) } else { console.error('Could not update snippet information.', snippet, row) } } export const handleSnippetPriorityChanges = () => { for (const field of <HTMLCollectionOf<HTMLInputElement>> document.getElementsByClassName('snippet-priority')) { field.addEventListener('input', () => updateSnippetPriority(field)) field.disabled = false } } services/manage/requests.ts 0000755 00000003530 15110437570 0012051 0 ustar 00 import { isNetworkAdmin } from '../../utils/screen' import type { SnippetSchema } from '../../types/schema/SnippetSchema' import type { Snippet, SnippetScope } from '../../types/Snippet' export interface ResponseData<T = unknown> { success: boolean data?: T } export type SuccessCallback = (response: ResponseData) => void const sendSnippetRequest = (query: string, onSuccess?: SuccessCallback) => { const request = new XMLHttpRequest() request.open('POST', window.ajaxurl, true) request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8') request.onload = () => { const success = 200 const errorStart = 400 if (success > request.status || errorStart <= request.status) { return } console.info(request.responseText) onSuccess?.(<ResponseData> JSON.parse(request.responseText)) } request.send(query) } /** * Update the data of a given snippet using AJAX * @param field * @param row * @param snippet * @param successCallback */ export const updateSnippet = (field: keyof Snippet, row: Element, snippet: Partial<Snippet>, successCallback?: SuccessCallback) => { const nonce = <HTMLInputElement | null> document.getElementById('code_snippets_ajax_nonce') const columnId = row.querySelector('.column-id') if (!nonce || !columnId?.textContent || !parseInt(columnId.textContent, 10)) { return } const updatedSnippet: Partial<SnippetSchema> = { id: parseInt(columnId.textContent, 10), shared_network: null !== /\bshared-network-snippet\b/.exec(row.className), network: snippet.shared_network ?? isNetworkAdmin(), scope: <SnippetScope | null> row.getAttribute('data-snippet-scope') ?? snippet.scope, ...snippet } const queryString = `action=update_code_snippet&_ajax_nonce=${nonce.value}&field=${field}&snippet=${JSON.stringify(updatedSnippet)}` sendSnippetRequest(queryString, successCallback) } services/settings/editor-preview.ts 0000755 00000003210 15110437570 0013546 0 ustar 00 import '../../editor' const parseSelect = (select: HTMLSelectElement) => select.options[select.selectedIndex].value const parseCheckbox = (checkbox: HTMLInputElement) => checkbox.checked const parseNumber = (input: HTMLInputElement) => parseInt(input.value, 10) const initialiseCodeMirror = () => { const { codeEditor } = window.wp const textarea = document.getElementById('code_snippets_editor_preview') if (textarea) { window.code_snippets_editor_preview = codeEditor.initialize(textarea) return window.code_snippets_editor_preview.codemirror } console.error('Could not initialise CodeMirror on textarea.', textarea) return undefined } export const handleEditorPreviewUpdates = () => { const editor = initialiseCodeMirror() const editorSettings = window.code_snippets_editor_settings for (const setting of editorSettings) { const element = document.querySelector(`[name="code_snippets_settings[editor][${setting.name}]"]`) element?.addEventListener('change', () => { const opt = setting.codemirror const value = (() => { switch (setting.type) { case 'select': return parseSelect(<HTMLSelectElement> element) case 'checkbox': return parseCheckbox(<HTMLInputElement> element) case 'number': return parseNumber(<HTMLInputElement> element) default: return null } })() if (null !== value) { if ('font_size' === setting.name) { const codeElement = document.querySelector('.CodeMirror-code') if (codeElement && codeElement instanceof HTMLElement) { codeElement.style.fontSize = `${value}px` } } else { editor?.setOption(opt, value) } } }) } } services/settings/index.ts 0000755 00000000230 15110437570 0011707 0 ustar 00 export { handleSettingsTabs } from './tabs' export { handleEditorPreviewUpdates } from './editor-preview' export { initVersionSwitch } from './version' services/settings/tabs.ts 0000755 00000003232 15110437570 0011536 0 ustar 00 const selectTab = (tabsWrapper: Element, tab: Element, section: string) => { // Swap the active tab class from the previously active tab to the current one. tabsWrapper.querySelector('.nav-tab-active')?.classList.remove('nav-tab-active') tab.classList.add('nav-tab-active') // Update the current active tab attribute so that only the active tab is displayed. tabsWrapper.closest('.wrap')?.setAttribute('data-active-tab', section) } // Refresh the editor preview if we're viewing the editor section. const refreshEditorPreview = (section: string) => { if ('editor' === section) { window.code_snippets_editor_preview?.codemirror.refresh() } } // Update the http referer value so that any redirections lead back to this tab. const updateHttpReferer = (section: string) => { const httpReferer = document.querySelector<HTMLInputElement>('input[name=_wp_http_referer]') if (!httpReferer) { console.error('could not find http referer') return } const newReferer = httpReferer.value.replace(/(?<base>[&?]section=)[^&]+/, `$1${section}`) httpReferer.value = newReferer + (newReferer === httpReferer.value ? `§ion=${section}` : '') } export const handleSettingsTabs = () => { const tabsWrapper = document.getElementById('settings-sections-tabs') if (!tabsWrapper) { console.error('Could not find snippets tabs') return } const tabs = tabsWrapper.querySelectorAll('.nav-tab') for (const tab of tabs) { tab.addEventListener('click', event => { event.preventDefault() const section = tab.getAttribute('data-section') if (section) { selectTab(tabsWrapper, tab, section) refreshEditorPreview(section) updateHttpReferer(section) } }) } } services/settings/version.ts 0000755 00000012176 15110437570 0012301 0 ustar 00 // Handles version switching UI on the settings screen. // Exported init function so callers can opt-in like other settings modules. // Uses vanilla DOM APIs and the global `code_snippets_version_switch` config // injected by PHP via wp_add_inline_script. interface VersionConfig { ajaxurl?: string nonce_switch?: string nonce_refresh?: string } interface AjaxResponse { success?: boolean data?: { message?: string } } declare global { interface Window { code_snippets_version_switch?: VersionConfig __code_snippets_i18n?: Record<string, string> } } const el = (id: string): HTMLElement | null => document.getElementById(id) const getConfig = (): VersionConfig => { const w = <{ code_snippets_version_switch?: VersionConfig }><unknown>window return w.code_snippets_version_switch ?? {} } const getCurrentVersion = (): string => (document.querySelector('.current-version')?.textContent ?? '').trim() const getI18n = (key: string, fallback: string): string => window.__code_snippets_i18n?.[key] ?? fallback const bindDropdown = ( dropdown: HTMLSelectElement, button: HTMLButtonElement | null, currentVersion: string, ): void => { dropdown.addEventListener('change', (): void => { const selectedVersion = dropdown.value if (!button) { return } if (!selectedVersion || selectedVersion === currentVersion) { button.disabled = true const warn = el('version-switch-warning') if (warn) { warn.setAttribute('style', 'display: none;') } } else { button.disabled = false const warn = el('version-switch-warning') if (warn) { warn.setAttribute('style', '') } } }) } const SUCCESS_RELOAD_MS = 3000 const postForm = async (data: Record<string, string>, cfg: VersionConfig): Promise<AjaxResponse> => { const body = new URLSearchParams() Object.keys(data).forEach(k => body.append(k, data[k])) const resp = await fetch(cfg.ajaxurl ?? '/wp-admin/admin-ajax.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: body.toString(), credentials: 'same-origin', }) const json = <AjaxResponse> await resp.json() return json } const bindSwitch = ( button: HTMLButtonElement, dropdown: HTMLSelectElement, result: HTMLDivElement, cfg: VersionConfig, currentVersion: string, ): void => { button.addEventListener('click', (): void => { void (async (): Promise<void> => { const targetVersion = dropdown.value if (!targetVersion || targetVersion === currentVersion) { result.className = 'notice notice-warning' result.innerHTML = `<p>${getI18n('selectDifferent', 'Please select a different version to switch to.')}</p>` result.style.display = '' return } button.disabled = true const originalText = button.textContent ?? '' button.textContent = getI18n('switching', 'Switching...') result.className = 'notice notice-info' result.innerHTML = `<p>${getI18n('processing', 'Processing version switch. Please wait...')}</p>` result.style.display = '' try { const response = await postForm({ action: 'code_snippets_switch_version', target_version: targetVersion, nonce: cfg.nonce_switch ?? '', }, cfg) if (response.success) { result.className = 'notice notice-success' result.innerHTML = `<p>${response.data?.message ?? ''}</p>` setTimeout(() => window.location.reload(), SUCCESS_RELOAD_MS) return } result.className = 'notice notice-error' result.innerHTML = `<p>${response.data?.message ?? getI18n('error', 'An error occurred.')}</p>` button.disabled = false button.textContent = originalText } catch (_err) { result.className = 'notice notice-error' result.innerHTML = `<p>${getI18n('errorSwitch', 'An error occurred while switching versions. Please try again.')}</p>` button.disabled = false button.textContent = originalText } })() }) } const REFRESH_RELOAD_MS = 1000 const bindRefresh = ( btn: HTMLButtonElement, cfg: VersionConfig, ): void => { btn.addEventListener('click', (): void => { void (async (): Promise<void> => { const original = btn.textContent ?? '' btn.disabled = true btn.textContent = getI18n('refreshing', 'Refreshing...') try { await postForm({ action: 'code_snippets_refresh_versions', nonce: cfg.nonce_refresh ?? '', }, cfg) btn.textContent = getI18n('refreshed', 'Refreshed!') setTimeout(() => { btn.disabled = false btn.textContent = original window.location.reload() }, REFRESH_RELOAD_MS) } catch { btn.disabled = false btn.textContent = original } })() }) } export const initVersionSwitch = (): void => { const cfg = getConfig() const currentVersion = getCurrentVersion() const button = <HTMLButtonElement | null> el('switch-version-btn') const dropdown = <HTMLSelectElement | null> el('target_version') const result = <HTMLDivElement | null> el('version-switch-result') const refreshBtn = <HTMLButtonElement | null> el('refresh-versions-btn') if (dropdown) { bindDropdown(dropdown, button, currentVersion) } if (button && dropdown && result) { bindSwitch(button, dropdown, result, cfg, currentVersion) } if (refreshBtn) { bindRefresh(refreshBtn, cfg) } } types/schema/SnippetSchema.ts 0000755 00000000744 15110437570 0012276 0 ustar 00 import type { SnippetScope } from '../Snippet' export interface WritableSnippetSchema { name?: string desc?: string code?: string tags?: string[] scope?: SnippetScope condition_id?: number active?: boolean priority?: number network?: boolean | null shared_network?: boolean | null } export interface SnippetSchema extends Readonly<Required<WritableSnippetSchema>> { readonly id: number readonly modified: string readonly code_error?: readonly [string, number] | null } types/schema/SnippetsExport.ts 0000755 00000000215 15110437570 0012533 0 ustar 00 import type { Snippet } from '../Snippet' export interface SnippetsExport { generator: string date_created: string snippets: Snippet[] } types/KeyboardShortcut.ts 0000755 00000002252 15110437570 0011563 0 ustar 00 import { _x } from '@wordpress/i18n' export const KEYBOARD_KEYS = <const> { 'Cmd': _x('Cmd', 'keyboard key', 'code-snippets'), 'Ctrl': _x('Ctrl', 'keyboard key', 'code-snippets'), 'Shift': _x('Shift', 'keyboard key', 'code-snippets'), 'Option': _x('Option', 'keyboard key', 'code-snippets'), 'Alt': _x('Alt', 'keyboard key', 'code-snippets'), 'Tab': _x('Tab', 'keyboard key', 'code-snippets'), 'Up': _x('Up', 'keyboard key', 'code-snippets'), 'Down': _x('Down', 'keyboard key', 'code-snippets'), 'A': _x('A', 'keyboard key', 'code-snippets'), 'D': _x('D', 'keyboard key', 'code-snippets'), 'F': _x('F', 'keyboard key', 'code-snippets'), 'G': _x('G', 'keyboard key', 'code-snippets'), 'R': _x('R', 'keyboard key', 'code-snippets'), 'S': _x('S', 'keyboard key', 'code-snippets'), 'Y': _x('Y', 'keyboard key', 'code-snippets'), 'Z': _x('Z', 'keyboard key', 'code-snippets'), '/': _x('/', 'keyboard key', 'code-snippets'), '[': _x(']', 'keyboard key', 'code-snippets'), ']': _x(']', 'keyboard key', 'code-snippets') } export type KeyboardKey = keyof typeof KEYBOARD_KEYS export interface KeyboardShortcut { label: string mod: KeyboardKey | KeyboardKey[] key: KeyboardKey } types/ScreenNotice.ts 0000755 00000000071 15110437570 0010645 0 ustar 00 export type ScreenNotice = ['error' | 'updated', string] types/SelectOption.ts 0000755 00000000573 15110437570 0010703 0 ustar 00 import type { GroupBase, Options, OptionsOrGroups } from 'react-select' export interface SelectOption<T> { readonly key?: string | number readonly value: T readonly label: string } export type SelectGroup<T> = GroupBase<SelectOption<T>> export type SelectOptions<T> = Options<SelectOption<T>> export type SelectGroups<T> = OptionsOrGroups<SelectOption<T>, SelectGroup<T>> types/Shortcodes.ts 0000755 00000000305 15110437570 0010401 0 ustar 00 export interface SourceShortcodeAtts { id: string line_numbers: boolean } export interface ContentShortcodeAtts { id: string name: string php: boolean format: boolean shortcodes: boolean } types/Snippet.ts 0000755 00000001644 15110437570 0007715 0 ustar 00 export interface Snippet { readonly id: number readonly name: string readonly desc: string readonly code: string readonly tags: string[] readonly scope: SnippetScope readonly priority: number readonly active: boolean readonly network: boolean readonly shared_network?: boolean | null readonly modified?: string readonly conditionId: number readonly code_error?: readonly [string, number] | null } export type SnippetCodeType = 'php' | 'html' | 'css' | 'js' export type SnippetType = SnippetCodeType | 'cond' export type SnippetCodeScope = typeof SNIPPET_TYPE_SCOPES[SnippetCodeType][number] export type SnippetScope = typeof SNIPPET_TYPE_SCOPES[SnippetType][number] export const SNIPPET_TYPE_SCOPES = <const> { php: ['global', 'admin', 'front-end', 'single-use'], html: ['content', 'head-content', 'footer-content'], css: ['admin-css', 'site-css'], js: ['site-head-js', 'site-footer-js'], cond: ['condition'] } types/Window.ts 0000755 00000002647 15110437570 0007546 0 ustar 00 import type Prism from 'prismjs' import type tinymce from 'tinymce' import type { Snippet } from './Snippet' import type { CodeEditorInstance, EditorOption, WordPressCodeEditor } from './WordPressCodeEditor' import type { WordPressEditor } from './WordPressEditor' declare global { interface Window { readonly wp: { readonly editor?: WordPressEditor readonly codeEditor: WordPressCodeEditor } readonly pagenow: string readonly ajaxurl: string readonly tinymce?: tinymce.EditorManager readonly wpActiveEditor?: string code_snippets_editor_preview?: CodeEditorInstance readonly code_snippets_editor_settings: EditorOption[] CODE_SNIPPETS_PRISM?: typeof Prism readonly CODE_SNIPPETS?: { isLicensed: boolean restAPI: { base: string snippets: string conditions: string cloud: string nonce: string localToken: string } urls: { plugin: string manage: string addNew: string edit: string connectCloud: string } } readonly CODE_SNIPPETS_EDIT?: { snippet: Snippet pageTitleActions: Record<string, string> isPreview: boolean isLicensed: boolean enableDownloads: boolean activateByDefault: boolean enableDescription: boolean hideUpsell: boolean editorTheme: string tagOptions: { enabled: boolean allowSpaces: boolean availableTags: string[] } descEditorOptions: { rows: number mediaButtons: boolean } } } } types/WordPressCodeEditor.ts 0000755 00000001354 15110437570 0012163 0 ustar 00 import type { Editor, EditorConfiguration } from 'codemirror' export interface EditorOption { name: string type: 'checkbox' | 'number' | 'select' codemirror: keyof EditorConfiguration } export interface CodeEditorInstance { codemirror: Editor settings: CodeEditorSettings } export interface CodeEditorSettings { codemirror: EditorConfiguration csslint: Record<string, unknown> htmlhint: Record<string, unknown> jshint: Record<string, unknown> onTabNext: () => void onTabPrevious: () => void onChangeLintingErrors: () => void onUpdateErrorNotice: () => void } export interface WordPressCodeEditor { initialize: (textarea: Element, options?: Partial<CodeEditorSettings>) => CodeEditorInstance defaultSettings: CodeEditorSettings } types/WordPressEditor.ts 0000755 00000001144 15110437570 0011365 0 ustar 00 import type tinymce from 'tinymce' export interface VisualEditorSettings { tinymce: boolean | tinymce.Settings & { toolbar1?: string | string[] toolbar2?: string | string[] toolbar3?: string | string[] toolbar4?: string | string[] } quicktags: boolean | Record<string, string> mediaButtons: boolean } export interface WordPressEditor { initialize: (id: string, settings?: Partial<VisualEditorSettings>) => void remove: (id: string) => void getContent: (id: string) => string } export interface LocalisedEditor extends tinymce.Editor { getLang: (s: string) => string | Record<string, string> } utils/snippets/api.ts 0000755 00000005617 15110437570 0010711 0 ustar 00 import { addQueryArgs } from '@wordpress/url' import { REST_SNIPPETS_BASE } from '../restAPI' import { createSnippetObject } from './snippets' import type { RestAPI } from '../../hooks/useRestAPI' import type { SnippetSchema, WritableSnippetSchema } from '../../types/schema/SnippetSchema' import type { Snippet } from '../../types/Snippet' import type { SnippetsExport } from '../../types/schema/SnippetsExport' export interface SnippetsAPI { fetchAll: (network?: boolean | null) => Promise<Snippet[]> fetch: (snippetId: number, network?: boolean | null) => Promise<Snippet> create: (snippet: Snippet) => Promise<Snippet> update: (snippet: Pick<Snippet, 'id' | 'network'> & Partial<Snippet>) => Promise<Snippet> delete: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<void> activate: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<Snippet> deactivate: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<Snippet> export: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<SnippetsExport> exportCode: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<string> attach: (snippet: Pick<Snippet, 'id' | 'network' | 'conditionId'>) => Promise<void> detach: (snippet: Pick<Snippet, 'id' | 'network'>) => Promise<void> } const buildURL = ({ id, network }: Pick<Snippet, 'id' | 'network'>, action?: string) => addQueryArgs( [REST_SNIPPETS_BASE, id, action].filter(Boolean).join('/'), { network: network ? true : undefined } ) const mapToSchema = ({ name, desc, code, tags, scope, priority, active, network, shared_network, conditionId }: Partial<Snippet>): WritableSnippetSchema => ({ name, desc, code, tags, scope, priority, active, network, shared_network, condition_id: conditionId }) export const buildSnippetsAPI = ({ get, post, del, put }: RestAPI): SnippetsAPI => ({ fetchAll: network => get<SnippetSchema[]>(addQueryArgs(REST_SNIPPETS_BASE, { network })) .then(response => response.map(createSnippetObject)), fetch: (snippetId, network) => get<SnippetSchema>(addQueryArgs(`${REST_SNIPPETS_BASE}/${snippetId}`, { network })) .then(createSnippetObject), create: snippet => post<SnippetSchema>(REST_SNIPPETS_BASE, mapToSchema(snippet)) .then(createSnippetObject), update: snippet => post<SnippetSchema>(snippet.id ? buildURL(snippet) : REST_SNIPPETS_BASE, mapToSchema(snippet)) .then(createSnippetObject), delete: snippet => del(buildURL(snippet)), activate: snippet => post<SnippetSchema>(buildURL(snippet, 'activate')) .then(createSnippetObject), deactivate: snippet => post<SnippetSchema>(buildURL(snippet, 'deactivate')) .then(createSnippetObject), export: snippet => get<SnippetsExport>(buildURL(snippet, 'export')), exportCode: snippet => get<string>(buildURL(snippet, 'export-code')), attach: snippet => put(buildURL(snippet, 'attach'), { condition_id: snippet.conditionId }), detach: snippet => put(buildURL(snippet, 'detach')) }) utils/snippets/objects.ts 0000755 00000004215 15110437570 0011562 0 ustar 00 import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet' import { isNetworkAdmin } from '../screen' import type { Snippet, SnippetScope } from '../../types/Snippet' const defaults: Omit<Snippet, 'tags'> = { id: 0, name: '', code: '', desc: '', scope: 'global', modified: '', active: false, network: isNetworkAdmin(), shared_network: null, priority: 10, conditionId: 0 } const isAbsInt = (value: unknown): value is number => 'number' === typeof value && 0 < value const parseStringArray = (value: unknown): string[] | undefined => Array.isArray(value) ? value.filter(entry => 'string' === typeof entry) : undefined export const isValidScope = (scope: unknown): scope is SnippetScope => 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => typeScopes.some(typeScope => typeScope === scope)) export const parseSnippetObject = (fields: unknown): Snippet => { const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { ...defaults, tags: [] } if ('object' !== typeof fields || null === fields) { return result } return { ...result, ...'id' in fields && isAbsInt(fields.id) && { id: fields.id }, ...'name' in fields && 'string' === typeof fields.name && { name: fields.name }, ...'desc' in fields && 'string' === typeof fields.desc && { desc: fields.desc }, ...'code' in fields && 'string' === typeof fields.code && { code: fields.code }, ...'tags' in fields && { tags: parseStringArray(fields.tags) ?? result.tags }, ...'scope' in fields && isValidScope(fields.scope) && { scope: fields.scope }, ...'modified' in fields && 'string' === typeof fields.modified && { modified: fields.modified }, ...'active' in fields && 'boolean' === typeof fields.active && { active: fields.active }, ...'network' in fields && 'boolean' === typeof fields.network && { network: fields.network }, ...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network }, ...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority }, ...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id } } } utils/snippets/snippets.ts 0000755 00000002632 15110437570 0011777 0 ustar 00 import { __ } from '@wordpress/i18n' import { parseSnippetObject } from './objects' import type { Snippet, SnippetType } from '../../types/Snippet' const PRO_TYPES = new Set<SnippetType>(['css', 'js', 'cond']) export const createSnippetObject = (fields: unknown): Snippet => parseSnippetObject(fields) export const getSnippetType = ({ scope }: Pick<Snippet, 'scope'>): SnippetType => { switch (true) { case scope.endsWith('-css'): return 'css' case scope.endsWith('-js'): return 'js' case scope.endsWith('content'): return 'html' case 'condition' === scope: return 'cond' default: return 'php' } } export const validateSnippet = (snippet: Snippet): undefined | string => { const missingTitle = '' === snippet.name.trim() const missingCode = '' === snippet.code.trim() switch (true) { case missingCode && missingTitle: return __('This snippet has no code or title.', 'code-snippets') case missingCode: return __('This snippet has no snippet code.', 'code-snippets') case missingTitle: return __('This snippet has no title.', 'code-snippets') default: return undefined } } export const isCondition = (snippet: Pick<Snippet, 'scope'>): boolean => 'condition' === snippet.scope export const isProSnippet = (snippet: Pick<Snippet, 'scope'>): boolean => PRO_TYPES.has(getSnippetType(snippet)) export const isProType = (type: SnippetType): boolean => PRO_TYPES.has(type) utils/errors.ts 0000755 00000000120 15110437570 0007567 0 ustar 00 export const handleUnknownError = (error: unknown) => { console.error(error) } utils/files.ts 0000755 00000002703 15110437570 0007366 0 ustar 00 import { getSnippetType } from './snippets/snippets' import type { SnippetsExport } from '../types/schema/SnippetsExport' import type { Snippet } from '../types/Snippet' const SECOND_IN_MS = 1000 const TIMEOUT_SECONDS = 40 const JSON_INDENT_SPACES = 2 const MIME_INFO = <const> { php: ['php', 'text/php'], html: ['php', 'text/php'], css: ['css', 'text/css'], js: ['js', 'text/javascript'], cond: ['json', 'application/json'], json: ['json', 'application/json'] } export const downloadAsFile = (content: BlobPart, filename: string, type: string) => { const link = document.createElement('a') link.download = filename link.href = URL.createObjectURL(new Blob([content], { type })) setTimeout(() => URL.revokeObjectURL(link.href), TIMEOUT_SECONDS * SECOND_IN_MS) setTimeout(() => link.click(), 0) } export const downloadSnippetExportFile = ( content: SnippetsExport | string, { id, name, scope }: Snippet, type?: keyof typeof MIME_INFO ) => { const sanitizedName = name.toLowerCase().replace(/[^\w-]+/g, '-').trim() const title = '' === sanitizedName ? `snippet-${id}` : sanitizedName if ('string' === typeof content) { const [ext, mimeType] = MIME_INFO[type ?? getSnippetType({ scope })] const filename = `${title}.code-snippets.${ext}` downloadAsFile(content, filename, mimeType) } else { const filename = `${title}.code-snippets.json` downloadAsFile(JSON.stringify(content, undefined, JSON_INDENT_SPACES), filename, 'application/json') } } utils/hooks.ts 0000755 00000000762 15110437570 0007412 0 ustar 00 import { createContext, useContext } from 'react' import type { Context } from 'react' export const createContextHook = <T>(name: string): [ Context<T | undefined>, () => T ] => { const contextValue = createContext<T | undefined>(undefined) const useContextHook = (): T => { const value = useContext(contextValue) if (value === undefined) { throw Error(`use${name} can only be used within a ${name} context provider.`) } return value } return [contextValue, useContextHook] } utils/Linter.ts 0000755 00000010701 15110437570 0007516 0 ustar 00 /** * Based on work licensed under the BSD 3-Clause license. * * Copyright (c) 2017, glayzzle * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import { Engine } from 'php-parser' import CodeMirror from 'codemirror' import type { Block, Location, Node } from 'php-parser' export interface Annotation { message: string severity: string from: CodeMirror.Position to: CodeMirror.Position } export interface Identifier extends Node { name: string } export interface Declaration extends Node { name: Identifier | string } export class Linter { private readonly code: string private readonly function_names: Set<string> private readonly class_names: Set<string> public readonly annotations: Annotation[] /** * Constructor. * @param code */ constructor(code: string) { this.code = code this.annotations = [] this.function_names = new Set() this.class_names = new Set() } /** * Lint the provided code. */ lint() { const parser = new Engine({ parser: { suppressErrors: true, version: 800 }, ast: { withPositions: true } }) try { const program = parser.parseEval(this.code) if (0 < program.errors.length) { for (const error of program.errors) { this.annotate(error.message, error.loc) } } this.visit(program) } catch (error) { console.error(error) } } /** * Visit nodes recursively. * @param node */ visit(node: Node) { if (node.kind) { this.validate(node) } if ('children' in node) { const block = <Block> node for (const child of block.children) { this.visit(child) } } } /** * Check whether a given identifier has already been defined, creating an annotation if so. * @param identifier * @param registry * @param label */ checkDuplicateIdentifier(identifier: Identifier, registry: Set<string>, label: string) { if (registry.has(identifier.name)) { this.annotate(`Cannot redeclare ${label} ${identifier.name}()`, identifier.loc) } else { registry.add(identifier.name) } } /** * Perform additional validations on nodes. * @param node */ validate(node: Node) { const decl = <Declaration> node const ident = <Identifier> decl.name if (!('name' in decl && 'name' in ident) || 'identifier' !== ident.kind) { return } if ('function' === node.kind) { this.checkDuplicateIdentifier(ident, this.function_names, 'function') } else if ('class' === node.kind) { this.checkDuplicateIdentifier(ident, this.class_names, 'class') } } /** * Create a lint annotation. * @param message * @param location * @param severity */ annotate(message: string, location: Location | null, severity = 'error') { const [start, end] = location ? location.end.offset < location.start.offset ? [location.end, location.start] : [location.start, location.end] : [{ line: 0, column: 0 }, { line: 0, column: 0 }] this.annotations.push({ message, severity, from: CodeMirror.Pos(start.line - 1, start.column), to: CodeMirror.Pos(end.line - 1, end.column) }) } } utils/restAPI.ts 0000755 00000000731 15110437570 0007572 0 ustar 00 import { trimTrailingChar } from './text' import type { AxiosRequestConfig } from 'axios' export const REST_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.base ?? '', '/') export const REST_SNIPPETS_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.snippets ?? '', '/') export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = { headers: { 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce, 'Access-Control': window.CODE_SNIPPETS?.restAPI.localToken } } utils/screen.ts 0000755 00000000371 15110437570 0007542 0 ustar 00 export const isNetworkAdmin = (): boolean => window.pagenow.endsWith('-network') export const isMacOS = (): boolean => null !== /mac/i.exec(window.navigator.userAgent) export const isLicensed = (): boolean => !!window.CODE_SNIPPETS?.isLicensed utils/text.ts 0000755 00000001423 15110437570 0007246 0 ustar 00 export const toCamelCase = (text: string): string => text.replace(/-(?<letter>[a-z])/g, (_, letter: string) => letter.toUpperCase()) export const trimLeadingChar = (text: string, character: string): string => character === text.charAt(0) ? text.slice(1) : text export const trimTrailingChar = (text: string, character: string): string => character === text.charAt(text.length - 1) ? text.slice(0, -1) : text export const truncateWords = (text: string, wordCount: number): string => { const words = text.trim().split(/\s+/) return words.length > wordCount ? `${words.slice(0, wordCount).join(' ')}…` : text } export const stripTags = (text: string): string => text .replace(/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi, '') .replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, '') editor.ts 0000755 00000001021 15110437570 0006402 0 ustar 00 import { defineMode, getMode, registerHelper } from 'codemirror' import { Linter } from './utils/Linter' import type { EditorConfiguration, ModeSpec } from 'codemirror' interface ModeSpecOptions { startOpen: boolean } const mode: ModeSpec<ModeSpecOptions> = { name: 'application/x-httpd-php', startOpen: true } defineMode('php-snippet', (config: EditorConfiguration) => getMode(config, mode)) registerHelper('lint', 'php', (text: string) => { const linter = new Linter(text) linter.lint() return linter.annotations }) edit.tsx 0000755 00000000546 15110437570 0006244 0 ustar 00 import React from 'react' import { createRoot } from 'react-dom/client' import { SnippetForm } from './components/SnippetForm' const container = document.getElementById('edit-snippet-form-container') if (container) { const root = createRoot(container) root.render(<SnippetForm />) } else { console.error('Could not find snippet edit form container.') } manage.ts 0000755 00000000325 15110437570 0006352 0 ustar 00 import { handleShowCloudPreview, handleSnippetActivationSwitches, handleSnippetPriorityChanges } from './services/manage' handleSnippetActivationSwitches() handleSnippetPriorityChanges() handleShowCloudPreview() mce.ts 0000755 00000005443 15110437570 0005674 0 ustar 00 import tinymce from 'tinymce' import type { Editor } from 'tinymce' import type { ContentShortcodeAtts, SourceShortcodeAtts } from './types/Shortcodes' import type { LocalisedEditor } from './types/WordPressEditor' const convertToValues = (array: Record<string, string>) => Object.keys(array).map(key => ({ text: array[Number(key)], value: key })) export const insertContentMenu = (editor: Editor, activeEditor: LocalisedEditor) => ({ text: activeEditor.getLang('code_snippets.insert_source_menu'), onclick: () => { editor.windowManager.open({ title: activeEditor.getLang('code_snippets.insert_source_title'), body: [ { type: 'listbox', name: 'id', label: activeEditor.getLang('code_snippets.snippet_label'), values: convertToValues(<Record<string, string>> activeEditor.getLang('code_snippets.all_snippets')) }, { type: 'checkbox', name: 'line_numbers', label: activeEditor.getLang('code_snippets.show_line_numbers_label') } ], onsubmit: (event: { data: SourceShortcodeAtts }) => { const id = parseInt(event.data.id, 10) if (!id) { return } let atts = '' if (event.data.line_numbers) { atts += ' line_numbers=true' } editor.insertContent(`[code_snippet_source id=${id}${atts}]`) } }, {}) } }) export const insertSourceMenu = (editor: Editor, ed: LocalisedEditor) => ({ text: ed.getLang('code_snippets.insert_content_menu'), onclick: () => { editor.windowManager.open({ title: ed.getLang('code_snippets.insert_content_title'), body: [ { type: 'listbox', name: 'id', label: ed.getLang('code_snippets.snippet_label'), values: convertToValues(<Record<string, string>> ed.getLang('code_snippets.content_snippets')) }, { type: 'checkbox', name: 'php', label: ed.getLang('code_snippets.php_att_label') }, { type: 'checkbox', name: 'format', label: ed.getLang('code_snippets.format_att_label') }, { type: 'checkbox', name: 'shortcodes', label: ed.getLang('code_snippets.shortcodes_att_label') } ], onsubmit: (event: { data: ContentShortcodeAtts }) => { const id = parseInt(event.data.id, 10) if (!id) { return } let atts = '' for (const [opt, val] of Object.entries(event.data)) { if ('id' !== opt && val) { atts += ` ${opt}=${val}` } } editor.insertContent(`[code_snippet id=${id}${atts}]`) } }, {}) } }) tinymce.PluginManager.add('code_snippets', function (editor) { const activeEditor = <LocalisedEditor> tinymce.activeEditor // Create the menu button with inline menu items editor.addButton('code_snippets', { icon: 'code', type: 'menubutton', menu: [ insertContentMenu(editor, activeEditor), insertSourceMenu(editor, activeEditor) ] }) }) prism.ts 0000755 00000001603 15110437570 0006254 0 ustar 00 import Prism from 'prismjs' import 'prismjs/components/prism-markup' import 'prismjs/components/prism-markup-templating' import 'prismjs/components/prism-clike' import 'prismjs/components/prism-css' import 'prismjs/components/prism-php' import 'prismjs/components/prism-javascript' import 'prismjs/plugins/line-highlight/prism-line-highlight' import 'prismjs/plugins/line-numbers/prism-line-numbers' import 'prismjs/plugins/toolbar/prism-toolbar' import 'prismjs/plugins/show-language/prism-show-language' import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard' import 'prismjs/plugins/inline-color/prism-inline-color' import 'prismjs/plugins/previewers/prism-previewers' import 'prismjs/plugins/autolinker/prism-autolinker' document.addEventListener('readystatechange', () => { if ('complete' === document.readyState) { Prism.highlightAll() } }) window.CODE_SNIPPETS_PRISM = Prism settings.ts 0000755 00000000257 15110437570 0006766 0 ustar 00 import { handleEditorPreviewUpdates, handleSettingsTabs, initVersionSwitch } from './services/settings' handleSettingsTabs() handleEditorPreviewUpdates() initVersionSwitch()
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0.02 |
proxy
|
phpinfo
|
Настройка