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 extensions source #11685

Merged
merged 2 commits into from
Mar 26, 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
13 changes: 13 additions & 0 deletions .svnignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ yarn.lock
docker
bin/pre-commit-hook.js
yarn-error.log
extensions/**/*.css
extensions/**/*.gif
extensions/**/*.jpeg
extensions/**/*.jpg
extensions/**/*.js
extensions/**/*.json
extensions/**/*.jsx
extensions/**/*.md
extensions/**/*.png
extensions/**/*.sass
extensions/**/*.scss
extensions/**/*.svg
**/__snapshots__
42 changes: 35 additions & 7 deletions bin/build-asset-cdn-json.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
<?php

$path = dirname( dirname( __FILE__ ) ) . '/';
// The repo root path.
$path = dirname( dirname( __FILE__ ) ) . '/';

// Build an iterator over all files in the repo that match the regex in the RegexIterator.
$directory = new RecursiveDirectoryIterator( $path );
$iterator = new RecursiveIteratorIterator( $directory );
$regex = new RegexIterator( $iterator, '/^.+\.(css|js)$/i', RecursiveRegexIterator::GET_MATCH );

$ignore_paths = array(
'_inc/client/',
'bin/',
'docker/',
'docs/',
'extensions/',
'logs/',
'node_modules/',
'tests/',
'tools/',
'vendor/',
);

$manifest = array();
foreach ( $regex as $file => $value ) {
$file = str_replace( $path, '', $file );
$directory = substr( $file, 0, strpos( $file, '/' ) );
if ( in_array( $directory, array( 'node_modules', 'tests' ) ) ) {
foreach ( $regex as $path_to_file => $value ) {
$path_from_repo_root = str_replace( $path, '', $path_to_file );

// Ignore top-level files.
if ( false === strpos( $path_from_repo_root, '/' ) ) {
continue;
}
$manifest[] = $file;

// Ignore explicit ignore list.
foreach ( $ignore_paths as $ignore_path ) {
if ( 0 === strpos( $path_from_repo_root, $ignore_path ) ) {
continue 2;
}
}

$manifest[] = $path_from_repo_root;
}

$export = var_export( $manifest, true );

file_put_contents( $path . 'modules/photon-cdn/jetpack-manifest.php', "<?php \r\n\$assets = $export;\r\n" );
file_put_contents( $path . 'modules/photon-cdn/jetpack-manifest.php', "<?php
// This file is autogenerated by bin/build-asset-cdn-json.php
\$assets = $export;\r\n" );
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/
├── 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