PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 2.6.6
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v2.6.6
3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 All 196 releases
convertkit / resources / backend / js / gutenberg.js

gutenberg.js in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 2.6.6, at resources/backend/js/gutenberg.js

811 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Registers blocks in the Gutenberg editor.
3 *
4 * @since 1.9.6.5
5 *
6 * @package ConvertKit
7 * @author ConvertKit
8 */
9
10 // Register Gutenberg Blocks if the Gutenberg Editor is loaded on screen.
11 // This prevents JS errors if this script is accidentally enqueued on a non-
12 // Gutenberg editor screen, or the Classic Editor Plugin is active.
13 if ( typeof wp !== 'undefined' &&
14 typeof wp.blocks !== 'undefined' ) {
15
16 // Register each ConvertKit Block in Gutenberg.
17 for ( const block in convertkit_blocks ) {
18 convertKitGutenbergRegisterBlock( convertkit_blocks[ block ] );
19 }
20
21 // Register ConvertKit Pre-publish actions in Gutenberg.
22 if ( typeof convertkit_pre_publish_actions !== 'undefined' ) {
23 convertKitGutenbergRegisterPrePublishActions( convertkit_pre_publish_actions );
24 }
25
26 }
27
28 /**
29 * Registers the given block in Gutenberg.
30 *
31 * @since 1.9.6.5
32 *
33 * @param object block Block
34 */
35 function convertKitGutenbergRegisterBlock( block ) {
36
37 ( function ( blocks, editor, element, components ) {
38
39 // Define some constants for the various items we'll use.
40 const el = element.createElement;
41 const { registerBlockType } = blocks;
42 const { InspectorControls } = editor;
43 const {
44 Fragment,
45 useState
46 } = element;
47 const {
48 Button,
49 Dashicon,
50 TextControl,
51 SelectControl,
52 ToggleControl,
53 Panel,
54 PanelBody,
55 PanelRow
56 } = components;
57
58 /**
59 * Returns the icon to display for this block, depending
60 * on the supplied block's configuration.
61 *
62 * @since 2.2.0
63 *
64 * @return element|string
65 */
66 const getIcon = function () {
67
68 // Return a fallback default icon if none is specified for this block.
69 if ( typeof block.gutenberg_icon === 'undefined' ) {
70 return 'dashicons-tablet';
71 }
72
73 // Return HTML element if the icon is an SVG string.
74 if ( block.gutenberg_icon.search( 'svg' ) >= 0 ) {
75 return element.RawHTML(
76 {
77 children: block.gutenberg_icon
78 }
79 );
80 }
81
82 // Just return the string, as it's a dashicon CSS class.
83 return block.gutenberg_icon;
84
85 }
86
87 /**
88 * Return a field element for the block sidebar, which is displayed in a panel's row
89 * when this block is being edited.
90 *
91 * @since 2.2.0
92 *
93 * @param object props Block properties.
94 * @param object field Field attributes.
95 * @param string attribute Attribute name to store the field's data in.
96 * @return array Field element
97 */
98 const getField = function ( props, field, attribute ) {
99
100 // Define some field properties shared across all field types.
101 let fieldProperties = {
102 id: 'convertkit_' + block.name + '_' + attribute,
103 label: field.label,
104 help: field.description,
105 value: props.attributes[ attribute ],
106 onChange: function ( value ) {
107 if ( field.type === 'number' ) {
108 // If value is a blank string i.e. no attribute value was provided,
109 // cast it to the field's minimum number setting.
110 // This prevents WordPress' block renderer API returning a 400 error
111 // because a blank value will be passed as a string, when WordPress
112 // expects it to be a numerical value.
113 if ( value === '' ) {
114 value = field.min;
115 }
116
117 // Cast value to integer if a value exists.
118 if ( value.length > 0 ) {
119 value = Number( value );
120 }
121 }
122
123 let newValue = {};
124 newValue[ attribute ] = value;
125 props.setAttributes( newValue );
126 }
127 };
128
129 // Define additional Field Properties and the Field Element,
130 // depending on the Field Type (select, textarea, text etc).
131 switch ( field.type ) {
132
133 case 'select':
134 // Build options for <select> input.
135 let fieldOptions = [];
136 fieldOptions.push(
137 {
138 label: '(None)',
139 value: '',
140 }
141 );
142 for ( let value in field.values ) {
143 fieldOptions.push(
144 {
145 label: field.values[ value ],
146 value: value
147 }
148 );
149 }
150
151 // Sort field's options alphabetically by label.
152 fieldOptions.sort(
153 function ( x, y ) {
154
155 let a = x.label.toUpperCase(),
156 b = y.label.toUpperCase();
157 return a.localeCompare( b );
158
159 }
160 );
161
162 // Assign options to field.
163 fieldProperties.options = fieldOptions;
164
165 // Return field element.
166 return el(
167 SelectControl,
168 fieldProperties
169 );
170 break;
171
172 case 'toggle':
173 // Define field properties.
174 fieldProperties.checked = props.attributes[ attribute ];
175
176 // Return field element.
177 return el(
178 ToggleControl,
179 fieldProperties
180 );
181 break;
182
183 case 'number':
184 // Define field properties.
185 fieldProperties.type = field.type;
186 fieldProperties.min = field.min;
187 fieldProperties.max = field.max;
188 fieldProperties.step = field.step;
189
190 // Return field element.
191 return el(
192 TextControl,
193 fieldProperties
194 );
195 break;
196
197 default:
198 // Return field element.
199 return el(
200 TextControl,
201 fieldProperties
202 );
203 break;
204 }
205
206 }
207
208 /**
209 * Return an array of rows to display in the given block sidebar's panel when
210 * this block is being edited.
211 *
212 * @since 2.2.0
213 *
214 * @param object props Block properties.
215 * @param string panel Panel name.
216 * @return array Panel rows
217 */
218 const getPanelRows = function ( props, panel ) {
219
220 // Build Inspector Control Panel Rows, one for each Field.
221 let rows = [];
222 for ( let i in block.panels[ panel ].fields ) {
223 const attribute = block.panels[ panel ].fields[ i ], // e.g. 'term'.
224 field = block.fields[ attribute ]; // field array.
225
226 // If this field doesn't exist as an attribute in the block's get_attributes(),
227 // this is a non-Gutenberg field (such as a color picker for shortcodes),
228 // which should be ignored.
229 if ( typeof block.attributes[ attribute ] === 'undefined' ) {
230 continue;
231 }
232
233 rows.push(
234 el(
235 PanelRow,
236 {
237 key: attribute
238 },
239 getField( props, field, attribute )
240 )
241 );
242 }
243
244 return rows;
245
246 }
247
248 /**
249 * Return an array of panels to display in the block's sidebar when the block
250 * is being edited.
251 *
252 * @since 2.2.0
253 *
254 * @param object props Block formatter properties.
255 * @return array Block sidebar panels.
256 */
257 const getPanels = function ( props ) {
258
259 let panels = [],
260 initialOpen = true;
261
262 // Build Inspector Control Panels.
263 for ( const panel in block.panels ) {
264 let panelRows = getPanelRows( props, panel );
265
266 // If no panel rows exist (e.g. this is a shortcode only panel,
267 // for styles, which Gutenberg registers in its own styles tab),
268 // don't add this panel.
269 if ( ! panelRows.length ) {
270 continue;
271 }
272
273 panels.push(
274 el(
275 PanelBody,
276 {
277 title: block.panels[ panel ].label,
278 key: panel,
279 initialOpen: initialOpen
280 },
281 panelRows
282 )
283 );
284
285 // Don't open any further panels.
286 initialOpen = false;
287 }
288
289 return panels;
290
291 }
292
293 /**
294 * Display settings sidebar when the block is being edited, and save
295 * changes that are made.
296 *
297 * @since 2.2.0
298 *
299 * @param object props Block properties.
300 * @return object Block settings sidebar elements
301 */
302 const editBlock = function ( props ) {
303
304 // If requesting an example of how this block looks (which is requested
305 // when the user adds a new block and hovers over this block's icon),
306 // show the preview image.
307 if ( props.attributes.is_gutenberg_example === true ) {
308 return (
309 Fragment,
310 {},
311 el(
312 'img',
313 {
314 src: block.gutenberg_example_image,
315 }
316 )
317 );
318 }
319
320 // Build Inspector Control Panels, which will appear in the Sidebar when editing the Block.
321 let panels = getPanels( props );
322
323 // Generate Block Preview.
324 let preview = '';
325
326 // If no API Key has been defined in the Plugin, or no resources exist in ConvertKit
327 // for this block, show a message in the block to tell the user what to do.
328 if ( ! block.has_access_token || ! block.has_resources ) {
329 return displayNoticeWithLink( props );
330 }
331
332 if ( typeof block.gutenberg_preview_render_callback !== 'undefined' ) {
333 // Use a custom callback function to render this block's preview in the Gutenberg Editor.
334 // This doesn't affect the output for this block on the frontend site, which will always
335 // use the block's PHP's render() function.
336 preview = window[ block.gutenberg_preview_render_callback ]( block, props );
337 }
338
339 // Return settings sidebar panel with fields and the block preview.
340 return (
341 el(
342 // Sidebar Panel with Fields.
343 Fragment,
344 {},
345 el(
346 InspectorControls,
347 {},
348 panels
349 ),
350 // Block Preview.
351 preview
352 )
353 );
354
355 }
356
357 /**
358 * Display a notice in the block with a clickable link to perform an action, and a refresh
359 * button to trigger editBlock(). Typically used when no API key exists in the Plugin,
360 * or no resources (forms, products) exist in ConvertKit.
361 *
362 * @since 2.2.5
363 *
364 * @param object props Block properties.
365 * @return object Notice.
366 */
367 const displayNoticeWithLink = function ( props ) {
368
369 // useState to toggle the refresh button's disabled state.
370 const [ buttonDisabled, setButtonDisabled ] = useState( false );
371
372 // Holds the array of elements to display in the notice component.
373 let elements;
374
375 // Define elements to display, based on whether the refresh button is disabled.
376 if ( buttonDisabled ) {
377 // Refresh button disabled; display a spinner and the button.
378 elements = [
379 spinner( props ),
380 refreshButton( props, buttonDisabled, setButtonDisabled )
381 ];
382 } else {
383 // Refresh button enabled; display the notice, link and button.
384 elements = [
385 ( ! block.has_access_token ? block.no_access_token.notice : block.no_resources.notice ),
386 noticeLink( props, setButtonDisabled ),
387 refreshButton( props, buttonDisabled, setButtonDisabled )
388 ];
389 }
390
391 // Return the element.
392 return el(
393 'div',
394 {
395 // convertkit-no-content class allows resources/backend/css/gutenberg.css
396 // to apply styling/branding to the block.
397 className: 'convertkit-' + block.name + ' convertkit-no-content'
398 },
399 elements
400 );
401
402 }
403
404 /**
405 * Returns a spinner element, to show that a block is loading / refreshing.
406 *
407 * @since 2.2.6
408 *
409 * @param object props Block properties.
410 * @return object Spinner.
411 */
412 const spinner = function ( props ) {
413
414 return el(
415 'span',
416 {
417 key: props.clientId + '-spinner',
418 className: 'spinner is-active'
419 }
420 );
421
422 }
423
424 /**
425 * Returns a WordPress Dashicon element.
426 *
427 * @since 2.2.6
428 *
429 * @param string iconName Dashicon Name.
430 * @return object Dashicon.
431 */
432 const dashIcon = function ( iconName ) {
433
434 return el(
435 Dashicon,
436 {
437 icon: iconName
438 }
439 );
440
441 }
442
443 /**
444 * Returns the notice link for the displayNoticeWithLink element.
445 *
446 * @since 2.2.6
447 *
448 * @param object props Block properties.
449 * @param object setButtonDisabled Function to enable or disable the refresh button.
450 * @return object Notice Link.
451 */
452 const noticeLink = function ( props, setButtonDisabled ) {
453
454 return el(
455 'a',
456 {
457 key: props.clientId + '-notice-link',
458 href: ( ! block.has_access_token ? block.no_access_token.link : block.no_resources.link ),
459 className: ( ! block.has_access_token ? 'convertkit-block-modal' : '' ),
460 target: '_blank',
461 onClick: function ( e ) {
462
463 // Show popup window with setup wizard if we need to define an API Key.
464 if ( ! block.has_access_token ) {
465 e.preventDefault();
466 showConvertKitPopupWindow( props, e.target, setButtonDisabled );
467 }
468
469 // Allow the link to load, as it's likely a link to the ConvertKit site.
470 }
471 },
472 ( ! block.has_access_token ? block.no_access_token.link_text : block.no_resources.link_text )
473 );
474
475 }
476
477 /**
478 * Returns a refresh button, used to refresh a block when it has no API Keys
479 * or resources.
480 *
481 * @since 2.2.6
482 *
483 * @param object props Block properties.
484 * @param bool buttonDisabled Whether the refresh button is disabled (true) or enabled (false)/
485 * @param object setButtonDisabled Function to enable or disable the refresh button.
486 * @return object Button.
487 */
488 const refreshButton = function ( props, buttonDisabled, setButtonDisabled ) {
489
490 return el(
491 Button,
492 {
493 key: props.clientId + '-refresh-button',
494 className: 'button button-secondary convertkit-block-refresh',
495 disabled: buttonDisabled,
496 text: 'Refresh',
497 icon: dashIcon( 'update' ),
498 onClick: function () {
499
500 // Refresh block definitions.
501 refreshBlocksDefinitions( props, setButtonDisabled );
502
503 }
504 }
505 )
506
507 }
508
509 /**
510 * Displays a new window with a given width and height to display the given URL.
511 *
512 * Typically used for displaying a modal version of the Setup Wizard, where the
513 * user clicks the 'Click here to connect your ConvertKit account' link in a block, and then
514 * enters their API Key and Secret. Will be used to show the ConvertKit
515 * OAuth window in the future.
516 *
517 * @since 2.2.6
518 *
519 * @param object props Block properties.
520 * @param object link Link that was clicked.
521 * @param object setButtonDisabled Function to enable or disable the refresh button.
522 */
523 const showConvertKitPopupWindow = function ( props, link, setButtonDisabled ) {
524
525 // Define popup width, height and positioning.
526 const width = 640,
527 height = 750,
528 top = ( window.screen.height - height ) / 2,
529 left = ( window.screen.width - width ) / 2;
530
531 // Open popup.
532 const convertKitPopup = window.open(
533 link.href + '&convertkit-modal=1',
534 'convertkit_popup_window',
535 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' + width + ',height=' + height + ',top=' + top + ',left=' + left
536 );
537
538 // Center popup and focus.
539 convertKitPopup.moveTo( left, top );
540 convertKitPopup.focus();
541
542 // Refresh the block when the popup is closed using self.close().
543 // Won't fire if the user closes the popup manually, which is fine because that means
544 // they didn't complete the steps, so refreshing wouldn't show anything new.
545 // The onbeforeunload would seem suitable here, but it fires whenever the popup window's
546 // document changes (e.g. as the user steps through a wizard), and doesn't fire when
547 // the window is closed.
548 // See https://stackoverflow.com/questions/9388380/capture-the-close-event-of-popup-window-in-javascript/48240128#48240128.
549 var convertKitPopupTimer = setInterval(
550 function () {
551 if ( convertKitPopup.closed ) {
552 clearInterval( convertKitPopupTimer );
553
554 // Refresh block.
555 refreshBlocksDefinitions( props, setButtonDisabled );
556 }
557 },
558 1000
559 );
560
561 }
562
563 /**
564 * Refreshes this block's properties by:
565 * - making an AJAX call to fetch all registered blocks via convertkit_get_blocks(),
566 * - storing the registered blocks in the `convertkit_blocks` global object,
567 * - updating this block's properties by updating the `block` object.
568 *
569 * @since 2.2.6
570 *
571 * @param object props Block properties.
572 * @param object setButtonDisabled Function to enable or disable the refresh button.
573 * @return object Notice.
574 */
575 const refreshBlocksDefinitions = function ( props, setButtonDisabled ) {
576
577 // Define data for WordPress AJAX request.
578 let data = new FormData();
579 data.append( 'action', 'convertkit_get_blocks' );
580 data.append( 'nonce', convertkit_gutenberg.get_blocks_nonce );
581
582 // Disable the button.
583 setButtonDisabled( true );
584
585 // Send AJAX request.
586 fetch(
587 ajaxurl,
588 {
589 method: 'POST',
590 credentials: 'same-origin',
591 body: data
592 }
593 )
594 .then(
595 function ( response ) {
596
597 // Convert response JSON string to object.
598 return response.json();
599
600 }
601 )
602 .then(
603 function ( response ) {
604
605 // Update global ConvertKit Blocks object, so that any updated resources
606 // are reflected when adding new ConvertKit Blocks.
607 convertkit_blocks = response.data;
608
609 // Update this block's properties, so that has_access_token, has_resources
610 // and the resources properties are updated.
611 block = convertkit_blocks[ block.name ];
612
613 // Call setAttributes on props to trigger the editBlock() function, which will re-render
614 // the block, reflecting any changes to its properties.
615 props.setAttributes(
616 {
617 refresh: Date.now()
618 }
619 );
620
621 // Enable refresh button.
622 setButtonDisabled( false );
623
624 }
625 )
626 .catch(
627 function ( error ) {
628
629 // Show an error in the Gutenberg editor.
630 wp.data.dispatch( 'core/notices' ).createErrorNotice(
631 'ConvertKit: ' + error,
632 {
633 id: 'convertkit-error'
634 }
635 );
636
637 // Enable refresh button.
638 setButtonDisabled( false );
639
640 }
641 );
642
643 }
644
645 // Register Block.
646 registerBlockType(
647 'convertkit/' + block.name,
648 {
649 title: block.title,
650 description:block.description,
651 category: block.category,
652 icon: getIcon,
653 keywords: block.keywords,
654 attributes: block.attributes,
655 supports: block.supports,
656 example: {
657 attributes: {
658 is_gutenberg_example: true,
659 }
660 },
661
662 // Editor.
663 edit: editBlock,
664
665 // Output.
666 save: function ( props ) {
667
668 // Deliberate; preview in the editor is determined by the return statement in `edit` above.
669 // On the frontend site, the block's render() PHP class is always called, so we dynamically
670 // fetch the content.
671 return null;
672
673 },
674 }
675 );
676
677 } (
678 window.wp.blocks,
679 window.wp.blockEditor,
680 window.wp.element,
681 window.wp.components
682 ) );
683
684 }
685
686 /**
687 * Registers pre-publish actions in Gutenberg's pre-publish checks panel.
688 *
689 * @since 2.4.0
690 *
691 * @param object actions Pre-publish actions.
692 */
693 function convertKitGutenbergRegisterPrePublishActions( actions ) {
694
695 ( function ( plugins, editPost, element, components, data ) {
696
697 // Define some constants for the various items we'll use.
698 const el = element.createElement;
699 const { ToggleControl } = components;
700 const { registerPlugin } = plugins;
701 const { PluginPrePublishPanel } = editPost;
702 const { useSelect, useDispatch, select } = data;
703
704 /**
705 * Returns a PluginPrePublishPanel for this Plugin, comprising of all
706 * pre-publish actions.
707 *
708 * @since 2.4.0
709 *
710 * @return PluginPrePublishPanel
711 */
712 const renderPanel = function () {
713
714 // Bail if the Post Type isn't a Post.
715 if ( select( 'core/editor' ).getCurrentPostType() !== 'post' ) {
716 return;
717 }
718
719 // Build rows.
720 let rows = [];
721 for ( const [ name, action ] of Object.entries( actions ) ) {
722
723 const key = '_convertkit_action_' + action.name;
724 const { meta } = useSelect(
725 function ( select ) {
726 return {
727 meta: select( 'core/editor' ).getEditedPostAttribute( 'meta' ),
728 };
729 }
730 );
731 const { editPost } = useDispatch( 'core/editor', [ meta[ key ] ] );
732
733 // Add row.
734 rows.push(
735 el(
736 ToggleControl,
737 {
738 id: 'convertkit_action_' + action.name,
739 label: action.label,
740 help: action.description,
741 value: true,
742 checked: meta[ key ],
743 onChange: function ( value ) {
744 editPost(
745 {
746 meta: { [ key ]: value },
747 }
748 );
749 }
750 }
751 )
752 );
753 }
754
755 // Return actions in the pre-publish panel.
756 return el(
757 PluginPrePublishPanel,
758 {
759 className: 'convertkit-pre-publish-actions',
760 title: 'ConvertKit',
761 initialOpen: true,
762 },
763 rows
764 );
765
766 }
767
768 // Register pre-publish actions.
769 registerPlugin(
770 'convertkit-pre-publish-actions',
771 {
772 render: renderPanel
773 }
774 );
775
776 } (
777 window.wp.plugins,
778 window.wp.editPost,
779 window.wp.element,
780 window.wp.components,
781 window.wp.data
782 ) );
783
784 }
785
786 /**
787 * Outputs a notice for the block. Typically used when a block's settings
788 * have not been defined, no API key exists in the Plugin or no resources
789 * (forms, products) exist in ConvertKit, and the user adds an e.g.
790 * Form / Product block.
791 *
792 * @since 2.2.3
793 *
794 * @param string block_name Block Name.
795 * @param string notice Notice to display.
796 * @return object HTMLElement
797 */
798 function convertKitGutenbergDisplayBlockNotice( block_name, notice ) {
799
800 return wp.element.createElement(
801 'div',
802 {
803 // convertkit-no-content class allows resources/backend/css/gutenberg.css
804 // to apply styling/branding to the block.
805 className: 'convertkit-' + block_name + ' convertkit-no-content'
806 },
807 notice
808 );
809
810 }
811