Skip to content

Commit ed854b6

Browse files
authored
Add Waves Block 🌊 (#86)
1 parent 49be30f commit ed854b6

34 files changed

+28906
-2
lines changed

‎blocks/waves/editor.scss‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* Editor styles */
2+
3+
.wp-block-a8c-waves {
4+
&__inner-container
5+
> .block-editor-inner-blocks
6+
> .block-editor-block-list__layout {
7+
margin-left: 0;
8+
margin-right: 0;
9+
}
10+
11+
&__resize-container:not( .is-resizing ) {
12+
// Important is used to have higher specificity than the inline style set by re-resizable library.
13+
height: auto !important;
14+
}
15+
16+
[data-align='left'] > &,
17+
[data-align='right'] > & {
18+
max-width: 290px;
19+
width: 100%;
20+
}
21+
}

‎blocks/waves/index.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[ "twgl/twgl.js", "waves.js" ]

‎blocks/waves/index.php‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
add_action( 'init', function() {
4+
register_block_type( 'a8c/waves', [
5+
'editor_script' => 'block-experiments',
6+
'style' => 'block-experiments',
7+
'editor_style' => 'block-experiments-editor',
8+
'render_callback' => function( $attribs, $content ) {
9+
wp_enqueue_script( 'a8c-waves-js' );
10+
return $content;
11+
},
12+
] );
13+
wp_register_script(
14+
'a8c-twgl-js',
15+
plugins_url( 'twgl/twgl.js', __FILE__ ),
16+
[], // no dependencies
17+
filemtime( plugin_dir_path( __FILE__ ) . 'twgl/twgl.js' ),
18+
true // in footer
19+
);
20+
wp_register_script(
21+
'a8c-waves-js',
22+
plugins_url( 'waves.js', __FILE__ ),
23+
[ 'a8c-twgl-js', 'wp-dom-ready' ],
24+
filemtime( plugin_dir_path( __FILE__ ) . 'waves.js' ),
25+
true // in footer
26+
);
27+
} );
28+
29+
add_action( 'enqueue_block_editor_assets', function() {
30+
wp_enqueue_script( 'a8c-waves-js' );
31+
} );

‎blocks/waves/src/edit.js‎

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import classnames from 'classnames';
5+
6+
/**
7+
* WordPress dependencies
8+
*/
9+
import {
10+
InspectorControls,
11+
PanelColorSettings,
12+
InnerBlocks,
13+
__experimentalUnitControl as UnitControl,
14+
} from '@wordpress/block-editor';
15+
import {
16+
PanelBody,
17+
RangeControl,
18+
ResizableBox,
19+
BaseControl,
20+
} from '@wordpress/components';
21+
import { useInstanceId } from '@wordpress/compose';
22+
import { useDispatch, useSelect } from '@wordpress/data';
23+
import { useState, useEffect, useRef } from '@wordpress/element';
24+
import { __ } from '@wordpress/i18n';
25+
26+
const DEFAULT_COLORS = {
27+
color1: '#000',
28+
color2: '#555',
29+
color3: '#AAA',
30+
color4: '#FFF',
31+
};
32+
33+
const MIN_HEIGHT = 50;
34+
35+
const CSS_UNITS = [
36+
{ value: 'px', label: 'px', default: 430 },
37+
{ value: 'em', label: 'em', default: 20 },
38+
{ value: 'rem', label: 'rem', default: 20 },
39+
{ value: 'vw', label: 'vw', default: 20 },
40+
{ value: 'vh', label: 'vh', default: 50 },
41+
];
42+
43+
const RESIZABLE_BOX_ENABLE_OPTION = {
44+
top: false,
45+
right: false,
46+
bottom: true,
47+
left: false,
48+
topRight: false,
49+
bottomRight: false,
50+
bottomLeft: false,
51+
topLeft: false,
52+
};
53+
54+
function HeightInput( { onChange, onUnitChange, unit = 'px', value = '' } ) {
55+
const [ temporaryInput, setTemporaryInput ] = useState( null );
56+
const instanceId = useInstanceId( UnitControl );
57+
const inputId = `a8c-waves-height-input-${ instanceId }`;
58+
const isPx = unit === 'px';
59+
60+
const handleOnChange = ( unprocessedValue ) => {
61+
const inputValue =
62+
unprocessedValue !== ''
63+
? parseInt( unprocessedValue, 10 )
64+
: undefined;
65+
66+
if ( isNaN( inputValue ) && inputValue !== undefined ) {
67+
setTemporaryInput( unprocessedValue );
68+
return;
69+
}
70+
setTemporaryInput( null );
71+
onChange( inputValue );
72+
};
73+
74+
const handleOnBlur = () => {
75+
if ( temporaryInput !== null ) {
76+
setTemporaryInput( null );
77+
}
78+
};
79+
80+
const inputValue = temporaryInput !== null ? temporaryInput : value;
81+
const min = isPx ? MIN_HEIGHT : 0;
82+
83+
return (
84+
<BaseControl
85+
label={ __( 'Minimum height of cover', 'waves' ) }
86+
id={ inputId }
87+
>
88+
<UnitControl
89+
id={ inputId }
90+
min={ min }
91+
onBlur={ handleOnBlur }
92+
onChange={ handleOnChange }
93+
onUnitChange={ onUnitChange }
94+
step="1"
95+
style={ { maxWidth: 80 } }
96+
unit={ unit }
97+
units={ CSS_UNITS }
98+
value={ inputValue }
99+
/>
100+
</BaseControl>
101+
);
102+
}
103+
104+
function Edit( { attributes, className, isSelected, setAttributes } ) {
105+
const { toggleSelection } = useDispatch( 'core/block-editor' );
106+
const [ temporaryMinHeight, setTemporaryMinHeight ] = useState( null );
107+
const [ isResizing, setIsResizing ] = useState( false );
108+
109+
const themeColors = useSelect(
110+
( select ) => select( 'core/block-editor' ).getSettings().colors,
111+
[]
112+
);
113+
const colors = {
114+
color1:
115+
attributes.color1 ||
116+
themeColors[ 0 ].color ||
117+
DEFAULT_COLORS.color1,
118+
color2:
119+
attributes.color2 ||
120+
themeColors[ 0 ].color ||
121+
DEFAULT_COLORS.color2,
122+
color3:
123+
attributes.color3 ||
124+
themeColors[ 1 % themeColors.length ].color ||
125+
DEFAULT_COLORS.color3,
126+
color4:
127+
attributes.color4 ||
128+
themeColors[ 2 % themeColors.length ].color ||
129+
DEFAULT_COLORS.color4,
130+
};
131+
132+
const renderPreview = ( newAttributes = {} ) =>
133+
window.a8cColorEffects.renderPreview( {
134+
complexity: attributes.complexity,
135+
mouseSpeed: 1,
136+
fluidSpeed: 1,
137+
...colors,
138+
...newAttributes,
139+
} );
140+
141+
useEffect( () => {
142+
// Defaults need to be saved in the attributes because they are dynamic
143+
// based on theme, and theme settings are not available from save.
144+
Object.entries( colors ).forEach( ( [ key, value ] ) => {
145+
if ( attributes[ key ] === undefined ) {
146+
setAttributes( { [ key ]: value } );
147+
}
148+
} );
149+
150+
// Save the initial preview in the attributes
151+
if ( attributes.previewImage === undefined ) {
152+
const previewImage = renderPreview();
153+
setAttributes( { previewImage } );
154+
}
155+
}, [] );
156+
157+
const minHeightWithUnit = attributes.minHeightUnit
158+
? `${ attributes.minHeight }${ attributes.minHeightUnit }`
159+
: attributes.minHeight;
160+
const style = {
161+
minHeight: temporaryMinHeight || minHeightWithUnit || undefined,
162+
backgroundImage: `url( "${ attributes.previewImage }" )`,
163+
};
164+
165+
const canvasRef = useRef();
166+
useEffect( () => {
167+
return window.a8cColorEffects.run( canvasRef.current );
168+
}, [ canvasRef.current ] );
169+
170+
return (
171+
<>
172+
<InspectorControls>
173+
<PanelBody title={ __( 'Animation', 'waves' ) } initialOpen>
174+
<RangeControl
175+
label={ __( 'Complexity', 'waves' ) }
176+
value={ attributes.complexity }
177+
onChange={ ( complexity ) => {
178+
const previewImage = renderPreview( {
179+
complexity,
180+
} );
181+
setAttributes( { complexity, previewImage } );
182+
} }
183+
min={ 1 }
184+
max={ 10 }
185+
/>
186+
<RangeControl
187+
label={ __( 'Mouse Speed', 'waves' ) }
188+
value={ attributes.mouseSpeed }
189+
onChange={ ( mouseSpeed ) =>
190+
setAttributes( { mouseSpeed } )
191+
}
192+
min={ 1 }
193+
max={ 100 }
194+
/>
195+
<RangeControl
196+
label={ __( 'Fluid Speed', 'waves' ) }
197+
value={ attributes.fluidSpeed }
198+
onChange={ ( fluidSpeed ) =>
199+
setAttributes( { fluidSpeed } )
200+
}
201+
min={ 1 }
202+
max={ 100 }
203+
/>
204+
</PanelBody>
205+
<PanelColorSettings
206+
title={ __( 'Color', 'waves' ) }
207+
initialOpen
208+
colorSettings={ [
209+
{
210+
label: __( 'Color 1', 'waves' ),
211+
value: colors.color1,
212+
onChange: ( color1 ) => {
213+
const previewImage = renderPreview( {
214+
color1,
215+
} );
216+
setAttributes( { color1, previewImage } );
217+
},
218+
},
219+
{
220+
label: __( 'Color 2', 'waves' ),
221+
value: colors.color2,
222+
onChange: ( color2 ) => {
223+
const previewImage = renderPreview( {
224+
color2,
225+
} );
226+
setAttributes( { color2, previewImage } );
227+
},
228+
},
229+
{
230+
label: __( 'Color 3', 'waves' ),
231+
value: colors.color3,
232+
onChange: ( color3 ) => {
233+
const previewImage = renderPreview( {
234+
color3,
235+
} );
236+
setAttributes( { color3, previewImage } );
237+
},
238+
},
239+
{
240+
label: __( 'Color 4', 'waves' ),
241+
value: colors.color4,
242+
onChange: ( color4 ) => {
243+
const previewImage = renderPreview( {
244+
color4,
245+
} );
246+
setAttributes( { color4, previewImage } );
247+
},
248+
},
249+
] }
250+
/>
251+
<PanelBody title={ __( 'Dimensions', 'waves' ) }>
252+
<HeightInput
253+
value={ temporaryMinHeight || attributes.minHeight }
254+
unit={ attributes.minHeightUnit }
255+
onChange={ ( minHeight ) =>
256+
setAttributes( { minHeight } )
257+
}
258+
onUnitChange={ ( minHeightUnit ) =>
259+
setAttributes( { minHeightUnit } )
260+
}
261+
/>
262+
</PanelBody>
263+
</InspectorControls>
264+
<ResizableBox
265+
className={ classnames( `${ className }__resize-container`, {
266+
'is-resizing': isResizing,
267+
} ) }
268+
enable={ RESIZABLE_BOX_ENABLE_OPTION }
269+
onResizeStart={ ( event, direction, elt ) => {
270+
toggleSelection( false );
271+
setAttributes( { minHeightUnit: 'px' } );
272+
setTemporaryMinHeight( elt.clientHeight );
273+
} }
274+
onResize={ ( event, direction, elt ) => {
275+
// Setting is-resizing here instead of onResizeStart fixes
276+
// an issue with the positioning of the resize bar when
277+
// starting a resize after having resized smaller than the
278+
// auto height
279+
setIsResizing( true );
280+
setTemporaryMinHeight( elt.clientHeight );
281+
} }
282+
onResizeStop={ ( event, direction, elt ) => {
283+
toggleSelection( true );
284+
setIsResizing( false );
285+
setAttributes( { minHeight: elt.clientHeight } );
286+
setTemporaryMinHeight( null );
287+
} }
288+
minHeight={ MIN_HEIGHT }
289+
showHandle={ isSelected }
290+
>
291+
<div className={ className } style={ style }>
292+
<canvas
293+
ref={ canvasRef }
294+
data-complexity={ attributes.complexity }
295+
data-mouse-speed={ attributes.mouseSpeed }
296+
data-fluid-speed={ attributes.fluidSpeed }
297+
data-color1={ colors.color1 }
298+
data-color2={ colors.color2 }
299+
data-color3={ colors.color3 }
300+
data-color4={ colors.color4 }
301+
/>
302+
<div className={ `${ className }__inner-container` }>
303+
<InnerBlocks
304+
template={ [
305+
[
306+
'core/paragraph',
307+
{
308+
align: 'center',
309+
fontSize: 'large',
310+
placeholder: __(
311+
'Write title…',
312+
'waves'
313+
),
314+
},
315+
],
316+
] }
317+
/>
318+
</div>
319+
</div>
320+
</ResizableBox>
321+
</>
322+
);
323+
}
324+
325+
export default Edit;

0 commit comments

Comments
 (0)