# convertkit/3.4.3/resources/backend/js/gutenberg.js

Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages, version 3.4.3. 1,745 lines.

- Page: https://pluginprobe.com/plugins/convertkit/3.4.3/code/resources/backend/js/gutenberg.js
- Raw: https://pluginprobe.com/plugins/convertkit/3.4.3/raw/resources/backend/js/gutenberg.js
- Modified: 2026-08-10T03:33:08+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/convertkit/3.4.3/code/resources/backend/js/gutenberg.js#L10-L20`.

```javascript
/**
 * Registers blocks in the Gutenberg editor.
 *
 * @since   1.9.6.5
 *
 * @author ConvertKit
 */

/**
 * @typedef {import('@wordpress/element').WPElement} WPElement
 */

// Register Gutenberg Blocks if the Gutenberg Editor is loaded on screen.
// This prevents JS errors if this script is accidentally enqueued on a non-
// Gutenberg editor screen, or the Classic Editor Plugin is active.
if (convertKitGutenbergEnabled()) {
	// Register each ConvertKit Block in Gutenberg.
	for (const block in convertkit_blocks) {
		convertKitGutenbergRegisterBlock(convertkit_blocks[block]);
	}

	if (convertKitEditingPostInGutenberg()) {
		// Register Plugin Sidebars in Gutenberg if we're editing a Post.
		if (typeof convertkit_plugin_sidebars !== 'undefined') {
			for (const pluginSidebar in convertkit_plugin_sidebars) {
				convertKitGutenbergRegisterPluginSidebar(
					convertkit_plugin_sidebars[pluginSidebar]
				);
			}
		}

		// Register ConvertKit Pre-publish actions in Gutenberg if we're editing a Post.
		if (typeof convertkit_pre_publish_actions !== 'undefined') {
			convertKitGutenbergRegisterPrePublishActions(
				convertkit_pre_publish_actions
			);
		}
	}
}

/**
 * Registers the given block in Gutenberg.
 *
 * @since 	1.9.6.5
 *
 * @param {Object} block Block.
 */
function convertKitGutenbergRegisterBlock(block) {
	(function (blocks, editor, element, components) {
		// Define some constants for the various items we'll use.
		const el = element.createElement;
		const { registerBlockType } = blocks;
		const { InspectorControls, InnerBlocks, useBlockProps } = editor;
		const { useState } = element;
		const {
			Button,
			Icon,
			TextControl,
			SelectControl,
			ToggleControl,
			Flex,
			FlexItem,
			PanelBody,
			PanelRow,
			ProgressBar,
		} = components;

		/**
		 * Returns the icon to display for this block, depending
		 * on the supplied block's configuration.
		 *
		 * @since   2.2.0
		 *
		 * @return  {WPElement|string} Either a WordPress element (RawHTML) or a dashicon string.
		 */
		const getIcon = function () {
			// Return a fallback default icon if none is specified for this block.
			if (typeof block.gutenberg_icon === 'undefined') {
				return 'dashicons-tablet';
			}

			// Return HTML element if the icon is an SVG string.
			if (block.gutenberg_icon.search('svg') >= 0) {
				return element.RawHTML({
					children: block.gutenberg_icon,
				});
			}

			// Just return the string, as it's a dashicon CSS class.
			return block.gutenberg_icon;
		};

		/**
		 * Return a field element for the block sidebar, which is displayed in a panel's row
		 * when this block is being edited.
		 *
		 * @since   2.2.0
		 *
		 * @param {Object} props     Block properties.
		 * @param {Object} field     Field attributes.
		 * @param {string} attribute Attribute name to store the field's data in.
		 * @return {Object}            Field element.
		 */
		const getField = function (props, field, attribute) {
			// If this field is conditionally displayed, check if the field should be displayed.
			if (typeof field.display_if !== 'undefined') {
				// Assume the condition has not been met for this field to be displayed.
				let display_field = false;

				// Assert whether the condition is met based on the field type.
				switch (block.fields[field.display_if.key].type) {
					case 'toggle':
						// Field's condition value will be 0 or 1.
						// Attributes field value will be false or true.
						display_field =
							Boolean(Number(field.display_if.value)) ===
							props.attributes[field.display_if.key];
						break;

					default:
						// Assert based on the condition's value type (array, string, number).
						switch (typeof field.display_if.value) {
							case 'object':
								display_field = Object.values(
									field.display_if.value
								).includes(
									props.attributes[field.display_if.key]
								);
								break;

							default:
								display_field =
									field.display_if.value ===
									props.attributes[field.display_if.key];
								break;
						}
						break;
				}

				// Skip this field if the condition is not met.
				if (!display_field) {
					return false;
				}
			}

			// Define some field properties shared across all field types.
			const fieldProperties = {
				id:
					'convertkit_' +
					block.name.replace(/-/g, '_') +
					'_' +
					attribute,
				label: field.label,
				help: field.description,
				value: props.attributes[attribute],

				// Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
				// preventing deprecation notices in the block editor and opt in to the new styles
				// from 7.0.
				__next40pxDefaultSize: true,
				__nextHasNoMarginBottom: true,

				onChange(value) {
					if (field.type === 'number') {
						// If value is a blank string i.e. no attribute value was provided,
						// cast it to the field's minimum number setting.
						// This prevents WordPress' block renderer API returning a 400 error
						// because a blank value will be passed as a string, when WordPress
						// expects it to be a numerical value.
						if (value === '') {
							value = field.min;
						}

						// Cast value to integer if a value exists.
						if (value.length > 0) {
							value = Number(value);
						}
					}

					const newValue = {};
					newValue[attribute] = value;
					props.setAttributes(newValue);
				},
			};

			const fieldOptions = [];

			// Define additional Field Properties and the Field Element,
			// depending on the Field Type (select, textarea, text etc).
			switch (field.type) {
				case 'select':
					// Build options for <select> input.
					fieldOptions.push({
						label: '(None)',
						value: '',
					});
					for (const value of Object.keys(field.values)) {
						fieldOptions.push({
							label: field.values[value],
							value,
						});
					}

					// If the block's saved value references a legacy form that
					// isn't in field.values (legacy forms are not offered as
					// new selection choices), append it here so the dropdown
					// still reflects the saved selection.
					if (
						fieldProperties.value &&
						field.legacy_values &&
						typeof field.legacy_values[fieldProperties.value] !==
							'undefined' &&
						!fieldOptions.some(
							(option) => option.value === fieldProperties.value
						)
					) {
						fieldOptions.push({
							label: field.legacy_values[fieldProperties.value],
							value: fieldProperties.value,
						});
					}

					// Sort field's options alphabetically by label.
					fieldOptions.sort(function (x, y) {
						const a = x.label.toUpperCase(),
							b = y.label.toUpperCase();
						return a.localeCompare(b);
					});

					// Assign options to field.
					fieldProperties.options = fieldOptions;

					// Return field element.
					return el(SelectControl, fieldProperties);

				case 'resource':
					// Build options for <select> input.
					fieldOptions.push({
						label: '(None)',
						value: '',
					});
					for (const value of Object.keys(field.values)) {
						fieldOptions.push({
							label: field.values[value],
							value,
						});
					}

					// If the block's saved value references a legacy form that
					// isn't in field.values (because legacy forms are not
					// offered as new selection choices), append it here so the
					// dropdown still reflects the saved selection.
					if (
						fieldProperties.value &&
						field.legacy_values &&
						typeof field.legacy_values[fieldProperties.value] !==
							'undefined' &&
						!fieldOptions.some(
							(option) => option.value === fieldProperties.value
						)
					) {
						fieldOptions.push({
							label: field.legacy_values[fieldProperties.value],
							value: fieldProperties.value,
						});
					}

					// Sort field's options alphabetically by label.
					fieldOptions.sort(function (x, y) {
						const a = x.label.toUpperCase(),
							b = y.label.toUpperCase();
						return a.localeCompare(b);
					});

					// Assign options to field.
					fieldProperties.options = fieldOptions;

					return el(
						Flex,
						{
							align: 'start',
						},
						[
							el(
								FlexItem,
								{
									key: attribute + '-select',
								},
								el(SelectControl, fieldProperties)
							),
							el(
								FlexItem,
								{
									key: attribute + '-refresh',
								},
								inlineRefreshButton(props)
							),
						]
					);

				case 'toggle':
					// Define field properties.
					fieldProperties.checked = props.attributes[attribute];

					// Return field element.
					return el(ToggleControl, fieldProperties);

				case 'number':
					// Define field properties.
					fieldProperties.type = field.type;
					fieldProperties.min = field.min;
					fieldProperties.max = field.max;
					fieldProperties.step = field.step;

					// Return field element.
					return el(TextControl, fieldProperties);

				default:
					// Return field element.
					return el(TextControl, fieldProperties);
			}
		};

		/**
		 * Return an array of rows to display in the given block sidebar's panel when
		 * this block is being edited.
		 *
		 * @since   2.2.0
		 *
		 * @param {Object} props Block properties.
		 * @param {string} panel Panel name.
		 * @return {Array}        Panel rows.
		 */
		const getPanelRows = function (props, panel) {
			// Build Inspector Control Panel Rows, one for each Field.
			const rows = [];
			for (const i in block.panels[panel].fields) {
				const attribute = block.panels[panel].fields[i], // e.g. 'term'.
					field = block.fields[attribute]; // field array.

				// If this field doesn't exist as an attribute in the block's get_attributes(),
				// this is a non-Gutenberg field (such as a color picker for shortcodes),
				// which should be ignored.
				if (typeof block.attributes[attribute] === 'undefined') {
					continue;
				}

				rows.push(
					el(
						PanelRow,
						{
							key: attribute,
						},
						getField(props, field, attribute)
					)
				);
			}

			return rows;
		};

		/**
		 * Return an array of panels to display in the block's sidebar when the block
		 * is being edited.
		 *
		 * @since   2.2.0
		 *
		 * @param {Object} props Block formatter properties.
		 * @return {Array} 	      Block sidebar panels.
		 */
		const getPanels = function (props) {
			const panels = [];
			let initialOpen = true;

			// Build Inspector Control Panels.
			for (const panel in block.panels) {
				const panelRows = getPanelRows(props, panel);

				// If no panel rows exist (e.g. this is a shortcode only panel,
				// for styles, which Gutenberg registers in its own styles tab),
				// don't add this panel.
				if (!panelRows.length) {
					continue;
				}

				panels.push(
					el(
						PanelBody,
						{
							title: block.panels[panel].label,
							key: panel,
							initialOpen,
						},
						panelRows
					)
				);

				// Don't open any further panels.
				initialOpen = false;
			}

			return panels;
		};

		/**
		 * Display settings sidebar when the block is being edited, and save
		 * changes that are made.
		 *
		 * @since   2.2.0
		 *
		 * @param {Object} props Block properties.
		 * @return {Object}       Block settings sidebar elements.
		 */
		const EditBlock = function (props) {
			const blockProps = useBlockProps();

			// Refresh button disabled state on DisplayNoticeWithLink.
			// This must be here to avoid React error on hook order change when the user e.g.
			// connects their Kit account from within the block itself.
			const [buttonDisabled, setButtonDisabled] = useState(false);

			// If requesting an example of how this block looks (which is requested
			// when the user adds a new block and hovers over this block's icon),
			// show the preview image.
			if (props.attributes.is_gutenberg_example === true) {
				return el(
					'div',
					blockProps,
					el('img', {
						src: block.gutenberg_example_image,
					})
				);
			}

			// If no access token has been defined in the Plugin, or no resources exist in Kit
			// for this block, show a message in the block to tell the user what to do.
			if (!block.has_access_token || !block.has_resources) {
				return DisplayNoticeWithLink(
					props,
					blockProps,
					buttonDisabled,
					setButtonDisabled
				);
			}

			// Build Inspector Control Panels, which will appear in the Sidebar when editing the Block.
			const panels = getPanels(props);

			// Generate Block Preview.
			let preview = '';

			// If a custom callback function to render this block's preview in the Gutenberg Editor
			// has been defined, use it.
			// This doesn't affect the output for this block on the frontend site, which will always
			// use the block's PHP's render() function.
			if (
				typeof block.gutenberg_preview_render_callback !== 'undefined'
			) {
				preview = window[block.gutenberg_preview_render_callback](
					block,
					props
				);
				return editBlockWithPanelsAndPreview(
					panels,
					preview,
					blockProps
				);
			}

			// If no settings have been defined for this block, render the block with a notice
			// with instructions on how to configure the block.
			if (
				typeof block.gutenberg_help_description_attribute !==
					'undefined' &&
				props.attributes[block.gutenberg_help_description_attribute] ===
					''
			) {
				preview = convertKitGutenbergDisplayBlockNotice(
					block.name,
					block.gutenberg_help_description
				);
				return editBlockWithPanelsAndPreview(
					panels,
					preview,
					blockProps
				);
			}

			// If no render_callback is defined, render the block.
			if (typeof block.gutenberg_template !== 'undefined') {
				// Build template for the new block.
				const template = [];
				for (const templateBlockName in block.gutenberg_template) {
					if (
						block.gutenberg_template.hasOwnProperty(
							templateBlockName
						)
					) {
						template.push([
							templateBlockName,
							block.gutenberg_template[templateBlockName],
						]);
					}
				}

				preview = el(
					'div',
					{},
					el(InnerBlocks, {
						template,
					})
				);
				return editBlockWithPanelsAndPreview(
					panels,
					preview,
					blockProps
				);
			}

			// Use the block's PHP's render() function by calling the ServerSideRender component.
			preview = el(wp.serverSideRender, {
				block: 'convertkit/' + block.name,
				attributes: props.attributes,

				// This is only output in the Gutenberg editor, so must be slightly different from the inner class name used to
				// apply styles with i.e. convertkit-block.name.
				className: 'convertkit-ssr-' + block.name,
			});
			return editBlockWithPanelsAndPreview(panels, preview, blockProps);
		};

		/**
		 * Display settings sidebar when the block is being edited, and save
		 * changes that are made.
		 *
		 * @since   3.0.0
		 *
		 * @param {Object} panels     Block panels.
		 * @param {Object} preview    Block preview.
		 * @param {Object} blockProps Block properties.
		 * @return {Object}           Block settings sidebar elements.
		 */
		const editBlockWithPanelsAndPreview = function (
			panels,
			preview,
			blockProps
		) {
			return el('div', blockProps, [
				el(InspectorControls, {}, panels),
				preview,
			]);
		};

		/**
		 * Save the block's content.
		 *
		 * @since   3.0.0
		 *
		 * @return {Object}       Block content.
		 */
		const saveBlock = function () {
			if (typeof block.gutenberg_template !== 'undefined') {
				// Use useBlockProps.save() to preserve styling classes and attributes
				// from block supports (colors, typography, spacing, etc.)
				const blockProps = useBlockProps.save();
				return el('div', blockProps, el(InnerBlocks.Content));
			}

			// Deliberate; preview in the editor is determined by the return statement in `edit` above.
			// On the frontend site, the block's render() PHP class is always called, so we dynamically
			// fetch the content.
			return null;
		};

		/**
		 * Display a notice in the block with a clickable link to perform an action, and a refresh
		 * button to trigger editBlock().  Typically used when no API key exists in the Plugin,
		 * or no resources (forms, products) exist in ConvertKit.
		 *
		 * @since 	2.2.5
		 *
		 * @param {Object}   props             Block properties.
		 * @param {Object}   blockProps        Block properties.
		 * @param {boolean}  buttonDisabled    Whether the refresh button is disabled (true) or enabled (false)
		 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
		 * @return {Object}                     Notice.
		 */
		const DisplayNoticeWithLink = function (
			props,
			blockProps,
			buttonDisabled,
			setButtonDisabled
		) {
			// Holds the array of elements to display in the notice component.
			let elements;

			// Define elements to display, based on whether the refresh button is disabled.
			if (buttonDisabled) {
				// Refresh button disabled; display a loading indicator and the button.
				elements = [
					loadingIndicator(props),
					refreshButton(props, buttonDisabled, setButtonDisabled),
				];
			} else {
				// Refresh button enabled; display the notice, link and button.
				elements = [
					el(
						'div',
						{
							key: props.clientId + '-notice',
						},
						!block.has_access_token
							? block.no_access_token.notice
							: block.no_resources.notice
					),
					noticeLink(props, setButtonDisabled),
					refreshButton(props, buttonDisabled, setButtonDisabled),
				];
			}

			// Return the element.
			return el(
				'div',
				blockProps,
				el(
					'div',
					{
						// convertkit-no-content class allows resources/backend/css/gutenberg.css
						// to apply styling/branding to the block.
						className:
							'convertkit-' +
							block.name +
							' convertkit-no-content',
					},
					elements
				)
			);
		};

		/**
		 * Returns an indeterminate progress bar element, to show that a block is loading / refreshing.
		 *
		 * @since 	2.2.6
		 *
		 * @param {Object} props Block properties.
		 * @return {Object}       Progress Bar.
		 */
		const loadingIndicator = function (props) {
			// If the ProgressBar component is not available i.e. WordPress < 6.3, return a spinner.
			if (typeof ProgressBar === 'undefined') {
				return el('span', {
					key: props.clientId + '-spinner',
					className: 'spinner is-active convertkit-block-refreshing',
				});
			}

			return el(ProgressBar, {
				key: props.clientId + '-progress-bar',
				className:
					'convertkit-progress-bar convertkit-block-refreshing',
			});
		};

		/**
		 * Returns a WordPress Icon element.
		 *
		 * @since 	2.7.7
		 *
		 * @param {string} iconName Icon Name.
		 * @return {Object} 		 Icon.
		 */
		const iconType = function (iconName) {
			return el(Icon, {
				icon: iconName,
			});
		};

		/**
		 * Returns the notice link for the DisplayNoticeWithLink element.
		 *
		 * @since 	2.2.6
		 *
		 * @param {Object}   props             Block properties.
		 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
		 * @return {Object}          			Notice Link.
		 */
		const noticeLink = function (props, setButtonDisabled) {
			// Get the URL to set the button to.
			const url = !block.has_access_token
				? block.no_access_token.link
				: block.no_resources.link;

			return el(
				Button,
				{
					key: props.clientId + '-notice-link',
					className: !block.has_access_token
						? 'convertkit-block-modal'
						: '',
					variant: 'link',
					onClick(e) {
						e.preventDefault();

						// Show popup window with setup wizard if we need to connect via OAuth.
						if (!block.has_access_token) {
							showConvertKitPopupWindow(
								props,
								url,
								setButtonDisabled
							);
							return;
						}

						// Allow the link to load, as it's likely a link to the Kit site.
						window.open(url, '_blank');
					},
				},
				!block.has_access_token
					? block.no_access_token.link_text
					: block.no_resources.link_text
			);
		};

		/**
		 * Returns a refresh button, used to refresh a block when it has no API Keys
		 * or resources.
		 *
		 * @since 	2.2.6
		 *
		 * @param {Object}   props             Block properties.
		 * @param {boolean}  buttonDisabled    Whether the refresh button is disabled (true) or enabled (false)
		 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
		 * @return {Object} 					Button.
		 */
		const refreshButton = function (
			props,
			buttonDisabled,
			setButtonDisabled
		) {
			return el(Button, {
				key: props.clientId + '-refresh-button',
				className:
					'wp-convertkit-refresh-resources' +
					(buttonDisabled ? ' is-refreshing' : ''),
				disabled: buttonDisabled,
				text: 'Refresh',
				icon: iconType('update'),
				variant: 'secondary',
				onClick() {
					// Refresh block definitions.
					refreshBlocksDefinitions(props, setButtonDisabled);
				},
			});
		};

		/**
		 * Returns an inline refresh button, used to refresh a block's resources.
		 *
		 * @since 	2.7.1
		 *
		 * @param {Object} props Block properties.
		 * @return {Object} 	  Button.
		 */
		const inlineRefreshButton = function (props) {
			return el(BlockInlineRefreshButton, props);
		};

		/**
		 * Returns a refresh button.
		 *
		 * @since 	2.7.1
		 *
		 * @param {Object} props Block properties.
		 * @return {Object} 	  Button.
		 */
		const BlockInlineRefreshButton = function (props) {
			const [buttonDisabled, setButtonDisabled] = useState(false);

			return el(Button, {
				key: props.clientId + '-refresh-button',
				className:
					'button button-secondary wp-convertkit-refresh-resources' +
					(buttonDisabled ? ' is-refreshing' : ''),
				disabled: buttonDisabled,
				icon: iconType('update'),
				onClick() {
					// Refresh block definitions.
					refreshBlocksDefinitions(props, setButtonDisabled);
				},
			});
		};

		/**
		 * Displays a new window with a given width and height to display the given URL.
		 *
		 * Typically used for displaying a modal version of the Setup Wizard, where the
		 * user clicks the 'Click here to connect your ConvertKit account' link in a block, and then
		 * enters their API Key and Secret.  Will be used to show the ConvertKit
		 * OAuth window in the future.
		 *
		 * @since 	2.2.6
		 *
		 * @param {Object}   props             Block properties.
		 * @param {string}   url               URL to display in the popup window.
		 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
		 */
		const showConvertKitPopupWindow = function (
			props,
			url,
			setButtonDisabled
		) {
			// Define popup width, height and positioning.
			const width = 640,
				height = 750,
				top = (window.screen.height - height) / 2,
				left = (window.screen.width - width) / 2;

			// Open popup.
			const convertKitPopup = window.open(
				url + '&convertkit-modal=1',
				'convertkit_popup_window',
				'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' +
					width +
					',height=' +
					height +
					',top=' +
					top +
					',left=' +
					left
			);

			// Center popup and focus.
			convertKitPopup.moveTo(left, top);
			convertKitPopup.focus();

			// Refresh the block when the popup is closed using self.close().
			// Won't fire if the user closes the popup manually, which is fine because that means
			// they didn't complete the steps, so refreshing wouldn't show anything new.
			// The onbeforeunload would seem suitable here, but it fires whenever the popup window's
			// document changes (e.g. as the user steps through a wizard), and doesn't fire when
			// the window is closed.
			// See https://stackoverflow.com/questions/9388380/capture-the-close-event-of-popup-window-in-javascript/48240128#48240128.
			const convertKitPopupTimer = setInterval(function () {
				if (convertKitPopup.closed) {
					clearInterval(convertKitPopupTimer);

					// Refresh block.
					refreshBlocksDefinitions(props, setButtonDisabled);
				}
			}, 1000);
		};

		/**
		 * Refreshes this block's properties by:
		 * - making an AJAX call to fetch all registered blocks via convertkit_get_blocks(),
		 * - storing the registered blocks in the `convertkit_blocks` global object,
		 * - updating this block's properties by updating the `block` object.
		 *
		 * @since 	2.2.6
		 *
		 * @param {Object}   props             Block properties.
		 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
		 */
		const refreshBlocksDefinitions = function (props, setButtonDisabled) {
			// Disable the button.
			if (typeof setButtonDisabled !== 'undefined') {
				setButtonDisabled(true);
			}

			// Send AJAX request.
			fetch(convertkit_gutenberg.ajaxurl, {
				method: 'GET',
				headers: {
					'Content-Type': 'application/json',
					'X-WP-Nonce': convertkit_gutenberg.get_blocks_nonce,
				},
			})
				.then(function (response) {
					// Convert response JSON string to object.
					return response.json();
				})
				.then(function (response) {
					// If the response includes a code, show an error notice.
					if (typeof response.code !== 'undefined') {
						// Show an error in the Gutenberg editor.
						wp.data
							.dispatch('core/notices')
							.createErrorNotice('Kit: ' + response.message, {
								id: 'convertkit-error',
							});

						// Enable refresh button.
						if (typeof setButtonDisabled !== 'undefined') {
							setButtonDisabled(false);
						}
						return;
					}

					// Update global ConvertKit Blocks object, so that any updated resources
					// are reflected when adding new ConvertKit Blocks.
					convertkit_blocks = response;

					// Update this block's properties, so that has_access_token, has_resources
					// and the resources properties are updated.
					block = convertkit_blocks[block.name];

					// Call setAttributes on props to trigger the editBlock() function, which will re-render
					// the block, reflecting any changes to its properties.
					props.setAttributes({
						refresh: Date.now(),
					});

					// Enable refresh button.
					if (typeof setButtonDisabled !== 'undefined') {
						setButtonDisabled(false);
					}
				})
				.catch(function (error) {
					// Show an error in the Gutenberg editor.
					wp.data
						.dispatch('core/notices')
						.createErrorNotice('Kit: ' + error, {
							id: 'convertkit-error',
						});

					// Enable refresh button.
					if (typeof setButtonDisabled !== 'undefined') {
						setButtonDisabled(false);
					}
				});
		};

		// Register Block.
		registerBlockType('convertkit/' + block.name, {
			apiVersion: convertkit_gutenberg.block_api_version,
			title: block.title,
			description: block.description,
			category: block.category,
			icon: getIcon,
			keywords: block.keywords,
			attributes: block.attributes,
			supports: block.supports,
			example: {
				attributes: {
					is_gutenberg_example: true,
				},
			},

			// Editor.
			edit: EditBlock,

			// Output.
			save: saveBlock,
		});
	})(
		window.wp.blocks,
		window.wp.blockEditor,
		window.wp.element,
		window.wp.components
	);
}

/**
 * Registers a Plugin Sidebar in Gutenberg.
 *
 * @since 3.3.0
 *
 * @param {Object} sidebar Plugin Sidebars
 */
function convertKitGutenbergRegisterPluginSidebar(sidebar) {
	(function (plugins, editor, element, components, data) {
		// Define some constants for the various items we'll use.
		const el = element.createElement;
		const { registerPlugin } = plugins;
		const { PluginSidebar } = editor;
		const { useState } = element;
		const {
			Icon,
			TextControl,
			SelectControl,
			Flex,
			FlexItem,
			FlexBlock,
			PanelBody,
			PanelRow,
			Button,
		} = components;
		const { useSelect, useDispatch, select } = data;

		/**
		 * Returns a PluginDocumentSettingPanel for this plugin, containing
		 * post-level settings.
		 *
		 * @since 3.3.0
		 */
		const RenderPanel = function () {
			const meta = useSelect(function (wpSelect) {
				return (
					wpSelect('core/editor').getEditedPostAttribute('meta') || {}
				);
			}, []);
			const { editPost: wpEditPost } = useDispatch('core/editor');
			const settings = meta[sidebar.meta_key] || sidebar.default_values;
			const currentPostType = select('core/editor').getCurrentPostType();

			// Seed each field's `values` into component state, so that when a
			// refresh button is clicked we can update the field's options
			// and trigger a re-render without mutating the global
			// convertkit_plugin_sidebars object.
			const [fieldValues, setFieldValues] = useState(function () {
				const initial = {};
				for (const key in sidebar.fields) {
					initial[key] = sidebar.fields[key].values;
				}
				return initial;
			});

			/**
			 * Updates the Post meta meta_key object.
			 *
			 * @since 3.3.0
			 *
			 * @param {string} key   Sub key within the meta_key object.
			 * @param {string} value Value to assign to the sub key.
			 */
			const updateSetting = function (key, value) {
				wpEditPost({
					meta: {
						[sidebar.meta_key]: Object.assign({}, settings, {
							[key]: value,
						}),
					},
				});
			};

			/**
			 * Return a field element for the settings panel.
			 *
			 * @since   3.3.0
			 *
			 * @param {Object} field Field properties.
			 * @param {string} key   Field name.
			 * @return {Object}        Field element.
			 */
			const getField = function (field, key) {
				// Override field.values with the latest values from state, which
				// may have been refreshed by clicking the refresh button.
				field = Object.assign({}, field, {
					values: fieldValues[key] || field.values,
				});

				// Build the help element. Supports HTML in the description by
				// rendering each line as a paragraph via element.RawHTML, which
				// allows inline tags like <code> and <a> to render correctly.
				let helpElement;
				if (Array.isArray(field.description)) {
					helpElement = el(
						'span',
						{},
						field.description.map(function (line, index) {
							return el(
								'p',
								{
									key: 'help-' + index,
									style: { margin: '0 0 0.5em 0' },
								},
								el(element.RawHTML, {}, line)
							);
						})
					);
				} else if (field.description) {
					helpElement = el(element.RawHTML, {}, field.description);
				}

				// Define some field properties shared across all field types.
				const fieldProperties = {
					key: 'convertkit_plugin_sidebar_' + key,
					id: 'convertkit_plugin_sidebar_' + key,
					label: field.label,
					help: helpElement,
					value: settings[key] || field.default_value || '',

					// Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
					// preventing deprecation notices in the block editor and opt in to the new styles
					// from 7.0.
					__next40pxDefaultSize: true,
					__nextHasNoMarginBottom: true,

					// Save Post Meta on value change.
					onChange(value) {
						updateSetting(key, value);
					},
				};

				// Define additional Field Properties and the Field Element,
				// depending on the Field Type (select, textarea, text etc).
				switch (field.type) {
					case 'select':
						// If the field has a resource_type, wrap the select in a
						// Flex container alongside a refresh button.
						if (field.resource_type) {
							const selectFieldProperties = Object.assign(
								{},
								fieldProperties,
								{ help: undefined }
							);

							return el(
								'div',
								{
									key:
										'convertkit_plugin_sidebar_' +
										key +
										'_wrapper',
								},
								el(
									Flex,
									{
										align: 'end',
										gap: 2,
									},
									[
										el(
											FlexBlock,
											{
												key: key + '-select',
											},
											getSelectField(
												field,
												selectFieldProperties
											)
										),
										el(
											FlexItem,
											{
												key: key + '-refresh',
											},
											el(InlineRefreshButton, {
												resource: field.resource_type,
												fieldKey: key,
											})
										),
									]
								),
								fieldProperties.help
									? el(
											'p',
											{
												key: key + '-help',
												className:
													'components-base-control__help',
											},
											fieldProperties.help
										)
									: null
							);
						}

						return getSelectField(field, fieldProperties);

					default:
						// Return field element.
						return el(TextControl, fieldProperties);
				}
			};

			/**
			 * Returns a select field element, with optgroups and options
			 * depending on the field's values.
			 *
			 * @since   3.3.1
			 *
			 * @param {Object} field           Field properties.
			 * @param {Object} fieldProperties Field properties.
			 * @return {Object}       Select field element.
			 */
			const getSelectField = function (field, fieldProperties) {
				// Check if any values are optgroups.
				const hasOptgroups = Object.keys(field.values).some(
					(subKey) =>
						typeof field.values[subKey] === 'object' &&
						field.values[subKey].label &&
						field.values[subKey].values
				);

				if (hasOptgroups) {
					const children = [];
					const seenValues = new Set();

					for (const value of Object.keys(field.values)) {
						if (
							typeof field.values[value] === 'object' &&
							field.values[value].label &&
							field.values[value].values
						) {
							// Optgroup.
							const groupChildren = [];
							for (const groupValue of Object.keys(
								field.values[value].values
							)) {
								seenValues.add(groupValue);
								groupChildren.push(
									el(
										'option',
										{
											value: groupValue,
											key: groupValue,
										},
										field.values[value].values[groupValue]
									)
								);
							}
							children.push(
								el(
									'optgroup',
									{
										label: field.values[value].label,
										key: value,
									},
									...groupChildren
								)
							);
						} else {
							// Option within optgroup.
							seenValues.add(value);
							children.push(
								el(
									'option',
									{ value, key: value },
									field.values[value]
								)
							);
						}
					}

					// If the saved value references a legacy form that isn't
					// otherwise in the dropdown, append it so the current
					// selection remains visible.
					if (
						fieldProperties.value &&
						field.legacy_values &&
						typeof field.legacy_values[fieldProperties.value] !==
							'undefined' &&
						!seenValues.has(fieldProperties.value)
					) {
						children.push(
							el(
								'option',
								{
									value: fieldProperties.value,
									key: fieldProperties.value,
								},
								field.legacy_values[fieldProperties.value]
							)
						);
					}

					return el(SelectControl, fieldProperties, ...children);
				}

				// Options only, no optgroups.
				const fieldOptions = [];
				for (const value of Object.keys(field.values)) {
					fieldOptions.push({
						label: field.values[value],
						value,
					});
				}

				// If the saved value references a legacy form that isn't in
				// field.values, append it so the current selection remains
				// visible in the dropdown.
				if (
					fieldProperties.value &&
					field.legacy_values &&
					typeof field.legacy_values[fieldProperties.value] !==
						'undefined' &&
					!fieldOptions.some(
						(option) => option.value === fieldProperties.value
					)
				) {
					fieldOptions.push({
						label: field.legacy_values[fieldProperties.value],
						value: fieldProperties.value,
					});
				}

				// Sort options alphabetically by label.
				fieldOptions.sort(function (x, y) {
					const a = x.label.toUpperCase(),
						b = y.label.toUpperCase();
					return a.localeCompare(b);
				});

				// Assign options to field properties.
				fieldProperties.options = fieldOptions;

				// Return field element.
				return el(SelectControl, fieldProperties);
			};

			/**
			 * Returns a WordPress Icon element.
			 *
			 * @since 	3.3.1
			 *
			 * @param {string} iconName Icon Name.
			 * @return {Object} 		 Icon.
			 */
			const iconType = function (iconName) {
				return el(Icon, {
					icon: iconName,
				});
			};

			/**
			 * Returns an inline refresh button, used to refresh a sidebar field's resources.
			 *
			 * @since 	3.3.1
			 *
			 * @param {Object} props          Component props.
			 * @param {string} props.resource Resource type (forms,tags,landing_pages,restrict_content).
			 * @param {string} props.fieldKey The sidebar field key whose values should be updated on refresh.
			 * @return {Object} 	          Button.
			 */
			const InlineRefreshButton = function ({ resource, fieldKey }) {
				const [buttonDisabled, setButtonDisabled] = useState(false);

				return el(Button, {
					key: fieldKey + '-refresh-button',
					className:
						'button button-secondary wp-convertkit-refresh-resources' +
						(buttonDisabled ? ' is-refreshing' : ''),
					disabled: buttonDisabled,
					icon: iconType('update'),
					// `data-resource` is used by tests and as a stable hook
					// matching the classic-editor refresh button.
					'data-resource': resource,
					onClick() {
						// Refresh resources.
						refreshResources(resource, fieldKey, setButtonDisabled);
					},
				});
			};

			/**
			 * Returns a label for a resource item. If the item has a `format`
			 * property (as forms do), append it in square brackets; otherwise
			 * return the name on its own.
			 *
			 * @since 	3.3.1
			 *
			 * @param {Object} item API response item.
			 * @return {string}     Label.
			 */
			const labelForItem = function (item) {
				// Detect whether this collection of items includes a `format`
				// property anywhere in the item (legacy forms may omit it, in
				// which case we fall back to 'inline').
				if (Object.prototype.hasOwnProperty.call(item, 'format')) {
					return (
						item.name +
						' [' +
						(item.format ? item.format : 'inline') +
						']'
					);
				}

				return item.name;
			};

			/**
			 * Builds a flat values map from an array of API items, preserving
			 * any existing placeholder options (those whose value is a string,
			 * rather than an optgroup object) from the current field values.
			 *
			 * @since 	3.3.1
			 *
			 * @param {Array}  items          API response items.
			 * @param {Object} existingValues Current values map for the field.
			 * @return {Object}               Rebuilt values map.
			 */
			const buildSelectValues = function (items, existingValues) {
				const values = {};

				// Preserve existing placeholder options (Default, None, etc.)
				// from the current values. Placeholders are identified by
				// having a string value rather than an optgroup object.
				for (const existingKey in existingValues) {
					if (typeof existingValues[existingKey] === 'string') {
						values[existingKey] = existingValues[existingKey];
					}
				}

				// Add the refreshed items.
				items.forEach(function (item) {
					values[item.id] = labelForItem(item);
				});

				return values;
			};

			/**
			 * Builds an optgroup-style values map from a response object whose
			 * keys are group names and whose values are arrays of items. Each
			 * option key within an optgroup is prefixed with the singularized
			 * group name (e.g. `forms` => `form_123`), matching the
			 * prefixing convention used on the PHP side for grouped fields.
			 *
			 * Preserves any existing top-level placeholder options from the
			 * current field values.
			 *
			 * @since 	3.3.1
			 *
			 * @param {Object} groups         API response keyed by group name.
			 * @param {Object} existingValues Current values map for the field.
			 * @return {Object}               Rebuilt values map.
			 */
			const buildSelectOptGroupValues = function (
				groups,
				existingValues
			) {
				const values = {};

				// Preserve any top-level placeholder options (e.g. 'Do not restrict...').
				for (const existingKey in existingValues) {
					if (typeof existingValues[existingKey] !== 'object') {
						values[existingKey] = existingValues[existingKey];
					}
				}

				// Build each optgroup from the response.
				for (const optGroupKey in groups) {
					// Skip if this optgroup doesn't have any options.
					const items = groups[optGroupKey];
					if (!Array.isArray(items) || items.length === 0) {
						continue;
					}

					// Derive the per-item key prefix from the group name
					// (e.g. 'forms' => 'form_', 'tags' => 'tag_').
					const itemKeyPrefix = optGroupKey.replace(/s$/, '') + '_';

					// Reuse the existing optgroup label if one exists, falling
					// back to the capitalized group key if not.
					const existingGroup = existingValues[optGroupKey];
					const label =
						existingGroup &&
						typeof existingGroup === 'object' &&
						existingGroup.label
							? existingGroup.label
							: optGroupKey.charAt(0).toUpperCase() +
								optGroupKey.slice(1);

					const groupValues = {};
					items.forEach(function (item) {
						groupValues[itemKeyPrefix + item.id] =
							labelForItem(item);
					});

					values[optGroupKey] = {
						label,
						values: groupValues,
					};
				}

				return values;
			};

			/**
			 * Refreshes resources for the given resource type, updating
			 * the specified field's values on success so the SelectControl
			 * re-renders with the latest options.
			 *
			 * @since 	3.3.1
			 *
			 * @param {string}   resource          Resource type, appended to the refresh URL.
			 * @param {string}   fieldKey          The sidebar field key whose values should be updated.
			 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
			 */
			const refreshResources = function (
				resource,
				fieldKey,
				setButtonDisabled
			) {
				// Disable the button.
				setButtonDisabled(true);

				// Send AJAX request.
				fetch(convertkit_gutenberg.refresh_resources_url + resource, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce':
							convertkit_gutenberg.refresh_resources_nonce,
					},
				})
					.then(function (response) {
						// Convert response JSON string to object.
						return response.json();
					})
					.then(function (response) {
						if (convertkit_gutenberg.debug) {
							console.log(response);
						}

						// If the response includes a code, show an error notice.
						if (typeof response.code !== 'undefined') {
							// Show an error in the Gutenberg editor.
							wp.data
								.dispatch('core/notices')
								.createErrorNotice('Kit: ' + response.message, {
									id: 'convertkit-error',
								});

							// Enable refresh button.
							setButtonDisabled(false);
							return;
						}

						// Rebuild the field's values from the response, and
						// update state so the SelectControl re-renders with
						// the latest options.
						// The response shape determines the values shape:
						// an array produces a flat list; an object keyed by
						// group name (e.g. { forms: [...], tags: [...] })
						// produces optgroups.
						setFieldValues(function (prev) {
							const existing = prev[fieldKey] || {};
							const values = Array.isArray(response)
								? buildSelectValues(response, existing)
								: buildSelectOptGroupValues(response, existing);

							return Object.assign({}, prev, {
								[fieldKey]: values,
							});
						});

						// Enable refresh button.
						setButtonDisabled(false);
					})
					.catch(function (error) {
						// Show an error in the Gutenberg editor.
						wp.data
							.dispatch('core/notices')
							.createErrorNotice('Kit: ' + error, {
								id: 'convertkit-error',
							});

						// Enable refresh button.
						setButtonDisabled(false);
					});
			};

			/**
			 * Return an array of field elements to display in the settings panel.
			 *
			 * @since   3.3.0
			 *
			 * @param {Object} fields Fields to display.
			 * @return {Array}         Panel rows.
			 */
			const getFields = function (fields) {
				const rows = [];

				for (const key in fields) {
					// Skip if the Post Type being edited is not the same as the Post Type specified in the field's post_type property.
					if (
						typeof fields[key].post_type !== 'undefined' &&
						fields[key].post_type !== currentPostType
					) {
						continue;
					}

					rows.push(
						el(
							PanelRow,
							{
								key,
							},
							getField(fields[key], key)
						)
					);
				}

				return el(PanelBody, {}, rows);
			};

			// Return the settings sidebar panel with fields.
			return el(
				PluginSidebar,
				{
					name: sidebar.name,
					title: sidebar.title,
					className: sidebar.name,
					icon: element.RawHTML({
						children: sidebar.gutenberg_icon,
					}),
				},
				getFields(sidebar.fields)
			);
		};

		// Register the plugin sidebar.
		registerPlugin('convertkit-' + sidebar.name.replace(/_/g, '-'), {
			render: RenderPanel,
		});
	})(
		window.wp.plugins,
		window.wp.editPost,
		window.wp.element,
		window.wp.components,
		window.wp.data
	);
}

/**
 * Registers pre-publish actions in Gutenberg's pre-publish checks panel.
 *
 * @since 2.4.0
 *
 * @param {Object} actions Pre-publish actions.
 */
function convertKitGutenbergRegisterPrePublishActions(actions) {
	(function (plugins, editPost, element, components, data) {
		const el = element.createElement;
		const { ToggleControl } = components;
		const { registerPlugin } = plugins;
		const { PluginPrePublishPanel } = editPost;
		const { useSelect, useDispatch, select } = data;

		/**
		 * Returns a PluginPrePublishPanel for this plugin, containing all
		 * pre-publish actions.
		 *
		 * @since 2.4.0
		 * @return {WPElement|null} Pre-publish panel element or null if not a post.
		 */
		const RenderPanel = function () {
			// --- Hooks must be called first ---
			const { meta } = useSelect((wpSelect) => ({
				meta: wpSelect('core/editor').getEditedPostAttribute('meta'),
			}));

			const { editPost: wpEditPost } = useDispatch('core/editor');

			const currentPostType = select('core/editor').getCurrentPostType();

			// Bail early if not a 'post'
			if (currentPostType !== 'post') {
				return null;
			}

			// Build rows safely using .map()
			const rows = Object.values(actions).map((action) => {
				const key = '_convertkit_action_' + action.name;

				return el(ToggleControl, {
					key,
					id: 'convertkit_action_' + action.name,
					label: action.label,
					help: action.description,
					value: true,
					checked: meta[key],
					onChange(value) {
						wpEditPost({ meta: { [key]: value } });
					},
				});
			});

			// Return the pre-publish panel with rows
			return el(
				PluginPrePublishPanel,
				{
					className: 'convertkit-pre-publish-actions',
					title: 'Kit',
					initialOpen: true,
				},
				rows
			);
		};

		// Register pre-publish actions
		registerPlugin('convertkit-pre-publish-actions', {
			render: RenderPanel,
		});
	})(
		window.wp.plugins,
		window.wp.editPost,
		window.wp.element,
		window.wp.components,
		window.wp.data
	);
}

/**
 * Outputs a notice for the block.  Typically used when a block's settings
 * have not been defined, no API key exists in the Plugin or no resources
 * (forms, products) exist in ConvertKit, and the user adds an e.g.
 * Form / Product block.
 *
 * @since 	2.2.3
 *
 * @param {string} block_name Block Name.
 * @param {string} notice     Notice to display.
 * @return {Object} 		   HTMLElement
 */
function convertKitGutenbergDisplayBlockNotice(block_name, notice) {
	return wp.element.createElement(
		'div',
		{
			// convertkit-no-content class allows resources/backend/css/gutenberg.css
			// to apply styling/branding to the block.
			className: 'convertkit-' + block_name + ' convertkit-no-content',
		},
		notice
	);
}

/**
 * Checks if the user is editing a post in the block editor.
 *
 * @since   3.0.8
 *
 * @return {boolean} User is editing in the block editor
 */
function convertKitEditingPostInGutenberg() {
	// If the user is editing a post in the block editor, wp.editPost will be defined.
	return typeof wp !== 'undefined' && typeof wp.editPost !== 'undefined';
}

/**
 * Checks if the Gutenberg editor is loaded on screen.
 *
 * Returns true when editing a Post, Page or Custom Post Type in the block editor,
 * or using the site editor.
 *
 * @since   3.0.8
 *
 * @return {boolean} Block editor is loaded
 */
function convertKitGutenbergEnabled() {
	return typeof wp !== 'undefined' && typeof wp.blockEditor !== 'undefined';
}

```
