diff --git a/blocks/library/index.js b/blocks/library/index.js index 8ae8ce98543f25..517db6b43ac0d3 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -22,3 +22,4 @@ import './text-columns'; import './verse'; import './video'; import './audio'; +import './post-title'; diff --git a/blocks/library/post-title/editor.scss b/blocks/library/post-title/editor.scss new file mode 100644 index 00000000000000..c0cd09b67543b6 --- /dev/null +++ b/blocks/library/post-title/editor.scss @@ -0,0 +1,38 @@ +.editor-post-permalink { + display: inline-flex; + align-items: center; + position: absolute; + top: -34px; + box-shadow: $shadow-popover; + border: 1px solid $light-gray-500; + background: $white; + padding: 5px; + font-family: $default-font; + font-size: $default-font-size; + left: 0; + right: 0; + + @include break-small() { + left: $block-padding + $block-mover-padding-visible - 2px; + right: $block-padding + $block-mover-padding-visible - 2px; + } +} + +.editor-post-permalink__label { + margin: 0 10px; +} + +.editor-post-permalink__link { + color: $dark-gray-200; + text-decoration: underline; + margin-right: 10px; + width: 100%; + overflow: hidden; + position: relative; + white-space: nowrap; + + &:after { + @include long-content-fade( $size: 20% ); + } +} + diff --git a/blocks/library/post-title/index.js b/blocks/library/post-title/index.js new file mode 100644 index 00000000000000..d072a69f63a3f0 --- /dev/null +++ b/blocks/library/post-title/index.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { registerBlockType, source } from '../../api'; +import PostTitleEdit from './post-title-edit'; + +registerBlockType( 'core/post-title', { + title: __( 'Post Title' ), + + keywords: [ __( 'title' ) ], + + className: false, + + category: 'common', + + attributes: { + content: { + type: 'string', + source: source.text(), + }, + }, + + isFixed: true, + + edit( { attributes, setAttributes, focus, setFocus } ) { + return setAttributes( { content: value } ) } + />; + }, + + save( { attributes } ) { + return

{ attributes.content }

; + }, +} ); diff --git a/blocks/library/post-title/post-permalink.js b/blocks/library/post-title/post-permalink.js new file mode 100644 index 00000000000000..45f6eb8be6c352 --- /dev/null +++ b/blocks/library/post-title/post-permalink.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Dashicon, ClipboardButton, Button } from '@wordpress/components'; + +/** + * Internal Dependencies + */ +import './editor.scss'; +import { isEditedPostNew, getEditedPostAttribute } from '../../../editor/selectors'; + +class PostPermalink extends Component { + constructor() { + super( ...arguments ); + this.state = { + showCopyConfirmation: false, + }; + this.onCopy = this.onCopy.bind( this ); + } + + componentWillUnmout() { + clearTimeout( this.dismissCopyConfirmation ); + } + + onCopy() { + this.setState( { + showCopyConfirmation: true, + } ); + + clearTimeout( this.dismissCopyConfirmation ); + this.dismissCopyConfirmation = setTimeout( () => { + this.setState( { + showCopyConfirmation: false, + } ); + }, 4000 ); + } + + render() { + const { isNew, link } = this.props; + if ( isNew || ! link ) { + return null; + } + + return ( +
+ + { __( 'Permalink:' ) } + + + { this.state.showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy' ) } + +
+ ); + } +} + +export default connect( + ( state ) => { + return { + isNew: isEditedPostNew( state ), + link: getEditedPostAttribute( state, 'link' ), + }; + } +)( PostPermalink ); + diff --git a/blocks/library/post-title/post-title-edit.js b/blocks/library/post-title/post-title-edit.js new file mode 100644 index 00000000000000..ef8ce9133e0840 --- /dev/null +++ b/blocks/library/post-title/post-title-edit.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import Editable from '../../editable'; +import PostPermaLink from './post-permalink'; +import { editPost } from '../../../editor/actions'; + +const PostTitleEdit = ( { title, focus, setFocus, onChange } ) => [ + focus && , + ]; + +export default connect( null, dispatch => ( { onChange: title => { + dispatch( editPost( { title: title[ 0 ] } ) ); +} } ) )( PostTitleEdit ); diff --git a/editor/block-mover/index.js b/editor/block-mover/index.js index d127eea6b03baa..7697d45e87da67 100644 --- a/editor/block-mover/index.js +++ b/editor/block-mover/index.js @@ -15,10 +15,10 @@ import { getBlockType } from '@wordpress/blocks'; * Internal dependencies */ import './style.scss'; -import { isFirstBlock, isLastBlock, getBlockIndex, getBlock } from '../selectors'; +import { canMoveBlockUp, isLastBlock, getBlockIndex, getBlock } from '../selectors'; import { getBlockMoverLabel } from './mover-label'; -function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex } ) { +function BlockMover( { onMoveUp, onMoveDown, canMoveUp, canMoveDown, uids, blockType, firstIndex } ) { // We emulate a disabled state because forcefully applying the `disabled` // attribute on the button while it has focus causes the screen to change // to an unfocused state (body as active element) without firing blur on, @@ -27,33 +27,33 @@ function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, f
); @@ -61,8 +61,8 @@ function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, f export default connect( ( state, ownProps ) => ( { - isFirst: isFirstBlock( state, first( ownProps.uids ) ), - isLast: isLastBlock( state, last( ownProps.uids ) ), + canMoveUp: canMoveBlockUp( state, first( ownProps.uids ) ), + canMoveDown: ! isLastBlock( state, last( ownProps.uids ) ), firstIndex: getBlockIndex( state, first( ownProps.uids ) ), blockType: getBlockType( getBlock( state, first( ownProps.uids ) ).name ), } ), diff --git a/editor/block-mover/mover-label.js b/editor/block-mover/mover-label.js index 1b09cce1742592..5ee0c77c1949ab 100644 --- a/editor/block-mover/mover-label.js +++ b/editor/block-mover/mover-label.js @@ -10,25 +10,25 @@ import { __, sprintf } from '@wordpress/i18n'; * @param {string} type Block type - in the case of a single block, should * define its 'type'. I.e. 'Text', 'Heading', 'Image' etc. * @param {number} firstIndex The index (position - 1) of the first block selected. - * @param {boolean} isFirst This is the first block. - * @param {boolean} isLast This is the last block. + * @param {boolean} canMoveUp Indicates whether the first selected block can move up. + * @param {boolean} canMoveDown Indicates whether the last selected block can move down. * @param {number} dir Direction of movement (> 0 is considered to be going * down, < 0 is up). * @return {string} Label for the block movement controls. */ -export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, isLast, dir ) { +export function getBlockMoverLabel( selectedCount, type, firstIndex, canMoveUp, canMoveDown, dir ) { const position = ( firstIndex + 1 ); if ( selectedCount > 1 ) { - return getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ); + return getMultiBlockMoverLabel( selectedCount, firstIndex, canMoveUp, canMoveDown, dir ); } - if ( isFirst && isLast ) { + if ( ( ! canMoveUp ) && ( ! canMoveDown ) ) { // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block "%s" is the only block, and cannot be moved' ), type ); + return sprintf( __( 'Block "%s" is the only moveable block, and cannot be moved' ), type ); } - if ( dir > 0 && ! isLast ) { + if ( dir > 0 && canMoveDown ) { // moving down return sprintf( __( 'Move "%(type)s" block from position %(position)d down to position %(newPosition)d' ), @@ -40,13 +40,13 @@ export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, is ); } - if ( dir > 0 && isLast ) { + if ( dir > 0 && ! canMoveDown ) { // moving down, and is the last item // translators: %s: Type of block (i.e. Text, Image etc) return sprintf( __( 'Block "%s" is at the end of the content and can’t be moved down' ), type ); } - if ( dir < 0 && ! isFirst ) { + if ( dir < 0 && canMoveUp ) { // moving up return sprintf( __( 'Move "%(type)s" block from position %(position)d up to position %(newPosition)d' ), @@ -58,10 +58,10 @@ export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, is ); } - if ( dir < 0 && isFirst ) { + if ( dir < 0 && ! canMoveUp ) { // moving up, and is the first item // translators: %s: Type of block (i.e. Text, Image etc) - return sprintf( __( 'Block "%s" is at the beginning of the content and can’t be moved up' ), type ); + return sprintf( __( 'Block "%s" is at the beginning of moveable blocks and can’t be moved up' ), type ); } } @@ -70,24 +70,24 @@ export function getBlockMoverLabel( selectedCount, type, firstIndex, isFirst, is * * @param {number} selectedCount Number of blocks selected. * @param {number} firstIndex The index (position - 1) of the first block selected. - * @param {boolean} isFirst This is the first block. - * @param {boolean} isLast This is the last block. + * @param {boolean} canMoveUp Indicates whether the first selected block can move up. + * @param {boolean} canMoveDown Indicates whether the last selected block can move down. * @param {number} dir Direction of movement (> 0 is considered to be going * down, < 0 is up). * @return {string} Label for the block movement controls. */ -export function getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isLast, dir ) { +export function getMultiBlockMoverLabel( selectedCount, firstIndex, canMoveUp, canMoveDown, dir ) { const position = ( firstIndex + 1 ); - if ( dir < 0 && isFirst ) { + if ( dir < 0 && ! canMoveUp ) { return __( 'Blocks cannot be moved up as they are already at the top' ); } - if ( dir > 0 && isLast ) { + if ( dir > 0 && ! canMoveDown ) { return __( 'Blocks cannot be moved down as they are already at the bottom' ); } - if ( dir < 0 && ! isFirst ) { + if ( dir < 0 && canMoveUp ) { return sprintf( __( 'Move %(selectedCount)d blocks from position %(position)d up by one place' ), { @@ -97,7 +97,7 @@ export function getMultiBlockMoverLabel( selectedCount, firstIndex, isFirst, isL ); } - if ( dir > 0 && ! isLast ) { + if ( dir > 0 && canMoveDown ) { return sprintf( __( 'Move %(selectedCount)d blocks from position %(position)s down by one place' ), { diff --git a/editor/effects.js b/editor/effects.js index 163982fba0e503..248f09ceadfb34 100644 --- a/editor/effects.js +++ b/editor/effects.js @@ -7,7 +7,7 @@ import { get, uniqueId } from 'lodash'; /** * WordPress dependencies */ -import { parse, getBlockType, switchToBlockType } from '@wordpress/blocks'; +import { parse, getBlockType, switchToBlockType, createBlock } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; /** @@ -254,12 +254,15 @@ export default { SET_INITIAL_POST( action ) { const { post } = action; const effects = []; + let blocks = []; // Parse content as blocks if ( post.content.raw ) { - effects.push( resetBlocks( parse( post.content.raw ) ) ); + blocks = blocks.concat( parse( post.content.raw ) ); } + effects.push( resetBlocks( blocks ) ); + // Resetting post should occur after blocks have been reset, since it's // the post reset that restarts history (used in dirty detection). effects.push( resetPost( post ) ); @@ -273,4 +276,8 @@ export default { return effects; }, + RESET_BLOCKS( action, store ) { + const blocks = [ createBlock( 'core/post-title' ) ].concat( action.blocks ); + store.dispatch( { type: 'ACTUALLY_RESET_BLOCKS', blocks } ); + }, }; diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index e7f729cf1f74ba..1c9e0383afa17f 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -338,8 +338,8 @@ class VisualEditorBlock extends Component { { ...wrapperProps } > - { ( showUI || isHovered ) && } - { ( showUI || isHovered ) && } + { ( showUI || isHovered ) && ( ! blockType.isFixed ) && } + { ( showUI || isHovered ) && ( ! blockType.isFixed ) && } { showUI && isValid && - diff --git a/editor/reducer.js b/editor/reducer.js index 6e59e9c26021cd..5fffcf2f75219f 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -73,7 +73,7 @@ export const editor = combineUndoableReducers( { return result; }, state ); - case 'RESET_BLOCKS': + case 'ACTUALLY_RESET_BLOCKS': if ( 'content' in state ) { return omit( state, 'content' ); } @@ -100,7 +100,7 @@ export const editor = combineUndoableReducers( { blocksByUid( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': + case 'ACTUALLY_RESET_BLOCKS': return keyBy( action.blocks, 'uid' ); case 'UPDATE_BLOCK_ATTRIBUTES': @@ -164,7 +164,7 @@ export const editor = combineUndoableReducers( { blockOrder( state = [], action ) { switch ( action.type ) { - case 'RESET_BLOCKS': + case 'ACTUALLY_RESET_BLOCKS': return action.blocks.map( ( { uid } ) => uid ); case 'INSERT_BLOCKS': { diff --git a/editor/selectors.js b/editor/selectors.js index b98904ea1fb64f..b0090d433e20e3 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -369,7 +369,8 @@ export function getEditedPostPreviewLink( state ) { * @return {Object} Parsed block object */ export function getBlock( state, uid ) { - return state.editor.blocksByUid[ uid ]; + const block = state.editor.blocksByUid[ uid ]; + return block.name === 'core/post-title' ? { ...block, attributes: { ...block.attributes, content: getEditedPostTitle( state ) } } : block; } /** @@ -390,6 +391,14 @@ export const getBlocks = createSelector( ] ); +export const getPostContentBlocks = createSelector( + state => getBlocks( state ).filter( b => b.name !== 'core/post-title' ), + ( state ) => [ + state.editor.blockOrder, + state.editor.blocksByUid, + ] +); + /** * Returns the number of blocks currently present in the post. * @@ -568,6 +577,19 @@ export function isFirstBlock( state, uid ) { return first( state.editor.blockOrder ) === uid; } +/** + * Returns true if the block corresponding to the specified unique ID can be + * moved up. + * + * @param {Object} state Global application state + * @param {String} uid Block unique ID + * @return {Boolean} Whether block is first in post + */ +export function canMoveBlockUp( state, uid ) { + // PostTitle is always the first block + return state.editor.blockOrder.length > 2 && uid !== state.editor.blockOrder[ 1 ]; +} + /** * Returns true if the block corresponding to the specified unique ID is the * last block of the post, or false otherwise. @@ -775,7 +797,7 @@ export const getEditedPostContent = createSelector( return edits.content; } - return serialize( getBlocks( state ) ); + return serialize( getPostContentBlocks( state ) ); }, ( state ) => [ state.editor.edits.content,