From f458c1a3a90d612e84ea68be16e0d1a6d36c8c72 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 6 Jan 2022 21:24:43 +0000 Subject: [PATCH] Add: Code editor to edit site. --- package-lock.json | 4 +- packages/base-styles/_z-index.scss | 2 +- .../src/components/text-editor/index.js | 65 +++++++-------- .../src/components/text-editor/style.scss | 80 ++++--------------- .../src/components/code-editor/index.js | 55 +++++++++++++ .../edit-site/src/components/editor/index.js | 23 ++++-- .../components/header/mode-switcher/index.js | 67 ++++++++++++++++ .../src/components/header/more-menu/index.js | 2 + .../components/keyboard-shortcuts/index.js | 18 ++++- packages/edit-site/src/store/actions.js | 21 +++++ packages/edit-site/src/store/defaults.js | 1 + packages/edit-site/src/store/reducer.js | 6 ++ packages/edit-site/src/store/selectors.js | 11 +++ packages/editor/package.json | 1 + .../src/components/post-text-editor/index.js | 77 +++--------------- packages/editor/src/style.scss | 1 - packages/interface/package.json | 3 +- .../components/code-editor-screen/index.js | 42 ++++++++++ .../components/code-editor-screen/style.scss | 52 ++++++++++++ .../src/components/code-editor/index.js | 76 ++++++++++++++++++ .../src/components/code-editor}/style.scss | 2 +- packages/interface/src/components/index.js | 2 + packages/interface/src/style.scss | 2 + 23 files changed, 428 insertions(+), 185 deletions(-) create mode 100644 packages/edit-site/src/components/code-editor/index.js create mode 100644 packages/edit-site/src/components/header/mode-switcher/index.js create mode 100644 packages/interface/src/components/code-editor-screen/index.js create mode 100644 packages/interface/src/components/code-editor-screen/style.scss create mode 100644 packages/interface/src/components/code-editor/index.js rename packages/{editor/src/components/post-text-editor => interface/src/components/code-editor}/style.scss (94%) diff --git a/package-lock.json b/package-lock.json index f6241b6f45b0cf..3fc9e79112f740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16755,6 +16755,7 @@ "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", + "@wordpress/interface": "file:packages/interface", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", @@ -17011,7 +17012,8 @@ "@wordpress/plugins": "file:packages/plugins", "@wordpress/viewport": "file:packages/viewport", "classnames": "^2.3.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "react-autosize-textarea": "^7.1.0" } }, "@wordpress/is-shallow-equal": { diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index fde8009dee509c..dfcba5d7a886b6 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -10,7 +10,7 @@ $z-layers: ( ".block-editor-block-list__layout .reusable-block-indicator": 1, ".block-editor-block-list__block-selection-button": 22, ".components-form-toggle__input": 1, - ".edit-post-text-editor__toolbar": 1, + ".interface-code-editor-screen__toolbar": 1, ".edit-post-sidebar__panel-tab.is-active": 1, // These next three share a stacking context diff --git a/packages/edit-post/src/components/text-editor/index.js b/packages/edit-post/src/components/text-editor/index.js index 47f5d1107d76e1..f39153f09cff84 100644 --- a/packages/edit-post/src/components/text-editor/index.js +++ b/packages/edit-post/src/components/text-editor/index.js @@ -4,54 +4,43 @@ import { PostTextEditor, PostTitle, - TextEditorGlobalKeyboardShortcuts, store as editorStore, + TextEditorGlobalKeyboardShortcuts, } from '@wordpress/editor'; -import { Button } from '@wordpress/components'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; -import { displayShortcut } from '@wordpress/keycodes'; -import { compose } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CodeEditorScreen } from '@wordpress/interface'; +import { useCallback } from '@wordpress/element'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ import { store as editPostStore } from '../../store'; -function TextEditor( { onExit, isRichEditingEnabled } ) { +export default function TextEditor() { + const { isRichEditingEnabled, shortcut } = useSelect( ( select ) => { + const { getEditorSettings } = select( editorStore ); + const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + return { + isRichEditingEnabled: getEditorSettings().richEditingEnabled, + shortcut: getShortcutRepresentation( 'core/edit-post/toggle-mode' ), + }; + }, [] ); + const { switchEditorMode } = useDispatch( editPostStore ); + const onExit = useCallback( () => switchEditorMode( 'visual' ), [ + switchEditorMode, + ] ); return ( -
- { isRichEditingEnabled && ( -
-

{ __( 'Editing code' ) }

- - -
- ) } -
+ <> + + -
-
+ + ); } - -export default compose( - withSelect( ( select ) => ( { - isRichEditingEnabled: select( editorStore ).getEditorSettings() - .richEditingEnabled, - } ) ), - withDispatch( ( dispatch ) => { - return { - onExit() { - dispatch( editPostStore ).switchEditorMode( 'visual' ); - }, - }; - } ) -)( TextEditor ); diff --git a/packages/edit-post/src/components/text-editor/style.scss b/packages/edit-post/src/components/text-editor/style.scss index 925e88df27180b..9502ee97ff7336 100644 --- a/packages/edit-post/src/components/text-editor/style.scss +++ b/packages/edit-post/src/components/text-editor/style.scss @@ -1,75 +1,23 @@ -.edit-post-text-editor { - position: relative; - width: 100%; - background-color: $white; - flex-grow: 1; +// Post title. +.edit-post-text-editor .editor-post-title { + max-width: none; + line-height: $default-line-height; - // Post title. - .editor-post-title { - max-width: none; - line-height: $default-line-height; + font-family: $editor-html-font; + font-size: 2.5em; + font-weight: normal; - font-family: $editor-html-font; - font-size: 2.5em; - font-weight: normal; + border: $border-width solid $gray-600; - border: $border-width solid $gray-600; - - // Same padding as body. - padding: $grid-unit-20; - - @include break-small() { - padding: $grid-unit-30; - } - - &:focus { - border-color: var(--wp-admin-theme-color); - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } -} - -.edit-post-text-editor__body { - width: 100%; - padding: 0 $grid-unit-15 $grid-unit-15 $grid-unit-15; - max-width: $break-xlarge; - margin-left: auto; - margin-right: auto; - - @include break-large() { - padding: $grid-unit-20 $grid-unit-30 #{ $grid-unit-60 * 2 } $grid-unit-30; - padding: 0 $grid-unit-30 $grid-unit-30 $grid-unit-30; - } - -} - -// Exit code editor toolbar. -.edit-post-text-editor__toolbar { - position: sticky; - z-index: z-index(".edit-post-text-editor__toolbar"); - top: 0; - left: 0; - right: 0; - display: flex; - background: rgba($white, 0.8); - padding: $grid-unit-05 $grid-unit-15; + // Same padding as body. + padding: $grid-unit-20; @include break-small() { - padding: $grid-unit-15; - } - - @include break-large() { - padding: $grid-unit-15 $grid-unit-30; - } - - h2 { - line-height: $button-size; - margin: 0 auto 0 0; - font-size: $default-font-size; - color: $gray-900; + padding: $grid-unit-30; } - .components-button svg { - order: 1; + &:focus { + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } diff --git a/packages/edit-site/src/components/code-editor/index.js b/packages/edit-site/src/components/code-editor/index.js new file mode 100644 index 00000000000000..bd7d29c603087c --- /dev/null +++ b/packages/edit-site/src/components/code-editor/index.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { + CodeEditorScreen, + CodeEditor as InterfaceCodeEditor, +} from '@wordpress/interface'; +import { parse } from '@wordpress/blocks'; +import { useEntityBlockEditor, useEntityProp } from '@wordpress/core-data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; + +export default function CodeEditor() { + const { templateType, shortcut } = useSelect( ( select ) => { + const { getEditedPostType } = select( editSiteStore ); + const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + return { + templateType: getEditedPostType(), + shortcut: getShortcutRepresentation( 'core/edit-site/toggle-mode' ), + }; + }, [] ); + const [ contentStructure, setContent ] = useEntityProp( + 'postType', + templateType, + 'content' + ); + const [ blocks, , onChange ] = useEntityBlockEditor( + 'postType', + templateType + ); + const content = + contentStructure instanceof Function + ? contentStructure( { blocks } ) + : contentStructure; + const { switchEditorMode } = useDispatch( editSiteStore ); + return ( + switchEditorMode( 'visual' ) } + exitShortcut={ shortcut } + > + { + onChange( parse( newContent ), { selection: undefined } ); + } } + onInput={ setContent } + /> + + ); +} diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 1a5e2a6dcaa322..12568c55468f39 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -30,6 +30,7 @@ import Header from '../header'; import { SidebarComplementaryAreaFills } from '../sidebar'; import NavigationSidebar from '../navigation-sidebar'; import BlockEditor from '../block-editor'; +import CodeEditor from '../code-editor'; import KeyboardShortcuts from '../keyboard-shortcuts'; import URLQueryController from '../url-query-controller'; import InserterSidebar from '../secondary-sidebar/inserter-sidebar'; @@ -60,6 +61,7 @@ function Editor( { onError } ) { isNavigationOpen, previousShortcut, nextShortcut, + editorMode, } = useSelect( ( select ) => { const { isInserterOpened, @@ -69,6 +71,7 @@ function Editor( { onError } ) { getEditedPostId, getPage, isNavigationOpened, + getEditorMode, } = select( editSiteStore ); const { hasFinishedResolution, getEntityRecord } = select( coreStore ); const postType = getEditedPostType(); @@ -102,6 +105,7 @@ function Editor( { onError } ) { nextShortcut: select( keyboardShortcutsStore ).getAllShortcutKeyCombinations( 'core/edit-site/next-region' ), + editorMode: getEditorMode(), }; }, [] ); const { setPage, setIsInserterOpened } = useDispatch( editSiteStore ); @@ -206,13 +210,18 @@ function Editor( { onError } ) { content={ <> - { template && ( - - ) } + { editorMode === 'visual' && + template && ( + + ) } + { editorMode === 'text' && + template && ( + + ) } { templateResolved && ! template && settings?.siteUrl && diff --git a/packages/edit-site/src/components/header/mode-switcher/index.js b/packages/edit-site/src/components/header/mode-switcher/index.js new file mode 100644 index 00000000000000..52c53b037f547f --- /dev/null +++ b/packages/edit-site/src/components/header/mode-switcher/index.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { MenuItemsChoice, MenuGroup } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; + +/** + * Set of available mode options. + * + * @type {Array} + */ +const MODES = [ + { + value: 'visual', + label: __( 'Visual editor' ), + }, + { + value: 'text', + label: __( 'Code editor' ), + }, +]; + +function ModeSwitcher() { + const { shortcut, mode } = useSelect( + ( select ) => ( { + shortcut: select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/edit-site/toggle-mode' ), + isRichEditingEnabled: select( editSiteStore ).getSettings() + .richEditingEnabled, + isCodeEditingEnabled: select( editSiteStore ).getSettings() + .codeEditingEnabled, + mode: select( editSiteStore ).getEditorMode(), + } ), + [] + ); + const { switchEditorMode } = useDispatch( editSiteStore ); + + const choices = MODES.map( ( choice ) => { + if ( choice.value !== mode ) { + return { ...choice, shortcut }; + } + return choice; + } ); + + return ( + + + + ); +} + +export default ModeSwitcher; diff --git a/packages/edit-site/src/components/header/more-menu/index.js b/packages/edit-site/src/components/header/more-menu/index.js index 2b82b318335766..45ad50d49d7cc1 100644 --- a/packages/edit-site/src/components/header/more-menu/index.js +++ b/packages/edit-site/src/components/header/more-menu/index.js @@ -23,6 +23,7 @@ import ToolsMoreMenuGroup from '../tools-more-menu-group'; import SiteExport from './site-export'; import WelcomeGuideMenuItem from './welcome-guide-menu-item'; import CopyContentMenuItem from './copy-content-menu-item'; +import ModeSwitcher from '../mode-switcher'; const POPOVER_PROPS = { className: 'edit-site-more-menu__content', @@ -76,6 +77,7 @@ export default function MoreMenu() { 'Spotlight mode deactivated' ) } /> + select( editSiteStore ).isListViewOpened(), [] @@ -35,7 +36,9 @@ function KeyboardShortcuts( { openEntitiesSavedStates } ) { [] ); const { redo, undo } = useDispatch( coreStore ); - const { setIsListViewOpened } = useDispatch( editSiteStore ); + const { setIsListViewOpened, switchEditorMode } = useDispatch( + editSiteStore + ); const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); @@ -80,6 +83,10 @@ function KeyboardShortcuts( { openEntitiesSavedStates } ) { } } ); + useShortcut( 'core/edit-site/toggle-mode', () => { + switchEditorMode( getEditorMode() === 'visual' ? 'text' : 'visual' ); + } ); + return null; } @@ -178,6 +185,15 @@ function KeyboardShortcutsRegister() { }, ], } ); + registerShortcut( { + name: 'core/edit-site/toggle-mode', + category: 'global', + description: __( 'Switch between visual editor and code editor.' ), + keyCombination: { + modifier: 'secondary', + character: 'm', + }, + } ); }, [ registerShortcut ] ); return null; diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index b43b1e5b83fca6..9d9be4525cc849 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -9,6 +9,8 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -520,3 +522,22 @@ export function* closeGeneralSidebar() { editSiteStoreName ); } + +export function* switchEditorMode( mode ) { + yield { + type: 'SWITCH_MODE', + mode, + }; + + // Unselect blocks when we switch to a non visual mode. + if ( mode !== 'visual' ) { + yield controls.dispatch( blockEditorStore.name, 'clearSelectedBlock' ); + } + const messages = { + visual: __( 'Visual editor selected' ), + mosaic: __( 'Mosaic view selected' ), + }; + if ( messages[ mode ] ) { + speak( messages[ mode ], 'assertive' ); + } +} diff --git a/packages/edit-site/src/store/defaults.js b/packages/edit-site/src/store/defaults.js index f351ef21533b3b..a7d828654cdc76 100644 --- a/packages/edit-site/src/store/defaults.js +++ b/packages/edit-site/src/store/defaults.js @@ -3,4 +3,5 @@ export const PREFERENCES_DEFAULTS = { welcomeGuide: true, welcomeGuideStyles: true, }, + editorMode: 'visual', }; diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index d4f8d451b2e02a..fa5ca383a4a5af 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -29,6 +29,12 @@ export const preferences = combineReducers( { return state; } }, + editorMode( state = PREFERENCES_DEFAULTS.editorMode, action ) { + if ( action.type === 'SWITCH_MODE' ) { + return action.mode; + } + return state; + }, } ); /** diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index e64dd8ad8cd5ea..c546e00f0eb68c 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -327,3 +327,14 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector( .filter( ( { templatePart } ) => !! templatePart ); } ); + +/** + * Returns the current editing mode. + * + * @param {Object} state Global application state. + * + * @return {string} Editing mode. + */ +export function getEditorMode( state ) { + return state.preferences.editorMode || 'visual'; +} diff --git a/packages/editor/package.json b/packages/editor/package.json index 311b62ced238fa..750c994f58b5ca 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -49,6 +49,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/interface": "file:../interface", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index 55bcb7228bf016..edd9b74042386d 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -1,17 +1,9 @@ -/** - * External dependencies - */ -import Textarea from 'react-autosize-textarea'; - /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; import { parse } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; -import { useInstanceId } from '@wordpress/compose'; -import { VisuallyHidden } from '@wordpress/components'; +import { CodeEditor } from '@wordpress/interface'; /** * Internal dependencies @@ -23,66 +15,15 @@ export default function PostTextEditor() { ( select ) => select( editorStore ).getEditedPostContent(), [] ); - const { editPost, resetEditorBlocks } = useDispatch( editorStore ); - - const [ value, setValue ] = useState( postContent ); - const [ isDirty, setIsDirty ] = useState( false ); - const instanceId = useInstanceId( PostTextEditor ); - - if ( ! isDirty && value !== postContent ) { - setValue( postContent ); - } - - /** - * Handles a textarea change event to notify the onChange prop callback and - * reflect the new value in the component's own state. This marks the start - * of the user's edits, if not already changed, preventing future props - * changes to value from replacing the rendered value. This is expected to - * be followed by a reset to dirty state via `stopEditing`. - * - * @see stopEditing - * - * @param {Event} event Change event. - */ - const onChange = ( event ) => { - const newValue = event.target.value; - editPost( { content: newValue } ); - setValue( newValue ); - setIsDirty( true ); - }; - - /** - * Function called when the user has completed their edits, responsible for - * ensuring that changes, if made, are surfaced to the onPersist prop - * callback and resetting dirty state. - */ - const stopEditing = () => { - if ( isDirty ) { - const blocks = parse( value ); - resetEditorBlocks( blocks ); - setIsDirty( false ); - } - }; - return ( - <> - - { __( 'Type text or HTML' ) } - -