Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions: Add block editor (Gutenberg) extensions source #11633

Merged
merged 1 commit into from
Mar 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions class.jetpack-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ public static function enqueue_block_editor_assets() {
'wp-edit-post',
'wp-editor',
'wp-element',
'wp-escape-html',
'wp-hooks',
'wp-i18n',
'wp-keycodes',
Expand Down
28 changes: 13 additions & 15 deletions extensions/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
# Jetpack Block Editor Extensions

This directory lists extensions for the Block Editor, also known as Gutenberg, [that was introduced in WordPress 5.0](https://wordpress.org/news/2018/12/bebo/).
This directory lists extensions for the Block Editor, also known as Gutenberg,
[that was introduced in WordPress 5.0](https://wordpress.org/news/2018/12/bebo/).

## Extension Type

We define different types of block editor extensions:

- Blocks are available in the editor itself, and live in the `blocks` directory.
- Plugins are available in the Jetpack sidebar that appears on the right side of the block editor. Those live in the `plugins` directory.

When adding a new extension, add a new directory for your extension the matching directory.
- Blocks are available in the editor itself.
- Plugins are available in the Jetpack sidebar that appears on the right side of the block editor.

## Extension Structure

Your extension should follow this structure:
Extensions loosely follow this structure:

```
.
└── blockname/
└── blockname.php ← PHP file where the block and its assets are registered.
└── block-or-plugin-name/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #11333 (unmerged) @ockham suggested separate folder for Plugins: extensions/plugins/seo/seo.php

Thoughts?

Copy link
Member

@simison simison Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose extension could be both plugins and blocks (and filters and whatnot) at the same time, so it might not make sense to optimize this way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be easier for our build tools and for new contributors if everything was in one place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does /extensions/blocks then make sense and should we just have /extensions ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the things in /blocks are actually blocks.

#11640 includes additional extensions (the sidebar and category) that aren't blocks. They are not located under blocks.

├── block-or-plugin-name.php ← PHP file where the block and its assets are registered.
├── editor.js ← script loaded only in the editor
├── editor.scss ← styles loaded only in the editor
├── view.js ← script loaded in the editor and theme
└── view.scss ← styles loaded in the editor and theme
```

If your block depends on another block, place them all in extensions folder:

```
.
├── blockname/
├── block-name/
└── sub-blockname/
```

**Note that this directory is still being populated. For now, you can find the blocks [here](https://github.com/Automattic/wp-calypso/tree/master/client/gutenberg/extensions).

## Develop new blocks

You can follow [the instructions here](../docs/guides/gutenberg-blocks.md) to add your own block to Jetpack.

## Block naming conventions
Coming when [#11640](https://github.com/Automattic/jetpack/pull/11640) lands.

Blocks should use the `jetpack/` prefix, e.g. `jetpack/markdown`.
200 changes: 200 additions & 0 deletions extensions/blocks/business-hours/components/day-edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash';
import { Component, Fragment } from '@wordpress/element';
import { IconButton, TextControl, ToggleControl } from '@wordpress/components';
import classNames from 'classnames';

/**
* Internal dependencies
*/
import { __ } from '../../../utils/i18n';

const defaultOpen = '09:00';
const defaultClose = '17:00';

class DayEdit extends Component {
renderInterval = ( interval, intervalIndex ) => {
const { day } = this.props;
const { opening, closing } = interval;
return (
<Fragment key={ intervalIndex }>
<div className="business-hours__row">
<div className={ classNames( day.name, 'business-hours__day' ) }>
{ intervalIndex === 0 && this.renderDayToggle() }
</div>
<div className={ classNames( day.name, 'business-hours__hours' ) }>
<TextControl
type="time"
label={ __( 'Opening' ) }
value={ opening }
className="business-hours__open"
placeholder={ defaultOpen }
onChange={ value => {
this.setHour( value, 'opening', intervalIndex );
} }
/>
<TextControl
type="time"
label={ __( 'Closing' ) }
value={ closing }
className="business-hours__close"
placeholder={ defaultClose }
onChange={ value => {
this.setHour( value, 'closing', intervalIndex );
} }
/>
</div>
<div className="business-hours__remove">
{ day.hours.length > 1 && (
<IconButton
isSmall
isLink
icon="trash"
onClick={ () => {
this.removeInterval( intervalIndex );
} }
/>
) }
</div>
</div>
{ intervalIndex === day.hours.length - 1 && (
<div className="business-hours__row business-hours-row__add">
<div className={ classNames( day.name, 'business-hours__day' ) }>&nbsp;</div>
<div className={ classNames( day.name, 'business-hours__hours' ) }>
<IconButton isLink label={ __( 'Add Hours' ) } onClick={ this.addInterval }>
{ __( 'Add Hours' ) }
</IconButton>
</div>
<div className="business-hours__remove">&nbsp;</div>
</div>
) }
</Fragment>
);
};

setHour = ( hourValue, hourType, hourIndex ) => {
const { day, attributes, setAttributes } = this.props;
const { days } = attributes;
setAttributes( {
days: days.map( value => {
if ( value.name === day.name ) {
return {
...value,
hours: value.hours.map( ( hour, index ) => {
if ( index === hourIndex ) {
return {
...hour,
[ hourType ]: hourValue,
};
}
return hour;
} ),
};
}
return value;
} ),
} );
};

toggleClosed = nextValue => {
const { day, attributes, setAttributes } = this.props;
const { days } = attributes;

setAttributes( {
days: days.map( value => {
if ( value.name === day.name ) {
const hours = nextValue
? [
{
opening: defaultOpen,
closing: defaultClose,
},
]
: [];
return {
...value,
hours,
};
}
return value;
} ),
} );
};

addInterval = () => {
const { day, attributes, setAttributes } = this.props;
const { days } = attributes;
day.hours.push( { opening: '', closing: '' } );
setAttributes( {
days: days.map( value => {
if ( value.name === day.name ) {
return {
...value,
hours: day.hours,
};
}
return value;
} ),
} );
};

removeInterval = hourIndex => {
const { day, attributes, setAttributes } = this.props;
const { days } = attributes;

setAttributes( {
days: days.map( value => {
if ( day.name === value.name ) {
return {
...value,
hours: value.hours.filter( ( hour, index ) => {
return hourIndex !== index;
} ),
};
}
return value;
} ),
} );
};

isClosed() {
const { day } = this.props;
return isEmpty( day.hours );
}

renderDayToggle() {
const { day, localization } = this.props;
return (
<Fragment>
<span className="business-hours__day-name">{ localization.days[ day.name ] }</span>
<ToggleControl
label={ this.isClosed() ? __( 'Closed' ) : __( 'Open' ) }
checked={ ! this.isClosed() }
onChange={ this.toggleClosed }
/>
</Fragment>
);
}

renderClosed() {
const { day } = this.props;
return (
<div className="business-hours__row business-hours-row__closed">
<div className={ classNames( day.name, 'business-hours__day' ) }>
{ this.renderDayToggle() }
</div>
<div className={ classNames( day.name, 'closed', 'business-hours__hours' ) }>&nbsp;</div>
<div className="business-hours__remove">&nbsp;</div>
</div>
);
}

render() {
const { day } = this.props;
return this.isClosed() ? this.renderClosed() : day.hours.map( this.renderInterval );
}
}

export default DayEdit;
58 changes: 58 additions & 0 deletions extensions/blocks/business-hours/components/day-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import { date } from '@wordpress/date';
import { isEmpty } from 'lodash';
import { sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { _x } from '../../../utils/i18n';

class DayPreview extends Component {
formatTime( time ) {
const { timeFormat } = this.props;
const [ hours, minutes ] = time.split( ':' );
const _date = new Date();
if ( ! hours || ! minutes ) {
return false;
}
_date.setHours( hours );
_date.setMinutes( minutes );
return date( timeFormat, _date );
}

renderInterval = ( interval, key ) => {
return (
<dd key={ key }>
{ sprintf(
_x( 'From %s to %s', 'from business opening hour to closing hour' ),
this.formatTime( interval.opening ),
this.formatTime( interval.closing )
) }
</dd>
);
};

render() {
const { day, localization } = this.props;
const hours = day.hours.filter(
// remove any malformed or empty intervals
interval => this.formatTime( interval.opening ) && this.formatTime( interval.closing )
);
return (
<Fragment>
<dt className={ day.name }>{ localization.days[ day.name ] }</dt>
{ isEmpty( hours ) ? (
<dd>{ _x( 'Closed', 'business is closed on a full day' ) }</dd>
) : (
hours.map( this.renderInterval )
) }
</Fragment>
);
}
}

export default DayPreview;
Loading