PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.1.0
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.1.0
3.4.4 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 All 197 releases
convertkit / resources / backend / js / gutenberg.js

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

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