PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.3
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 3.4.3, at resources/backend/js/gutenberg.js

1,745 lines 47.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 if (convertKitEditingPostInGutenberg()) {
23 // Register Plugin Sidebars in Gutenberg if we're editing a Post.
24 if (typeof convertkit_plugin_sidebars !== 'undefined') {
25 for (const pluginSidebar in convertkit_plugin_sidebars) {
26 convertKitGutenbergRegisterPluginSidebar(
27 convertkit_plugin_sidebars[pluginSidebar]
28 );
29 }
30 }
31
32 // Register ConvertKit Pre-publish actions in Gutenberg if we're editing a Post.
33 if (typeof convertkit_pre_publish_actions !== 'undefined') {
34 convertKitGutenbergRegisterPrePublishActions(
35 convertkit_pre_publish_actions
36 );
37 }
38 }
39 }
40
41 /**
42 * Registers the given block in Gutenberg.
43 *
44 * @since 1.9.6.5
45 *
46 * @param {Object} block Block.
47 */
48 function convertKitGutenbergRegisterBlock(block) {
49 (function (blocks, editor, element, components) {
50 // Define some constants for the various items we'll use.
51 const el = element.createElement;
52 const { registerBlockType } = blocks;
53 const { InspectorControls, InnerBlocks, useBlockProps } = editor;
54 const { useState } = element;
55 const {
56 Button,
57 Icon,
58 TextControl,
59 SelectControl,
60 ToggleControl,
61 Flex,
62 FlexItem,
63 PanelBody,
64 PanelRow,
65 ProgressBar,
66 } = components;
67
68 /**
69 * Returns the icon to display for this block, depending
70 * on the supplied block's configuration.
71 *
72 * @since 2.2.0
73 *
74 * @return {WPElement|string} Either a WordPress element (RawHTML) or a dashicon string.
75 */
76 const getIcon = function () {
77 // Return a fallback default icon if none is specified for this block.
78 if (typeof block.gutenberg_icon === 'undefined') {
79 return 'dashicons-tablet';
80 }
81
82 // Return HTML element if the icon is an SVG string.
83 if (block.gutenberg_icon.search('svg') >= 0) {
84 return element.RawHTML({
85 children: block.gutenberg_icon,
86 });
87 }
88
89 // Just return the string, as it's a dashicon CSS class.
90 return block.gutenberg_icon;
91 };
92
93 /**
94 * Return a field element for the block sidebar, which is displayed in a panel's row
95 * when this block is being edited.
96 *
97 * @since 2.2.0
98 *
99 * @param {Object} props Block properties.
100 * @param {Object} field Field attributes.
101 * @param {string} attribute Attribute name to store the field's data in.
102 * @return {Object} Field element.
103 */
104 const getField = function (props, field, attribute) {
105 // If this field is conditionally displayed, check if the field should be displayed.
106 if (typeof field.display_if !== 'undefined') {
107 // Assume the condition has not been met for this field to be displayed.
108 let display_field = false;
109
110 // Assert whether the condition is met based on the field type.
111 switch (block.fields[field.display_if.key].type) {
112 case 'toggle':
113 // Field's condition value will be 0 or 1.
114 // Attributes field value will be false or true.
115 display_field =
116 Boolean(Number(field.display_if.value)) ===
117 props.attributes[field.display_if.key];
118 break;
119
120 default:
121 // Assert based on the condition's value type (array, string, number).
122 switch (typeof field.display_if.value) {
123 case 'object':
124 display_field = Object.values(
125 field.display_if.value
126 ).includes(
127 props.attributes[field.display_if.key]
128 );
129 break;
130
131 default:
132 display_field =
133 field.display_if.value ===
134 props.attributes[field.display_if.key];
135 break;
136 }
137 break;
138 }
139
140 // Skip this field if the condition is not met.
141 if (!display_field) {
142 return false;
143 }
144 }
145
146 // Define some field properties shared across all field types.
147 const fieldProperties = {
148 id:
149 'convertkit_' +
150 block.name.replace(/-/g, '_') +
151 '_' +
152 attribute,
153 label: field.label,
154 help: field.description,
155 value: props.attributes[attribute],
156
157 // Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
158 // preventing deprecation notices in the block editor and opt in to the new styles
159 // from 7.0.
160 __next40pxDefaultSize: true,
161 __nextHasNoMarginBottom: true,
162
163 onChange(value) {
164 if (field.type === 'number') {
165 // If value is a blank string i.e. no attribute value was provided,
166 // cast it to the field's minimum number setting.
167 // This prevents WordPress' block renderer API returning a 400 error
168 // because a blank value will be passed as a string, when WordPress
169 // expects it to be a numerical value.
170 if (value === '') {
171 value = field.min;
172 }
173
174 // Cast value to integer if a value exists.
175 if (value.length > 0) {
176 value = Number(value);
177 }
178 }
179
180 const newValue = {};
181 newValue[attribute] = value;
182 props.setAttributes(newValue);
183 },
184 };
185
186 const fieldOptions = [];
187
188 // Define additional Field Properties and the Field Element,
189 // depending on the Field Type (select, textarea, text etc).
190 switch (field.type) {
191 case 'select':
192 // Build options for <select> input.
193 fieldOptions.push({
194 label: '(None)',
195 value: '',
196 });
197 for (const value of Object.keys(field.values)) {
198 fieldOptions.push({
199 label: field.values[value],
200 value,
201 });
202 }
203
204 // If the block's saved value references a legacy form that
205 // isn't in field.values (legacy forms are not offered as
206 // new selection choices), append it here so the dropdown
207 // still reflects the saved selection.
208 if (
209 fieldProperties.value &&
210 field.legacy_values &&
211 typeof field.legacy_values[fieldProperties.value] !==
212 'undefined' &&
213 !fieldOptions.some(
214 (option) => option.value === fieldProperties.value
215 )
216 ) {
217 fieldOptions.push({
218 label: field.legacy_values[fieldProperties.value],
219 value: fieldProperties.value,
220 });
221 }
222
223 // Sort field's options alphabetically by label.
224 fieldOptions.sort(function (x, y) {
225 const a = x.label.toUpperCase(),
226 b = y.label.toUpperCase();
227 return a.localeCompare(b);
228 });
229
230 // Assign options to field.
231 fieldProperties.options = fieldOptions;
232
233 // Return field element.
234 return el(SelectControl, fieldProperties);
235
236 case 'resource':
237 // Build options for <select> input.
238 fieldOptions.push({
239 label: '(None)',
240 value: '',
241 });
242 for (const value of Object.keys(field.values)) {
243 fieldOptions.push({
244 label: field.values[value],
245 value,
246 });
247 }
248
249 // If the block's saved value references a legacy form that
250 // isn't in field.values (because legacy forms are not
251 // offered as new selection choices), append it here so the
252 // dropdown still reflects the saved selection.
253 if (
254 fieldProperties.value &&
255 field.legacy_values &&
256 typeof field.legacy_values[fieldProperties.value] !==
257 'undefined' &&
258 !fieldOptions.some(
259 (option) => option.value === fieldProperties.value
260 )
261 ) {
262 fieldOptions.push({
263 label: field.legacy_values[fieldProperties.value],
264 value: fieldProperties.value,
265 });
266 }
267
268 // Sort field's options alphabetically by label.
269 fieldOptions.sort(function (x, y) {
270 const a = x.label.toUpperCase(),
271 b = y.label.toUpperCase();
272 return a.localeCompare(b);
273 });
274
275 // Assign options to field.
276 fieldProperties.options = fieldOptions;
277
278 return el(
279 Flex,
280 {
281 align: 'start',
282 },
283 [
284 el(
285 FlexItem,
286 {
287 key: attribute + '-select',
288 },
289 el(SelectControl, fieldProperties)
290 ),
291 el(
292 FlexItem,
293 {
294 key: attribute + '-refresh',
295 },
296 inlineRefreshButton(props)
297 ),
298 ]
299 );
300
301 case 'toggle':
302 // Define field properties.
303 fieldProperties.checked = props.attributes[attribute];
304
305 // Return field element.
306 return el(ToggleControl, fieldProperties);
307
308 case 'number':
309 // Define field properties.
310 fieldProperties.type = field.type;
311 fieldProperties.min = field.min;
312 fieldProperties.max = field.max;
313 fieldProperties.step = field.step;
314
315 // Return field element.
316 return el(TextControl, fieldProperties);
317
318 default:
319 // Return field element.
320 return el(TextControl, fieldProperties);
321 }
322 };
323
324 /**
325 * Return an array of rows to display in the given block sidebar's panel when
326 * this block is being edited.
327 *
328 * @since 2.2.0
329 *
330 * @param {Object} props Block properties.
331 * @param {string} panel Panel name.
332 * @return {Array} Panel rows.
333 */
334 const getPanelRows = function (props, panel) {
335 // Build Inspector Control Panel Rows, one for each Field.
336 const rows = [];
337 for (const i in block.panels[panel].fields) {
338 const attribute = block.panels[panel].fields[i], // e.g. 'term'.
339 field = block.fields[attribute]; // field array.
340
341 // If this field doesn't exist as an attribute in the block's get_attributes(),
342 // this is a non-Gutenberg field (such as a color picker for shortcodes),
343 // which should be ignored.
344 if (typeof block.attributes[attribute] === 'undefined') {
345 continue;
346 }
347
348 rows.push(
349 el(
350 PanelRow,
351 {
352 key: attribute,
353 },
354 getField(props, field, attribute)
355 )
356 );
357 }
358
359 return rows;
360 };
361
362 /**
363 * Return an array of panels to display in the block's sidebar when the block
364 * is being edited.
365 *
366 * @since 2.2.0
367 *
368 * @param {Object} props Block formatter properties.
369 * @return {Array} Block sidebar panels.
370 */
371 const getPanels = function (props) {
372 const panels = [];
373 let initialOpen = true;
374
375 // Build Inspector Control Panels.
376 for (const panel in block.panels) {
377 const panelRows = getPanelRows(props, panel);
378
379 // If no panel rows exist (e.g. this is a shortcode only panel,
380 // for styles, which Gutenberg registers in its own styles tab),
381 // don't add this panel.
382 if (!panelRows.length) {
383 continue;
384 }
385
386 panels.push(
387 el(
388 PanelBody,
389 {
390 title: block.panels[panel].label,
391 key: panel,
392 initialOpen,
393 },
394 panelRows
395 )
396 );
397
398 // Don't open any further panels.
399 initialOpen = false;
400 }
401
402 return panels;
403 };
404
405 /**
406 * Display settings sidebar when the block is being edited, and save
407 * changes that are made.
408 *
409 * @since 2.2.0
410 *
411 * @param {Object} props Block properties.
412 * @return {Object} Block settings sidebar elements.
413 */
414 const EditBlock = function (props) {
415 const blockProps = useBlockProps();
416
417 // Refresh button disabled state on DisplayNoticeWithLink.
418 // This must be here to avoid React error on hook order change when the user e.g.
419 // connects their Kit account from within the block itself.
420 const [buttonDisabled, setButtonDisabled] = useState(false);
421
422 // If requesting an example of how this block looks (which is requested
423 // when the user adds a new block and hovers over this block's icon),
424 // show the preview image.
425 if (props.attributes.is_gutenberg_example === true) {
426 return el(
427 'div',
428 blockProps,
429 el('img', {
430 src: block.gutenberg_example_image,
431 })
432 );
433 }
434
435 // If no access token has been defined in the Plugin, or no resources exist in Kit
436 // for this block, show a message in the block to tell the user what to do.
437 if (!block.has_access_token || !block.has_resources) {
438 return DisplayNoticeWithLink(
439 props,
440 blockProps,
441 buttonDisabled,
442 setButtonDisabled
443 );
444 }
445
446 // Build Inspector Control Panels, which will appear in the Sidebar when editing the Block.
447 const panels = getPanels(props);
448
449 // Generate Block Preview.
450 let preview = '';
451
452 // If a custom callback function to render this block's preview in the Gutenberg Editor
453 // has been defined, use it.
454 // This doesn't affect the output for this block on the frontend site, which will always
455 // use the block's PHP's render() function.
456 if (
457 typeof block.gutenberg_preview_render_callback !== 'undefined'
458 ) {
459 preview = window[block.gutenberg_preview_render_callback](
460 block,
461 props
462 );
463 return editBlockWithPanelsAndPreview(
464 panels,
465 preview,
466 blockProps
467 );
468 }
469
470 // If no settings have been defined for this block, render the block with a notice
471 // with instructions on how to configure the block.
472 if (
473 typeof block.gutenberg_help_description_attribute !==
474 'undefined' &&
475 props.attributes[block.gutenberg_help_description_attribute] ===
476 ''
477 ) {
478 preview = convertKitGutenbergDisplayBlockNotice(
479 block.name,
480 block.gutenberg_help_description
481 );
482 return editBlockWithPanelsAndPreview(
483 panels,
484 preview,
485 blockProps
486 );
487 }
488
489 // If no render_callback is defined, render the block.
490 if (typeof block.gutenberg_template !== 'undefined') {
491 // Build template for the new block.
492 const template = [];
493 for (const templateBlockName in block.gutenberg_template) {
494 if (
495 block.gutenberg_template.hasOwnProperty(
496 templateBlockName
497 )
498 ) {
499 template.push([
500 templateBlockName,
501 block.gutenberg_template[templateBlockName],
502 ]);
503 }
504 }
505
506 preview = el(
507 'div',
508 {},
509 el(InnerBlocks, {
510 template,
511 })
512 );
513 return editBlockWithPanelsAndPreview(
514 panels,
515 preview,
516 blockProps
517 );
518 }
519
520 // Use the block's PHP's render() function by calling the ServerSideRender component.
521 preview = el(wp.serverSideRender, {
522 block: 'convertkit/' + block.name,
523 attributes: props.attributes,
524
525 // This is only output in the Gutenberg editor, so must be slightly different from the inner class name used to
526 // apply styles with i.e. convertkit-block.name.
527 className: 'convertkit-ssr-' + block.name,
528 });
529 return editBlockWithPanelsAndPreview(panels, preview, blockProps);
530 };
531
532 /**
533 * Display settings sidebar when the block is being edited, and save
534 * changes that are made.
535 *
536 * @since 3.0.0
537 *
538 * @param {Object} panels Block panels.
539 * @param {Object} preview Block preview.
540 * @param {Object} blockProps Block properties.
541 * @return {Object} Block settings sidebar elements.
542 */
543 const editBlockWithPanelsAndPreview = function (
544 panels,
545 preview,
546 blockProps
547 ) {
548 return el('div', blockProps, [
549 el(InspectorControls, {}, panels),
550 preview,
551 ]);
552 };
553
554 /**
555 * Save the block's content.
556 *
557 * @since 3.0.0
558 *
559 * @return {Object} Block content.
560 */
561 const saveBlock = function () {
562 if (typeof block.gutenberg_template !== 'undefined') {
563 // Use useBlockProps.save() to preserve styling classes and attributes
564 // from block supports (colors, typography, spacing, etc.)
565 const blockProps = useBlockProps.save();
566 return el('div', blockProps, el(InnerBlocks.Content));
567 }
568
569 // Deliberate; preview in the editor is determined by the return statement in `edit` above.
570 // On the frontend site, the block's render() PHP class is always called, so we dynamically
571 // fetch the content.
572 return null;
573 };
574
575 /**
576 * Display a notice in the block with a clickable link to perform an action, and a refresh
577 * button to trigger editBlock(). Typically used when no API key exists in the Plugin,
578 * or no resources (forms, products) exist in ConvertKit.
579 *
580 * @since 2.2.5
581 *
582 * @param {Object} props Block properties.
583 * @param {Object} blockProps Block properties.
584 * @param {boolean} buttonDisabled Whether the refresh button is disabled (true) or enabled (false)
585 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
586 * @return {Object} Notice.
587 */
588 const DisplayNoticeWithLink = function (
589 props,
590 blockProps,
591 buttonDisabled,
592 setButtonDisabled
593 ) {
594 // Holds the array of elements to display in the notice component.
595 let elements;
596
597 // Define elements to display, based on whether the refresh button is disabled.
598 if (buttonDisabled) {
599 // Refresh button disabled; display a loading indicator and the button.
600 elements = [
601 loadingIndicator(props),
602 refreshButton(props, buttonDisabled, setButtonDisabled),
603 ];
604 } else {
605 // Refresh button enabled; display the notice, link and button.
606 elements = [
607 el(
608 'div',
609 {
610 key: props.clientId + '-notice',
611 },
612 !block.has_access_token
613 ? block.no_access_token.notice
614 : block.no_resources.notice
615 ),
616 noticeLink(props, setButtonDisabled),
617 refreshButton(props, buttonDisabled, setButtonDisabled),
618 ];
619 }
620
621 // Return the element.
622 return el(
623 'div',
624 blockProps,
625 el(
626 'div',
627 {
628 // convertkit-no-content class allows resources/backend/css/gutenberg.css
629 // to apply styling/branding to the block.
630 className:
631 'convertkit-' +
632 block.name +
633 ' convertkit-no-content',
634 },
635 elements
636 )
637 );
638 };
639
640 /**
641 * Returns an indeterminate progress bar element, to show that a block is loading / refreshing.
642 *
643 * @since 2.2.6
644 *
645 * @param {Object} props Block properties.
646 * @return {Object} Progress Bar.
647 */
648 const loadingIndicator = function (props) {
649 // If the ProgressBar component is not available i.e. WordPress < 6.3, return a spinner.
650 if (typeof ProgressBar === 'undefined') {
651 return el('span', {
652 key: props.clientId + '-spinner',
653 className: 'spinner is-active convertkit-block-refreshing',
654 });
655 }
656
657 return el(ProgressBar, {
658 key: props.clientId + '-progress-bar',
659 className:
660 'convertkit-progress-bar convertkit-block-refreshing',
661 });
662 };
663
664 /**
665 * Returns a WordPress Icon element.
666 *
667 * @since 2.7.7
668 *
669 * @param {string} iconName Icon Name.
670 * @return {Object} Icon.
671 */
672 const iconType = function (iconName) {
673 return el(Icon, {
674 icon: iconName,
675 });
676 };
677
678 /**
679 * Returns the notice link for the DisplayNoticeWithLink element.
680 *
681 * @since 2.2.6
682 *
683 * @param {Object} props Block properties.
684 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
685 * @return {Object} Notice Link.
686 */
687 const noticeLink = function (props, setButtonDisabled) {
688 // Get the URL to set the button to.
689 const url = !block.has_access_token
690 ? block.no_access_token.link
691 : block.no_resources.link;
692
693 return el(
694 Button,
695 {
696 key: props.clientId + '-notice-link',
697 className: !block.has_access_token
698 ? 'convertkit-block-modal'
699 : '',
700 variant: 'link',
701 onClick(e) {
702 e.preventDefault();
703
704 // Show popup window with setup wizard if we need to connect via OAuth.
705 if (!block.has_access_token) {
706 showConvertKitPopupWindow(
707 props,
708 url,
709 setButtonDisabled
710 );
711 return;
712 }
713
714 // Allow the link to load, as it's likely a link to the Kit site.
715 window.open(url, '_blank');
716 },
717 },
718 !block.has_access_token
719 ? block.no_access_token.link_text
720 : block.no_resources.link_text
721 );
722 };
723
724 /**
725 * Returns a refresh button, used to refresh a block when it has no API Keys
726 * or resources.
727 *
728 * @since 2.2.6
729 *
730 * @param {Object} props Block properties.
731 * @param {boolean} buttonDisabled Whether the refresh button is disabled (true) or enabled (false)
732 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
733 * @return {Object} Button.
734 */
735 const refreshButton = function (
736 props,
737 buttonDisabled,
738 setButtonDisabled
739 ) {
740 return el(Button, {
741 key: props.clientId + '-refresh-button',
742 className:
743 'wp-convertkit-refresh-resources' +
744 (buttonDisabled ? ' is-refreshing' : ''),
745 disabled: buttonDisabled,
746 text: 'Refresh',
747 icon: iconType('update'),
748 variant: 'secondary',
749 onClick() {
750 // Refresh block definitions.
751 refreshBlocksDefinitions(props, setButtonDisabled);
752 },
753 });
754 };
755
756 /**
757 * Returns an inline refresh button, used to refresh a block's resources.
758 *
759 * @since 2.7.1
760 *
761 * @param {Object} props Block properties.
762 * @return {Object} Button.
763 */
764 const inlineRefreshButton = function (props) {
765 return el(BlockInlineRefreshButton, props);
766 };
767
768 /**
769 * Returns a refresh button.
770 *
771 * @since 2.7.1
772 *
773 * @param {Object} props Block properties.
774 * @return {Object} Button.
775 */
776 const BlockInlineRefreshButton = function (props) {
777 const [buttonDisabled, setButtonDisabled] = useState(false);
778
779 return el(Button, {
780 key: props.clientId + '-refresh-button',
781 className:
782 'button button-secondary wp-convertkit-refresh-resources' +
783 (buttonDisabled ? ' is-refreshing' : ''),
784 disabled: buttonDisabled,
785 icon: iconType('update'),
786 onClick() {
787 // Refresh block definitions.
788 refreshBlocksDefinitions(props, setButtonDisabled);
789 },
790 });
791 };
792
793 /**
794 * Displays a new window with a given width and height to display the given URL.
795 *
796 * Typically used for displaying a modal version of the Setup Wizard, where the
797 * user clicks the 'Click here to connect your ConvertKit account' link in a block, and then
798 * enters their API Key and Secret. Will be used to show the ConvertKit
799 * OAuth window in the future.
800 *
801 * @since 2.2.6
802 *
803 * @param {Object} props Block properties.
804 * @param {string} url URL to display in the popup window.
805 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
806 */
807 const showConvertKitPopupWindow = function (
808 props,
809 url,
810 setButtonDisabled
811 ) {
812 // Define popup width, height and positioning.
813 const width = 640,
814 height = 750,
815 top = (window.screen.height - height) / 2,
816 left = (window.screen.width - width) / 2;
817
818 // Open popup.
819 const convertKitPopup = window.open(
820 url + '&convertkit-modal=1',
821 'convertkit_popup_window',
822 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' +
823 width +
824 ',height=' +
825 height +
826 ',top=' +
827 top +
828 ',left=' +
829 left
830 );
831
832 // Center popup and focus.
833 convertKitPopup.moveTo(left, top);
834 convertKitPopup.focus();
835
836 // Refresh the block when the popup is closed using self.close().
837 // Won't fire if the user closes the popup manually, which is fine because that means
838 // they didn't complete the steps, so refreshing wouldn't show anything new.
839 // The onbeforeunload would seem suitable here, but it fires whenever the popup window's
840 // document changes (e.g. as the user steps through a wizard), and doesn't fire when
841 // the window is closed.
842 // See https://stackoverflow.com/questions/9388380/capture-the-close-event-of-popup-window-in-javascript/48240128#48240128.
843 const convertKitPopupTimer = setInterval(function () {
844 if (convertKitPopup.closed) {
845 clearInterval(convertKitPopupTimer);
846
847 // Refresh block.
848 refreshBlocksDefinitions(props, setButtonDisabled);
849 }
850 }, 1000);
851 };
852
853 /**
854 * Refreshes this block's properties by:
855 * - making an AJAX call to fetch all registered blocks via convertkit_get_blocks(),
856 * - storing the registered blocks in the `convertkit_blocks` global object,
857 * - updating this block's properties by updating the `block` object.
858 *
859 * @since 2.2.6
860 *
861 * @param {Object} props Block properties.
862 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
863 */
864 const refreshBlocksDefinitions = function (props, setButtonDisabled) {
865 // Disable the button.
866 if (typeof setButtonDisabled !== 'undefined') {
867 setButtonDisabled(true);
868 }
869
870 // Send AJAX request.
871 fetch(convertkit_gutenberg.ajaxurl, {
872 method: 'GET',
873 headers: {
874 'Content-Type': 'application/json',
875 'X-WP-Nonce': convertkit_gutenberg.get_blocks_nonce,
876 },
877 })
878 .then(function (response) {
879 // Convert response JSON string to object.
880 return response.json();
881 })
882 .then(function (response) {
883 // If the response includes a code, show an error notice.
884 if (typeof response.code !== 'undefined') {
885 // Show an error in the Gutenberg editor.
886 wp.data
887 .dispatch('core/notices')
888 .createErrorNotice('Kit: ' + response.message, {
889 id: 'convertkit-error',
890 });
891
892 // Enable refresh button.
893 if (typeof setButtonDisabled !== 'undefined') {
894 setButtonDisabled(false);
895 }
896 return;
897 }
898
899 // Update global ConvertKit Blocks object, so that any updated resources
900 // are reflected when adding new ConvertKit Blocks.
901 convertkit_blocks = response;
902
903 // Update this block's properties, so that has_access_token, has_resources
904 // and the resources properties are updated.
905 block = convertkit_blocks[block.name];
906
907 // Call setAttributes on props to trigger the editBlock() function, which will re-render
908 // the block, reflecting any changes to its properties.
909 props.setAttributes({
910 refresh: Date.now(),
911 });
912
913 // Enable refresh button.
914 if (typeof setButtonDisabled !== 'undefined') {
915 setButtonDisabled(false);
916 }
917 })
918 .catch(function (error) {
919 // Show an error in the Gutenberg editor.
920 wp.data
921 .dispatch('core/notices')
922 .createErrorNotice('Kit: ' + error, {
923 id: 'convertkit-error',
924 });
925
926 // Enable refresh button.
927 if (typeof setButtonDisabled !== 'undefined') {
928 setButtonDisabled(false);
929 }
930 });
931 };
932
933 // Register Block.
934 registerBlockType('convertkit/' + block.name, {
935 apiVersion: convertkit_gutenberg.block_api_version,
936 title: block.title,
937 description: block.description,
938 category: block.category,
939 icon: getIcon,
940 keywords: block.keywords,
941 attributes: block.attributes,
942 supports: block.supports,
943 example: {
944 attributes: {
945 is_gutenberg_example: true,
946 },
947 },
948
949 // Editor.
950 edit: EditBlock,
951
952 // Output.
953 save: saveBlock,
954 });
955 })(
956 window.wp.blocks,
957 window.wp.blockEditor,
958 window.wp.element,
959 window.wp.components
960 );
961 }
962
963 /**
964 * Registers a Plugin Sidebar in Gutenberg.
965 *
966 * @since 3.3.0
967 *
968 * @param {Object} sidebar Plugin Sidebars
969 */
970 function convertKitGutenbergRegisterPluginSidebar(sidebar) {
971 (function (plugins, editor, element, components, data) {
972 // Define some constants for the various items we'll use.
973 const el = element.createElement;
974 const { registerPlugin } = plugins;
975 const { PluginSidebar } = editor;
976 const { useState } = element;
977 const {
978 Icon,
979 TextControl,
980 SelectControl,
981 Flex,
982 FlexItem,
983 FlexBlock,
984 PanelBody,
985 PanelRow,
986 Button,
987 } = components;
988 const { useSelect, useDispatch, select } = data;
989
990 /**
991 * Returns a PluginDocumentSettingPanel for this plugin, containing
992 * post-level settings.
993 *
994 * @since 3.3.0
995 */
996 const RenderPanel = function () {
997 const meta = useSelect(function (wpSelect) {
998 return (
999 wpSelect('core/editor').getEditedPostAttribute('meta') || {}
1000 );
1001 }, []);
1002 const { editPost: wpEditPost } = useDispatch('core/editor');
1003 const settings = meta[sidebar.meta_key] || sidebar.default_values;
1004 const currentPostType = select('core/editor').getCurrentPostType();
1005
1006 // Seed each field's `values` into component state, so that when a
1007 // refresh button is clicked we can update the field's options
1008 // and trigger a re-render without mutating the global
1009 // convertkit_plugin_sidebars object.
1010 const [fieldValues, setFieldValues] = useState(function () {
1011 const initial = {};
1012 for (const key in sidebar.fields) {
1013 initial[key] = sidebar.fields[key].values;
1014 }
1015 return initial;
1016 });
1017
1018 /**
1019 * Updates the Post meta meta_key object.
1020 *
1021 * @since 3.3.0
1022 *
1023 * @param {string} key Sub key within the meta_key object.
1024 * @param {string} value Value to assign to the sub key.
1025 */
1026 const updateSetting = function (key, value) {
1027 wpEditPost({
1028 meta: {
1029 [sidebar.meta_key]: Object.assign({}, settings, {
1030 [key]: value,
1031 }),
1032 },
1033 });
1034 };
1035
1036 /**
1037 * Return a field element for the settings panel.
1038 *
1039 * @since 3.3.0
1040 *
1041 * @param {Object} field Field properties.
1042 * @param {string} key Field name.
1043 * @return {Object} Field element.
1044 */
1045 const getField = function (field, key) {
1046 // Override field.values with the latest values from state, which
1047 // may have been refreshed by clicking the refresh button.
1048 field = Object.assign({}, field, {
1049 values: fieldValues[key] || field.values,
1050 });
1051
1052 // Build the help element. Supports HTML in the description by
1053 // rendering each line as a paragraph via element.RawHTML, which
1054 // allows inline tags like <code> and <a> to render correctly.
1055 let helpElement;
1056 if (Array.isArray(field.description)) {
1057 helpElement = el(
1058 'span',
1059 {},
1060 field.description.map(function (line, index) {
1061 return el(
1062 'p',
1063 {
1064 key: 'help-' + index,
1065 style: { margin: '0 0 0.5em 0' },
1066 },
1067 el(element.RawHTML, {}, line)
1068 );
1069 })
1070 );
1071 } else if (field.description) {
1072 helpElement = el(element.RawHTML, {}, field.description);
1073 }
1074
1075 // Define some field properties shared across all field types.
1076 const fieldProperties = {
1077 key: 'convertkit_plugin_sidebar_' + key,
1078 id: 'convertkit_plugin_sidebar_' + key,
1079 label: field.label,
1080 help: helpElement,
1081 value: settings[key] || field.default_value || '',
1082
1083 // Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
1084 // preventing deprecation notices in the block editor and opt in to the new styles
1085 // from 7.0.
1086 __next40pxDefaultSize: true,
1087 __nextHasNoMarginBottom: true,
1088
1089 // Save Post Meta on value change.
1090 onChange(value) {
1091 updateSetting(key, value);
1092 },
1093 };
1094
1095 // Define additional Field Properties and the Field Element,
1096 // depending on the Field Type (select, textarea, text etc).
1097 switch (field.type) {
1098 case 'select':
1099 // If the field has a resource_type, wrap the select in a
1100 // Flex container alongside a refresh button.
1101 if (field.resource_type) {
1102 const selectFieldProperties = Object.assign(
1103 {},
1104 fieldProperties,
1105 { help: undefined }
1106 );
1107
1108 return el(
1109 'div',
1110 {
1111 key:
1112 'convertkit_plugin_sidebar_' +
1113 key +
1114 '_wrapper',
1115 },
1116 el(
1117 Flex,
1118 {
1119 align: 'end',
1120 gap: 2,
1121 },
1122 [
1123 el(
1124 FlexBlock,
1125 {
1126 key: key + '-select',
1127 },
1128 getSelectField(
1129 field,
1130 selectFieldProperties
1131 )
1132 ),
1133 el(
1134 FlexItem,
1135 {
1136 key: key + '-refresh',
1137 },
1138 el(InlineRefreshButton, {
1139 resource: field.resource_type,
1140 fieldKey: key,
1141 })
1142 ),
1143 ]
1144 ),
1145 fieldProperties.help
1146 ? el(
1147 'p',
1148 {
1149 key: key + '-help',
1150 className:
1151 'components-base-control__help',
1152 },
1153 fieldProperties.help
1154 )
1155 : null
1156 );
1157 }
1158
1159 return getSelectField(field, fieldProperties);
1160
1161 default:
1162 // Return field element.
1163 return el(TextControl, fieldProperties);
1164 }
1165 };
1166
1167 /**
1168 * Returns a select field element, with optgroups and options
1169 * depending on the field's values.
1170 *
1171 * @since 3.3.1
1172 *
1173 * @param {Object} field Field properties.
1174 * @param {Object} fieldProperties Field properties.
1175 * @return {Object} Select field element.
1176 */
1177 const getSelectField = function (field, fieldProperties) {
1178 // Check if any values are optgroups.
1179 const hasOptgroups = Object.keys(field.values).some(
1180 (subKey) =>
1181 typeof field.values[subKey] === 'object' &&
1182 field.values[subKey].label &&
1183 field.values[subKey].values
1184 );
1185
1186 if (hasOptgroups) {
1187 const children = [];
1188 const seenValues = new Set();
1189
1190 for (const value of Object.keys(field.values)) {
1191 if (
1192 typeof field.values[value] === 'object' &&
1193 field.values[value].label &&
1194 field.values[value].values
1195 ) {
1196 // Optgroup.
1197 const groupChildren = [];
1198 for (const groupValue of Object.keys(
1199 field.values[value].values
1200 )) {
1201 seenValues.add(groupValue);
1202 groupChildren.push(
1203 el(
1204 'option',
1205 {
1206 value: groupValue,
1207 key: groupValue,
1208 },
1209 field.values[value].values[groupValue]
1210 )
1211 );
1212 }
1213 children.push(
1214 el(
1215 'optgroup',
1216 {
1217 label: field.values[value].label,
1218 key: value,
1219 },
1220 ...groupChildren
1221 )
1222 );
1223 } else {
1224 // Option within optgroup.
1225 seenValues.add(value);
1226 children.push(
1227 el(
1228 'option',
1229 { value, key: value },
1230 field.values[value]
1231 )
1232 );
1233 }
1234 }
1235
1236 // If the saved value references a legacy form that isn't
1237 // otherwise in the dropdown, append it so the current
1238 // selection remains visible.
1239 if (
1240 fieldProperties.value &&
1241 field.legacy_values &&
1242 typeof field.legacy_values[fieldProperties.value] !==
1243 'undefined' &&
1244 !seenValues.has(fieldProperties.value)
1245 ) {
1246 children.push(
1247 el(
1248 'option',
1249 {
1250 value: fieldProperties.value,
1251 key: fieldProperties.value,
1252 },
1253 field.legacy_values[fieldProperties.value]
1254 )
1255 );
1256 }
1257
1258 return el(SelectControl, fieldProperties, ...children);
1259 }
1260
1261 // Options only, no optgroups.
1262 const fieldOptions = [];
1263 for (const value of Object.keys(field.values)) {
1264 fieldOptions.push({
1265 label: field.values[value],
1266 value,
1267 });
1268 }
1269
1270 // If the saved value references a legacy form that isn't in
1271 // field.values, append it so the current selection remains
1272 // visible in the dropdown.
1273 if (
1274 fieldProperties.value &&
1275 field.legacy_values &&
1276 typeof field.legacy_values[fieldProperties.value] !==
1277 'undefined' &&
1278 !fieldOptions.some(
1279 (option) => option.value === fieldProperties.value
1280 )
1281 ) {
1282 fieldOptions.push({
1283 label: field.legacy_values[fieldProperties.value],
1284 value: fieldProperties.value,
1285 });
1286 }
1287
1288 // Sort options alphabetically by label.
1289 fieldOptions.sort(function (x, y) {
1290 const a = x.label.toUpperCase(),
1291 b = y.label.toUpperCase();
1292 return a.localeCompare(b);
1293 });
1294
1295 // Assign options to field properties.
1296 fieldProperties.options = fieldOptions;
1297
1298 // Return field element.
1299 return el(SelectControl, fieldProperties);
1300 };
1301
1302 /**
1303 * Returns a WordPress Icon element.
1304 *
1305 * @since 3.3.1
1306 *
1307 * @param {string} iconName Icon Name.
1308 * @return {Object} Icon.
1309 */
1310 const iconType = function (iconName) {
1311 return el(Icon, {
1312 icon: iconName,
1313 });
1314 };
1315
1316 /**
1317 * Returns an inline refresh button, used to refresh a sidebar field's resources.
1318 *
1319 * @since 3.3.1
1320 *
1321 * @param {Object} props Component props.
1322 * @param {string} props.resource Resource type (forms,tags,landing_pages,restrict_content).
1323 * @param {string} props.fieldKey The sidebar field key whose values should be updated on refresh.
1324 * @return {Object} Button.
1325 */
1326 const InlineRefreshButton = function ({ resource, fieldKey }) {
1327 const [buttonDisabled, setButtonDisabled] = useState(false);
1328
1329 return el(Button, {
1330 key: fieldKey + '-refresh-button',
1331 className:
1332 'button button-secondary wp-convertkit-refresh-resources' +
1333 (buttonDisabled ? ' is-refreshing' : ''),
1334 disabled: buttonDisabled,
1335 icon: iconType('update'),
1336 // `data-resource` is used by tests and as a stable hook
1337 // matching the classic-editor refresh button.
1338 'data-resource': resource,
1339 onClick() {
1340 // Refresh resources.
1341 refreshResources(resource, fieldKey, setButtonDisabled);
1342 },
1343 });
1344 };
1345
1346 /**
1347 * Returns a label for a resource item. If the item has a `format`
1348 * property (as forms do), append it in square brackets; otherwise
1349 * return the name on its own.
1350 *
1351 * @since 3.3.1
1352 *
1353 * @param {Object} item API response item.
1354 * @return {string} Label.
1355 */
1356 const labelForItem = function (item) {
1357 // Detect whether this collection of items includes a `format`
1358 // property anywhere in the item (legacy forms may omit it, in
1359 // which case we fall back to 'inline').
1360 if (Object.prototype.hasOwnProperty.call(item, 'format')) {
1361 return (
1362 item.name +
1363 ' [' +
1364 (item.format ? item.format : 'inline') +
1365 ']'
1366 );
1367 }
1368
1369 return item.name;
1370 };
1371
1372 /**
1373 * Builds a flat values map from an array of API items, preserving
1374 * any existing placeholder options (those whose value is a string,
1375 * rather than an optgroup object) from the current field values.
1376 *
1377 * @since 3.3.1
1378 *
1379 * @param {Array} items API response items.
1380 * @param {Object} existingValues Current values map for the field.
1381 * @return {Object} Rebuilt values map.
1382 */
1383 const buildSelectValues = function (items, existingValues) {
1384 const values = {};
1385
1386 // Preserve existing placeholder options (Default, None, etc.)
1387 // from the current values. Placeholders are identified by
1388 // having a string value rather than an optgroup object.
1389 for (const existingKey in existingValues) {
1390 if (typeof existingValues[existingKey] === 'string') {
1391 values[existingKey] = existingValues[existingKey];
1392 }
1393 }
1394
1395 // Add the refreshed items.
1396 items.forEach(function (item) {
1397 values[item.id] = labelForItem(item);
1398 });
1399
1400 return values;
1401 };
1402
1403 /**
1404 * Builds an optgroup-style values map from a response object whose
1405 * keys are group names and whose values are arrays of items. Each
1406 * option key within an optgroup is prefixed with the singularized
1407 * group name (e.g. `forms` => `form_123`), matching the
1408 * prefixing convention used on the PHP side for grouped fields.
1409 *
1410 * Preserves any existing top-level placeholder options from the
1411 * current field values.
1412 *
1413 * @since 3.3.1
1414 *
1415 * @param {Object} groups API response keyed by group name.
1416 * @param {Object} existingValues Current values map for the field.
1417 * @return {Object} Rebuilt values map.
1418 */
1419 const buildSelectOptGroupValues = function (
1420 groups,
1421 existingValues
1422 ) {
1423 const values = {};
1424
1425 // Preserve any top-level placeholder options (e.g. 'Do not restrict...').
1426 for (const existingKey in existingValues) {
1427 if (typeof existingValues[existingKey] !== 'object') {
1428 values[existingKey] = existingValues[existingKey];
1429 }
1430 }
1431
1432 // Build each optgroup from the response.
1433 for (const optGroupKey in groups) {
1434 // Skip if this optgroup doesn't have any options.
1435 const items = groups[optGroupKey];
1436 if (!Array.isArray(items) || items.length === 0) {
1437 continue;
1438 }
1439
1440 // Derive the per-item key prefix from the group name
1441 // (e.g. 'forms' => 'form_', 'tags' => 'tag_').
1442 const itemKeyPrefix = optGroupKey.replace(/s$/, '') + '_';
1443
1444 // Reuse the existing optgroup label if one exists, falling
1445 // back to the capitalized group key if not.
1446 const existingGroup = existingValues[optGroupKey];
1447 const label =
1448 existingGroup &&
1449 typeof existingGroup === 'object' &&
1450 existingGroup.label
1451 ? existingGroup.label
1452 : optGroupKey.charAt(0).toUpperCase() +
1453 optGroupKey.slice(1);
1454
1455 const groupValues = {};
1456 items.forEach(function (item) {
1457 groupValues[itemKeyPrefix + item.id] =
1458 labelForItem(item);
1459 });
1460
1461 values[optGroupKey] = {
1462 label,
1463 values: groupValues,
1464 };
1465 }
1466
1467 return values;
1468 };
1469
1470 /**
1471 * Refreshes resources for the given resource type, updating
1472 * the specified field's values on success so the SelectControl
1473 * re-renders with the latest options.
1474 *
1475 * @since 3.3.1
1476 *
1477 * @param {string} resource Resource type, appended to the refresh URL.
1478 * @param {string} fieldKey The sidebar field key whose values should be updated.
1479 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
1480 */
1481 const refreshResources = function (
1482 resource,
1483 fieldKey,
1484 setButtonDisabled
1485 ) {
1486 // Disable the button.
1487 setButtonDisabled(true);
1488
1489 // Send AJAX request.
1490 fetch(convertkit_gutenberg.refresh_resources_url + resource, {
1491 method: 'POST',
1492 headers: {
1493 'Content-Type': 'application/json',
1494 'X-WP-Nonce':
1495 convertkit_gutenberg.refresh_resources_nonce,
1496 },
1497 })
1498 .then(function (response) {
1499 // Convert response JSON string to object.
1500 return response.json();
1501 })
1502 .then(function (response) {
1503 if (convertkit_gutenberg.debug) {
1504 console.log(response);
1505 }
1506
1507 // If the response includes a code, show an error notice.
1508 if (typeof response.code !== 'undefined') {
1509 // Show an error in the Gutenberg editor.
1510 wp.data
1511 .dispatch('core/notices')
1512 .createErrorNotice('Kit: ' + response.message, {
1513 id: 'convertkit-error',
1514 });
1515
1516 // Enable refresh button.
1517 setButtonDisabled(false);
1518 return;
1519 }
1520
1521 // Rebuild the field's values from the response, and
1522 // update state so the SelectControl re-renders with
1523 // the latest options.
1524 // The response shape determines the values shape:
1525 // an array produces a flat list; an object keyed by
1526 // group name (e.g. { forms: [...], tags: [...] })
1527 // produces optgroups.
1528 setFieldValues(function (prev) {
1529 const existing = prev[fieldKey] || {};
1530 const values = Array.isArray(response)
1531 ? buildSelectValues(response, existing)
1532 : buildSelectOptGroupValues(response, existing);
1533
1534 return Object.assign({}, prev, {
1535 [fieldKey]: values,
1536 });
1537 });
1538
1539 // Enable refresh button.
1540 setButtonDisabled(false);
1541 })
1542 .catch(function (error) {
1543 // Show an error in the Gutenberg editor.
1544 wp.data
1545 .dispatch('core/notices')
1546 .createErrorNotice('Kit: ' + error, {
1547 id: 'convertkit-error',
1548 });
1549
1550 // Enable refresh button.
1551 setButtonDisabled(false);
1552 });
1553 };
1554
1555 /**
1556 * Return an array of field elements to display in the settings panel.
1557 *
1558 * @since 3.3.0
1559 *
1560 * @param {Object} fields Fields to display.
1561 * @return {Array} Panel rows.
1562 */
1563 const getFields = function (fields) {
1564 const rows = [];
1565
1566 for (const key in fields) {
1567 // Skip if the Post Type being edited is not the same as the Post Type specified in the field's post_type property.
1568 if (
1569 typeof fields[key].post_type !== 'undefined' &&
1570 fields[key].post_type !== currentPostType
1571 ) {
1572 continue;
1573 }
1574
1575 rows.push(
1576 el(
1577 PanelRow,
1578 {
1579 key,
1580 },
1581 getField(fields[key], key)
1582 )
1583 );
1584 }
1585
1586 return el(PanelBody, {}, rows);
1587 };
1588
1589 // Return the settings sidebar panel with fields.
1590 return el(
1591 PluginSidebar,
1592 {
1593 name: sidebar.name,
1594 title: sidebar.title,
1595 className: sidebar.name,
1596 icon: element.RawHTML({
1597 children: sidebar.gutenberg_icon,
1598 }),
1599 },
1600 getFields(sidebar.fields)
1601 );
1602 };
1603
1604 // Register the plugin sidebar.
1605 registerPlugin('convertkit-' + sidebar.name.replace(/_/g, '-'), {
1606 render: RenderPanel,
1607 });
1608 })(
1609 window.wp.plugins,
1610 window.wp.editPost,
1611 window.wp.element,
1612 window.wp.components,
1613 window.wp.data
1614 );
1615 }
1616
1617 /**
1618 * Registers pre-publish actions in Gutenberg's pre-publish checks panel.
1619 *
1620 * @since 2.4.0
1621 *
1622 * @param {Object} actions Pre-publish actions.
1623 */
1624 function convertKitGutenbergRegisterPrePublishActions(actions) {
1625 (function (plugins, editPost, element, components, data) {
1626 const el = element.createElement;
1627 const { ToggleControl } = components;
1628 const { registerPlugin } = plugins;
1629 const { PluginPrePublishPanel } = editPost;
1630 const { useSelect, useDispatch, select } = data;
1631
1632 /**
1633 * Returns a PluginPrePublishPanel for this plugin, containing all
1634 * pre-publish actions.
1635 *
1636 * @since 2.4.0
1637 * @return {WPElement|null} Pre-publish panel element or null if not a post.
1638 */
1639 const RenderPanel = function () {
1640 // --- Hooks must be called first ---
1641 const { meta } = useSelect((wpSelect) => ({
1642 meta: wpSelect('core/editor').getEditedPostAttribute('meta'),
1643 }));
1644
1645 const { editPost: wpEditPost } = useDispatch('core/editor');
1646
1647 const currentPostType = select('core/editor').getCurrentPostType();
1648
1649 // Bail early if not a 'post'
1650 if (currentPostType !== 'post') {
1651 return null;
1652 }
1653
1654 // Build rows safely using .map()
1655 const rows = Object.values(actions).map((action) => {
1656 const key = '_convertkit_action_' + action.name;
1657
1658 return el(ToggleControl, {
1659 key,
1660 id: 'convertkit_action_' + action.name,
1661 label: action.label,
1662 help: action.description,
1663 value: true,
1664 checked: meta[key],
1665 onChange(value) {
1666 wpEditPost({ meta: { [key]: value } });
1667 },
1668 });
1669 });
1670
1671 // Return the pre-publish panel with rows
1672 return el(
1673 PluginPrePublishPanel,
1674 {
1675 className: 'convertkit-pre-publish-actions',
1676 title: 'Kit',
1677 initialOpen: true,
1678 },
1679 rows
1680 );
1681 };
1682
1683 // Register pre-publish actions
1684 registerPlugin('convertkit-pre-publish-actions', {
1685 render: RenderPanel,
1686 });
1687 })(
1688 window.wp.plugins,
1689 window.wp.editPost,
1690 window.wp.element,
1691 window.wp.components,
1692 window.wp.data
1693 );
1694 }
1695
1696 /**
1697 * Outputs a notice for the block. Typically used when a block's settings
1698 * have not been defined, no API key exists in the Plugin or no resources
1699 * (forms, products) exist in ConvertKit, and the user adds an e.g.
1700 * Form / Product block.
1701 *
1702 * @since 2.2.3
1703 *
1704 * @param {string} block_name Block Name.
1705 * @param {string} notice Notice to display.
1706 * @return {Object} HTMLElement
1707 */
1708 function convertKitGutenbergDisplayBlockNotice(block_name, notice) {
1709 return wp.element.createElement(
1710 'div',
1711 {
1712 // convertkit-no-content class allows resources/backend/css/gutenberg.css
1713 // to apply styling/branding to the block.
1714 className: 'convertkit-' + block_name + ' convertkit-no-content',
1715 },
1716 notice
1717 );
1718 }
1719
1720 /**
1721 * Checks if the user is editing a post in the block editor.
1722 *
1723 * @since 3.0.8
1724 *
1725 * @return {boolean} User is editing in the block editor
1726 */
1727 function convertKitEditingPostInGutenberg() {
1728 // If the user is editing a post in the block editor, wp.editPost will be defined.
1729 return typeof wp !== 'undefined' && typeof wp.editPost !== 'undefined';
1730 }
1731
1732 /**
1733 * Checks if the Gutenberg editor is loaded on screen.
1734 *
1735 * Returns true when editing a Post, Page or Custom Post Type in the block editor,
1736 * or using the site editor.
1737 *
1738 * @since 3.0.8
1739 *
1740 * @return {boolean} Block editor is loaded
1741 */
1742 function convertKitGutenbergEnabled() {
1743 return typeof wp !== 'undefined' && typeof wp.blockEditor !== 'undefined';
1744 }
1745