UNPKG

11.1 kBJavaScriptView Raw
1/**
2 * External dependencies
3 */
4import classnames from 'classnames';
5
6/**
7 * WordPress dependencies
8 */
9import { Fragment, Component } from '@wordpress/element';
10import {
11 InspectorControls,
12 BlockControls,
13 RichText,
14 PanelColorSettings,
15 createCustomColorsHOC,
16} from '@wordpress/block-editor';
17import { __ } from '@wordpress/i18n';
18import {
19 PanelBody,
20 ToggleControl,
21 TextControl,
22 Button,
23 Toolbar,
24 DropdownMenu,
25} from '@wordpress/components';
26
27/**
28 * Internal dependencies
29 */
30import {
31 createTable,
32 updateCellContent,
33 insertRow,
34 deleteRow,
35 insertColumn,
36 deleteColumn,
37} from './state';
38
39const BACKGROUND_COLORS = [
40 {
41 color: '#f3f4f5',
42 name: 'Subtle light gray',
43 slug: 'subtle-light-gray',
44 },
45 {
46 color: '#e9fbe5',
47 name: 'Subtle pale green',
48 slug: 'subtle-pale-green',
49 },
50 {
51 color: '#e7f5fe',
52 name: 'Subtle pale blue',
53 slug: 'subtle-pale-blue',
54 },
55 {
56 color: '#fcf0ef',
57 name: 'Subtle pale pink',
58 slug: 'subtle-pale-pink',
59 },
60];
61
62const withCustomBackgroundColors = createCustomColorsHOC( BACKGROUND_COLORS );
63
64export class TableEdit extends Component {
65 constructor() {
66 super( ...arguments );
67
68 this.onCreateTable = this.onCreateTable.bind( this );
69 this.onChangeFixedLayout = this.onChangeFixedLayout.bind( this );
70 this.onChange = this.onChange.bind( this );
71 this.onChangeInitialColumnCount = this.onChangeInitialColumnCount.bind( this );
72 this.onChangeInitialRowCount = this.onChangeInitialRowCount.bind( this );
73 this.renderSection = this.renderSection.bind( this );
74 this.getTableControls = this.getTableControls.bind( this );
75 this.onInsertRow = this.onInsertRow.bind( this );
76 this.onInsertRowBefore = this.onInsertRowBefore.bind( this );
77 this.onInsertRowAfter = this.onInsertRowAfter.bind( this );
78 this.onDeleteRow = this.onDeleteRow.bind( this );
79 this.onInsertColumn = this.onInsertColumn.bind( this );
80 this.onInsertColumnBefore = this.onInsertColumnBefore.bind( this );
81 this.onInsertColumnAfter = this.onInsertColumnAfter.bind( this );
82 this.onDeleteColumn = this.onDeleteColumn.bind( this );
83
84 this.state = {
85 initialRowCount: 2,
86 initialColumnCount: 2,
87 selectedCell: null,
88 };
89 }
90
91 /**
92 * Updates the initial column count used for table creation.
93 *
94 * @param {number} initialColumnCount New initial column count.
95 */
96 onChangeInitialColumnCount( initialColumnCount ) {
97 this.setState( { initialColumnCount } );
98 }
99
100 /**
101 * Updates the initial row count used for table creation.
102 *
103 * @param {number} initialRowCount New initial row count.
104 */
105 onChangeInitialRowCount( initialRowCount ) {
106 this.setState( { initialRowCount } );
107 }
108
109 /**
110 * Creates a table based on dimensions in local state.
111 *
112 * @param {Object} event Form submit event.
113 */
114 onCreateTable( event ) {
115 event.preventDefault();
116
117 const { setAttributes } = this.props;
118 let { initialRowCount, initialColumnCount } = this.state;
119
120 initialRowCount = parseInt( initialRowCount, 10 ) || 2;
121 initialColumnCount = parseInt( initialColumnCount, 10 ) || 2;
122
123 setAttributes( createTable( {
124 rowCount: initialRowCount,
125 columnCount: initialColumnCount,
126 } ) );
127 }
128
129 /**
130 * Toggles whether the table has a fixed layout or not.
131 */
132 onChangeFixedLayout() {
133 const { attributes, setAttributes } = this.props;
134 const { hasFixedLayout } = attributes;
135
136 setAttributes( { hasFixedLayout: ! hasFixedLayout } );
137 }
138
139 /**
140 * Changes the content of the currently selected cell.
141 *
142 * @param {Array} content A RichText content value.
143 */
144 onChange( content ) {
145 const { selectedCell } = this.state;
146
147 if ( ! selectedCell ) {
148 return;
149 }
150
151 const { attributes, setAttributes } = this.props;
152 const { section, rowIndex, columnIndex } = selectedCell;
153
154 setAttributes( updateCellContent( attributes, {
155 section,
156 rowIndex,
157 columnIndex,
158 content,
159 } ) );
160 }
161
162 /**
163 * Inserts a row at the currently selected row index, plus `delta`.
164 *
165 * @param {number} delta Offset for selected row index at which to insert.
166 */
167 onInsertRow( delta ) {
168 const { selectedCell } = this.state;
169
170 if ( ! selectedCell ) {
171 return;
172 }
173
174 const { attributes, setAttributes } = this.props;
175 const { section, rowIndex } = selectedCell;
176
177 this.setState( { selectedCell: null } );
178 setAttributes( insertRow( attributes, {
179 section,
180 rowIndex: rowIndex + delta,
181 } ) );
182 }
183
184 /**
185 * Inserts a row before the currently selected row.
186 */
187 onInsertRowBefore() {
188 this.onInsertRow( 0 );
189 }
190
191 /**
192 * Inserts a row after the currently selected row.
193 */
194 onInsertRowAfter() {
195 this.onInsertRow( 1 );
196 }
197
198 /**
199 * Deletes the currently selected row.
200 */
201 onDeleteRow() {
202 const { selectedCell } = this.state;
203
204 if ( ! selectedCell ) {
205 return;
206 }
207
208 const { attributes, setAttributes } = this.props;
209 const { section, rowIndex } = selectedCell;
210
211 this.setState( { selectedCell: null } );
212 setAttributes( deleteRow( attributes, { section, rowIndex } ) );
213 }
214
215 /**
216 * Inserts a column at the currently selected column index, plus `delta`.
217 *
218 * @param {number} delta Offset for selected column index at which to insert.
219 */
220 onInsertColumn( delta = 0 ) {
221 const { selectedCell } = this.state;
222
223 if ( ! selectedCell ) {
224 return;
225 }
226
227 const { attributes, setAttributes } = this.props;
228 const { section, columnIndex } = selectedCell;
229
230 this.setState( { selectedCell: null } );
231 setAttributes( insertColumn( attributes, {
232 section,
233 columnIndex: columnIndex + delta,
234 } ) );
235 }
236
237 /**
238 * Inserts a column before the currently selected column.
239 */
240 onInsertColumnBefore() {
241 this.onInsertColumn( 0 );
242 }
243
244 /**
245 * Inserts a column after the currently selected column.
246 */
247 onInsertColumnAfter() {
248 this.onInsertColumn( 1 );
249 }
250
251 /**
252 * Deletes the currently selected column.
253 */
254 onDeleteColumn() {
255 const { selectedCell } = this.state;
256
257 if ( ! selectedCell ) {
258 return;
259 }
260
261 const { attributes, setAttributes } = this.props;
262 const { section, columnIndex } = selectedCell;
263
264 this.setState( { selectedCell: null } );
265 setAttributes( deleteColumn( attributes, { section, columnIndex } ) );
266 }
267
268 /**
269 * Creates an onFocus handler for a specified cell.
270 *
271 * @param {Object} selectedCell Object with `section`, `rowIndex`, and
272 * `columnIndex` properties.
273 *
274 * @return {Function} Function to call on focus.
275 */
276 createOnFocus( selectedCell ) {
277 return () => {
278 this.setState( { selectedCell } );
279 };
280 }
281
282 /**
283 * Gets the table controls to display in the block toolbar.
284 *
285 * @return {Array} Table controls.
286 */
287 getTableControls() {
288 const { selectedCell } = this.state;
289
290 return [
291 {
292 icon: 'table-row-before',
293 title: __( 'Add Row Before' ),
294 isDisabled: ! selectedCell,
295 onClick: this.onInsertRowBefore,
296 },
297 {
298 icon: 'table-row-after',
299 title: __( 'Add Row After' ),
300 isDisabled: ! selectedCell,
301 onClick: this.onInsertRowAfter,
302 },
303 {
304 icon: 'table-row-delete',
305 title: __( 'Delete Row' ),
306 isDisabled: ! selectedCell,
307 onClick: this.onDeleteRow,
308 },
309 {
310 icon: 'table-col-before',
311 title: __( 'Add Column Before' ),
312 isDisabled: ! selectedCell,
313 onClick: this.onInsertColumnBefore,
314 },
315 {
316 icon: 'table-col-after',
317 title: __( 'Add Column After' ),
318 isDisabled: ! selectedCell,
319 onClick: this.onInsertColumnAfter,
320 },
321 {
322 icon: 'table-col-delete',
323 title: __( 'Delete Column' ),
324 isDisabled: ! selectedCell,
325 onClick: this.onDeleteColumn,
326 },
327 ];
328 }
329
330 /**
331 * Renders a table section.
332 *
333 * @param {string} options.type Section type: head, body, or foot.
334 * @param {Array} options.rows The rows to render.
335 *
336 * @return {Object} React element for the section.
337 */
338 renderSection( { type, rows } ) {
339 if ( ! rows.length ) {
340 return null;
341 }
342
343 const Tag = `t${ type }`;
344 const { selectedCell } = this.state;
345
346 return (
347 <Tag>
348 { rows.map( ( { cells }, rowIndex ) => (
349 <tr key={ rowIndex }>
350 { cells.map( ( { content, tag: CellTag }, columnIndex ) => {
351 const isSelected = selectedCell && (
352 type === selectedCell.section &&
353 rowIndex === selectedCell.rowIndex &&
354 columnIndex === selectedCell.columnIndex
355 );
356
357 const cell = {
358 section: type,
359 rowIndex,
360 columnIndex,
361 };
362
363 const cellClasses = classnames( { 'is-selected': isSelected } );
364
365 return (
366 <CellTag
367 key={ columnIndex }
368 className={ cellClasses }
369 >
370 <RichText
371 className="wp-block-table__cell-content"
372 value={ content }
373 onChange={ this.onChange }
374 unstableOnFocus={ this.createOnFocus( cell ) }
375 />
376 </CellTag>
377 );
378 } ) }
379 </tr>
380 ) ) }
381 </Tag>
382 );
383 }
384
385 componentDidUpdate() {
386 const { isSelected } = this.props;
387 const { selectedCell } = this.state;
388
389 if ( ! isSelected && selectedCell ) {
390 this.setState( { selectedCell: null } );
391 }
392 }
393
394 render() {
395 const {
396 attributes,
397 className,
398 backgroundColor,
399 setBackgroundColor,
400 } = this.props;
401 const { initialRowCount, initialColumnCount } = this.state;
402 const { hasFixedLayout, head, body, foot } = attributes;
403 const isEmpty = ! head.length && ! body.length && ! foot.length;
404 const Section = this.renderSection;
405
406 if ( isEmpty ) {
407 return (
408 <form onSubmit={ this.onCreateTable }>
409 <TextControl
410 type="number"
411 label={ __( 'Column Count' ) }
412 value={ initialColumnCount }
413 onChange={ this.onChangeInitialColumnCount }
414 min="1"
415 />
416 <TextControl
417 type="number"
418 label={ __( 'Row Count' ) }
419 value={ initialRowCount }
420 onChange={ this.onChangeInitialRowCount }
421 min="1"
422 />
423 <Button isPrimary type="submit">{ __( 'Create' ) }</Button>
424 </form>
425 );
426 }
427
428 const classes = classnames( className, backgroundColor.class, {
429 'has-fixed-layout': hasFixedLayout,
430 'has-background': !! backgroundColor.color,
431 } );
432
433 return (
434 <Fragment>
435 <BlockControls>
436 <Toolbar>
437 <DropdownMenu
438 icon="editor-table"
439 label={ __( 'Edit table' ) }
440 controls={ this.getTableControls() }
441 />
442 </Toolbar>
443 </BlockControls>
444 <InspectorControls>
445 <PanelBody title={ __( 'Table Settings' ) } className="blocks-table-settings">
446 <ToggleControl
447 label={ __( 'Fixed width table cells' ) }
448 checked={ !! hasFixedLayout }
449 onChange={ this.onChangeFixedLayout }
450 />
451 </PanelBody>
452 <PanelColorSettings
453 title={ __( 'Color Settings' ) }
454 initialOpen={ false }
455 colorSettings={ [
456 {
457 value: backgroundColor.color,
458 onChange: setBackgroundColor,
459 label: __( 'Background Color' ),
460 disableCustomColors: true,
461 colors: BACKGROUND_COLORS,
462 },
463 ] }
464 />
465 </InspectorControls>
466 <table className={ classes }>
467 <Section type="head" rows={ head } />
468 <Section type="body" rows={ body } />
469 <Section type="foot" rows={ foot } />
470 </table>
471 </Fragment>
472 );
473 }
474}
475
476export default withCustomBackgroundColors( 'backgroundColor' )( TableEdit );