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

1,293 lines 34.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Registers blocks in the Gutenberg editor.
3 *
4 * @since 1.9.6.5
5 *
6 * @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 // Sort field's options alphabetically by label.
205 fieldOptions.sort(function (x, y) {
206 const a = x.label.toUpperCase(),
207 b = y.label.toUpperCase();
208 return a.localeCompare(b);
209 });
210
211 // Assign options to field.
212 fieldProperties.options = fieldOptions;
213
214 // Return field element.
215 return el(SelectControl, fieldProperties);
216
217 case 'resource':
218 // Build options for <select> input.
219 fieldOptions.push({
220 label: '(None)',
221 value: '',
222 });
223 for (const value of Object.keys(field.values)) {
224 fieldOptions.push({
225 label: field.values[value],
226 value,
227 });
228 }
229
230 // Sort field's options alphabetically by label.
231 fieldOptions.sort(function (x, y) {
232 const a = x.label.toUpperCase(),
233 b = y.label.toUpperCase();
234 return a.localeCompare(b);
235 });
236
237 // Assign options to field.
238 fieldProperties.options = fieldOptions;
239
240 return el(
241 Flex,
242 {
243 align: 'start',
244 },
245 [
246 el(
247 FlexItem,
248 {
249 key: attribute + '-select',
250 },
251 el(SelectControl, fieldProperties)
252 ),
253 el(
254 FlexItem,
255 {
256 key: attribute + '-refresh',
257 },
258 inlineRefreshButton(props)
259 ),
260 ]
261 );
262
263 case 'toggle':
264 // Define field properties.
265 fieldProperties.checked = props.attributes[attribute];
266
267 // Return field element.
268 return el(ToggleControl, fieldProperties);
269
270 case 'number':
271 // Define field properties.
272 fieldProperties.type = field.type;
273 fieldProperties.min = field.min;
274 fieldProperties.max = field.max;
275 fieldProperties.step = field.step;
276
277 // Return field element.
278 return el(TextControl, fieldProperties);
279
280 default:
281 // Return field element.
282 return el(TextControl, fieldProperties);
283 }
284 };
285
286 /**
287 * Return an array of rows to display in the given block sidebar's panel when
288 * this block is being edited.
289 *
290 * @since 2.2.0
291 *
292 * @param {Object} props Block properties.
293 * @param {string} panel Panel name.
294 * @return {Array} Panel rows.
295 */
296 const getPanelRows = function (props, panel) {
297 // Build Inspector Control Panel Rows, one for each Field.
298 const rows = [];
299 for (const i in block.panels[panel].fields) {
300 const attribute = block.panels[panel].fields[i], // e.g. 'term'.
301 field = block.fields[attribute]; // field array.
302
303 // If this field doesn't exist as an attribute in the block's get_attributes(),
304 // this is a non-Gutenberg field (such as a color picker for shortcodes),
305 // which should be ignored.
306 if (typeof block.attributes[attribute] === 'undefined') {
307 continue;
308 }
309
310 rows.push(
311 el(
312 PanelRow,
313 {
314 key: attribute,
315 },
316 getField(props, field, attribute)
317 )
318 );
319 }
320
321 return rows;
322 };
323
324 /**
325 * Return an array of panels to display in the block's sidebar when the block
326 * is being edited.
327 *
328 * @since 2.2.0
329 *
330 * @param {Object} props Block formatter properties.
331 * @return {Array} Block sidebar panels.
332 */
333 const getPanels = function (props) {
334 const panels = [];
335 let initialOpen = true;
336
337 // Build Inspector Control Panels.
338 for (const panel in block.panels) {
339 const panelRows = getPanelRows(props, panel);
340
341 // If no panel rows exist (e.g. this is a shortcode only panel,
342 // for styles, which Gutenberg registers in its own styles tab),
343 // don't add this panel.
344 if (!panelRows.length) {
345 continue;
346 }
347
348 panels.push(
349 el(
350 PanelBody,
351 {
352 title: block.panels[panel].label,
353 key: panel,
354 initialOpen,
355 },
356 panelRows
357 )
358 );
359
360 // Don't open any further panels.
361 initialOpen = false;
362 }
363
364 return panels;
365 };
366
367 /**
368 * Display settings sidebar when the block is being edited, and save
369 * changes that are made.
370 *
371 * @since 2.2.0
372 *
373 * @param {Object} props Block properties.
374 * @return {Object} Block settings sidebar elements.
375 */
376 const EditBlock = function (props) {
377 const blockProps = useBlockProps();
378
379 // Refresh button disabled state on DisplayNoticeWithLink.
380 // This must be here to avoid React error on hook order change when the user e.g.
381 // connects their Kit account from within the block itself.
382 const [buttonDisabled, setButtonDisabled] = useState(false);
383
384 // If requesting an example of how this block looks (which is requested
385 // when the user adds a new block and hovers over this block's icon),
386 // show the preview image.
387 if (props.attributes.is_gutenberg_example === true) {
388 return el(
389 'div',
390 blockProps,
391 el('img', {
392 src: block.gutenberg_example_image,
393 })
394 );
395 }
396
397 // If no access token has been defined in the Plugin, or no resources exist in Kit
398 // for this block, show a message in the block to tell the user what to do.
399 if (!block.has_access_token || !block.has_resources) {
400 return DisplayNoticeWithLink(
401 props,
402 blockProps,
403 buttonDisabled,
404 setButtonDisabled
405 );
406 }
407
408 // Build Inspector Control Panels, which will appear in the Sidebar when editing the Block.
409 const panels = getPanels(props);
410
411 // Generate Block Preview.
412 let preview = '';
413
414 // If a custom callback function to render this block's preview in the Gutenberg Editor
415 // has been defined, use it.
416 // This doesn't affect the output for this block on the frontend site, which will always
417 // use the block's PHP's render() function.
418 if (
419 typeof block.gutenberg_preview_render_callback !== 'undefined'
420 ) {
421 preview = window[block.gutenberg_preview_render_callback](
422 block,
423 props
424 );
425 return editBlockWithPanelsAndPreview(
426 panels,
427 preview,
428 blockProps
429 );
430 }
431
432 // If no settings have been defined for this block, render the block with a notice
433 // with instructions on how to configure the block.
434 if (
435 typeof block.gutenberg_help_description_attribute !==
436 'undefined' &&
437 props.attributes[block.gutenberg_help_description_attribute] ===
438 ''
439 ) {
440 preview = convertKitGutenbergDisplayBlockNotice(
441 block.name,
442 block.gutenberg_help_description
443 );
444 return editBlockWithPanelsAndPreview(
445 panels,
446 preview,
447 blockProps
448 );
449 }
450
451 // If no render_callback is defined, render the block.
452 if (typeof block.gutenberg_template !== 'undefined') {
453 // Build template for the new block.
454 const template = [];
455 for (const templateBlockName in block.gutenberg_template) {
456 if (
457 block.gutenberg_template.hasOwnProperty(
458 templateBlockName
459 )
460 ) {
461 template.push([
462 templateBlockName,
463 block.gutenberg_template[templateBlockName],
464 ]);
465 }
466 }
467
468 preview = el(
469 'div',
470 {},
471 el(InnerBlocks, {
472 template,
473 })
474 );
475 return editBlockWithPanelsAndPreview(
476 panels,
477 preview,
478 blockProps
479 );
480 }
481
482 // Use the block's PHP's render() function by calling the ServerSideRender component.
483 preview = el(wp.serverSideRender, {
484 block: 'convertkit/' + block.name,
485 attributes: props.attributes,
486
487 // This is only output in the Gutenberg editor, so must be slightly different from the inner class name used to
488 // apply styles with i.e. convertkit-block.name.
489 className: 'convertkit-ssr-' + block.name,
490 });
491 return editBlockWithPanelsAndPreview(panels, preview, blockProps);
492 };
493
494 /**
495 * Display settings sidebar when the block is being edited, and save
496 * changes that are made.
497 *
498 * @since 3.0.0
499 *
500 * @param {Object} panels Block panels.
501 * @param {Object} preview Block preview.
502 * @param {Object} blockProps Block properties.
503 * @return {Object} Block settings sidebar elements.
504 */
505 const editBlockWithPanelsAndPreview = function (
506 panels,
507 preview,
508 blockProps
509 ) {
510 return el('div', blockProps, [
511 el(InspectorControls, {}, panels),
512 preview,
513 ]);
514 };
515
516 /**
517 * Save the block's content.
518 *
519 * @since 3.0.0
520 *
521 * @return {Object} Block content.
522 */
523 const saveBlock = function () {
524 if (typeof block.gutenberg_template !== 'undefined') {
525 // Use useBlockProps.save() to preserve styling classes and attributes
526 // from block supports (colors, typography, spacing, etc.)
527 const blockProps = useBlockProps.save();
528 return el('div', blockProps, el(InnerBlocks.Content));
529 }
530
531 // Deliberate; preview in the editor is determined by the return statement in `edit` above.
532 // On the frontend site, the block's render() PHP class is always called, so we dynamically
533 // fetch the content.
534 return null;
535 };
536
537 /**
538 * Display a notice in the block with a clickable link to perform an action, and a refresh
539 * button to trigger editBlock(). Typically used when no API key exists in the Plugin,
540 * or no resources (forms, products) exist in ConvertKit.
541 *
542 * @since 2.2.5
543 *
544 * @param {Object} props Block properties.
545 * @param {Object} blockProps Block properties.
546 * @param {boolean} buttonDisabled Whether the refresh button is disabled (true) or enabled (false)
547 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
548 * @return {Object} Notice.
549 */
550 const DisplayNoticeWithLink = function (
551 props,
552 blockProps,
553 buttonDisabled,
554 setButtonDisabled
555 ) {
556 // Holds the array of elements to display in the notice component.
557 let elements;
558
559 // Define elements to display, based on whether the refresh button is disabled.
560 if (buttonDisabled) {
561 // Refresh button disabled; display a loading indicator and the button.
562 elements = [
563 loadingIndicator(props),
564 refreshButton(props, buttonDisabled, setButtonDisabled),
565 ];
566 } else {
567 // Refresh button enabled; display the notice, link and button.
568 elements = [
569 el(
570 'div',
571 {
572 key: props.clientId + '-notice',
573 },
574 !block.has_access_token
575 ? block.no_access_token.notice
576 : block.no_resources.notice
577 ),
578 noticeLink(props, setButtonDisabled),
579 refreshButton(props, buttonDisabled, setButtonDisabled),
580 ];
581 }
582
583 // Return the element.
584 return el(
585 'div',
586 blockProps,
587 el(
588 'div',
589 {
590 // convertkit-no-content class allows resources/backend/css/gutenberg.css
591 // to apply styling/branding to the block.
592 className:
593 'convertkit-' +
594 block.name +
595 ' convertkit-no-content',
596 },
597 elements
598 )
599 );
600 };
601
602 /**
603 * Returns an indeterminate progress bar element, to show that a block is loading / refreshing.
604 *
605 * @since 2.2.6
606 *
607 * @param {Object} props Block properties.
608 * @return {Object} Progress Bar.
609 */
610 const loadingIndicator = function (props) {
611 // If the ProgressBar component is not available i.e. WordPress < 6.3, return a spinner.
612 if (typeof ProgressBar === 'undefined') {
613 return el('span', {
614 key: props.clientId + '-spinner',
615 className: 'spinner is-active convertkit-block-refreshing',
616 });
617 }
618
619 return el(ProgressBar, {
620 key: props.clientId + '-progress-bar',
621 className:
622 'convertkit-progress-bar convertkit-block-refreshing',
623 });
624 };
625
626 /**
627 * Returns a WordPress Icon element.
628 *
629 * @since 2.7.7
630 *
631 * @param {string} iconName Icon Name.
632 * @return {Object} Icon.
633 */
634 const iconType = function (iconName) {
635 return el(Icon, {
636 icon: iconName,
637 });
638 };
639
640 /**
641 * Returns the notice link for the DisplayNoticeWithLink element.
642 *
643 * @since 2.2.6
644 *
645 * @param {Object} props Block properties.
646 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
647 * @return {Object} Notice Link.
648 */
649 const noticeLink = function (props, setButtonDisabled) {
650 // Get the URL to set the button to.
651 const url = !block.has_access_token
652 ? block.no_access_token.link
653 : block.no_resources.link;
654
655 return el(
656 Button,
657 {
658 key: props.clientId + '-notice-link',
659 className: !block.has_access_token
660 ? 'convertkit-block-modal'
661 : '',
662 variant: 'link',
663 onClick(e) {
664 e.preventDefault();
665
666 // Show popup window with setup wizard if we need to connect via OAuth.
667 if (!block.has_access_token) {
668 showConvertKitPopupWindow(
669 props,
670 url,
671 setButtonDisabled
672 );
673 return;
674 }
675
676 // Allow the link to load, as it's likely a link to the Kit site.
677 window.open(url, '_blank');
678 },
679 },
680 !block.has_access_token
681 ? block.no_access_token.link_text
682 : block.no_resources.link_text
683 );
684 };
685
686 /**
687 * Returns a refresh button, used to refresh a block when it has no API Keys
688 * or resources.
689 *
690 * @since 2.2.6
691 *
692 * @param {Object} props Block properties.
693 * @param {boolean} buttonDisabled Whether the refresh button is disabled (true) or enabled (false)
694 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
695 * @return {Object} Button.
696 */
697 const refreshButton = function (
698 props,
699 buttonDisabled,
700 setButtonDisabled
701 ) {
702 return el(Button, {
703 key: props.clientId + '-refresh-button',
704 className:
705 'wp-convertkit-refresh-resources' +
706 (buttonDisabled ? ' is-refreshing' : ''),
707 disabled: buttonDisabled,
708 text: 'Refresh',
709 icon: iconType('update'),
710 variant: 'secondary',
711 onClick() {
712 // Refresh block definitions.
713 refreshBlocksDefinitions(props, setButtonDisabled);
714 },
715 });
716 };
717
718 /**
719 * Returns an inline refresh button, used to refresh a block's resources.
720 *
721 * @since 2.7.1
722 *
723 * @param {Object} props Block properties.
724 * @return {Object} Button.
725 */
726 const inlineRefreshButton = function (props) {
727 return el(BlockInlineRefreshButton, props);
728 };
729
730 /**
731 * Returns a refresh button.
732 *
733 * @since 2.7.1
734 *
735 * @param {Object} props Block properties.
736 * @return {Object} Button.
737 */
738 const BlockInlineRefreshButton = function (props) {
739 const [buttonDisabled, setButtonDisabled] = useState(false);
740
741 return el(Button, {
742 key: props.clientId + '-refresh-button',
743 className:
744 'button button-secondary wp-convertkit-refresh-resources' +
745 (buttonDisabled ? ' is-refreshing' : ''),
746 disabled: buttonDisabled,
747 icon: iconType('update'),
748 onClick() {
749 // Refresh block definitions.
750 refreshBlocksDefinitions(props, setButtonDisabled);
751 },
752 });
753 };
754
755 /**
756 * Displays a new window with a given width and height to display the given URL.
757 *
758 * Typically used for displaying a modal version of the Setup Wizard, where the
759 * user clicks the 'Click here to connect your ConvertKit account' link in a block, and then
760 * enters their API Key and Secret. Will be used to show the ConvertKit
761 * OAuth window in the future.
762 *
763 * @since 2.2.6
764 *
765 * @param {Object} props Block properties.
766 * @param {string} url URL to display in the popup window.
767 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
768 */
769 const showConvertKitPopupWindow = function (
770 props,
771 url,
772 setButtonDisabled
773 ) {
774 // Define popup width, height and positioning.
775 const width = 640,
776 height = 750,
777 top = (window.screen.height - height) / 2,
778 left = (window.screen.width - width) / 2;
779
780 // Open popup.
781 const convertKitPopup = window.open(
782 url + '&convertkit-modal=1',
783 'convertkit_popup_window',
784 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' +
785 width +
786 ',height=' +
787 height +
788 ',top=' +
789 top +
790 ',left=' +
791 left
792 );
793
794 // Center popup and focus.
795 convertKitPopup.moveTo(left, top);
796 convertKitPopup.focus();
797
798 // Refresh the block when the popup is closed using self.close().
799 // Won't fire if the user closes the popup manually, which is fine because that means
800 // they didn't complete the steps, so refreshing wouldn't show anything new.
801 // The onbeforeunload would seem suitable here, but it fires whenever the popup window's
802 // document changes (e.g. as the user steps through a wizard), and doesn't fire when
803 // the window is closed.
804 // See https://stackoverflow.com/questions/9388380/capture-the-close-event-of-popup-window-in-javascript/48240128#48240128.
805 const convertKitPopupTimer = setInterval(function () {
806 if (convertKitPopup.closed) {
807 clearInterval(convertKitPopupTimer);
808
809 // Refresh block.
810 refreshBlocksDefinitions(props, setButtonDisabled);
811 }
812 }, 1000);
813 };
814
815 /**
816 * Refreshes this block's properties by:
817 * - making an AJAX call to fetch all registered blocks via convertkit_get_blocks(),
818 * - storing the registered blocks in the `convertkit_blocks` global object,
819 * - updating this block's properties by updating the `block` object.
820 *
821 * @since 2.2.6
822 *
823 * @param {Object} props Block properties.
824 * @param {Function} setButtonDisabled Function to enable or disable the refresh button.
825 */
826 const refreshBlocksDefinitions = function (props, setButtonDisabled) {
827 // Disable the button.
828 if (typeof setButtonDisabled !== 'undefined') {
829 setButtonDisabled(true);
830 }
831
832 // Send AJAX request.
833 fetch(convertkit_gutenberg.ajaxurl, {
834 method: 'GET',
835 headers: {
836 'Content-Type': 'application/json',
837 'X-WP-Nonce': convertkit_gutenberg.get_blocks_nonce,
838 },
839 })
840 .then(function (response) {
841 // Convert response JSON string to object.
842 return response.json();
843 })
844 .then(function (response) {
845 // If the response includes a code, show an error notice.
846 if (typeof response.code !== 'undefined') {
847 // Show an error in the Gutenberg editor.
848 wp.data
849 .dispatch('core/notices')
850 .createErrorNotice('Kit: ' + response.message, {
851 id: 'convertkit-error',
852 });
853
854 // Enable refresh button.
855 if (typeof setButtonDisabled !== 'undefined') {
856 setButtonDisabled(false);
857 }
858 return;
859 }
860
861 // Update global ConvertKit Blocks object, so that any updated resources
862 // are reflected when adding new ConvertKit Blocks.
863 convertkit_blocks = response;
864
865 // Update this block's properties, so that has_access_token, has_resources
866 // and the resources properties are updated.
867 block = convertkit_blocks[block.name];
868
869 // Call setAttributes on props to trigger the editBlock() function, which will re-render
870 // the block, reflecting any changes to its properties.
871 props.setAttributes({
872 refresh: Date.now(),
873 });
874
875 // Enable refresh button.
876 if (typeof setButtonDisabled !== 'undefined') {
877 setButtonDisabled(false);
878 }
879 })
880 .catch(function (error) {
881 // Show an error in the Gutenberg editor.
882 wp.data
883 .dispatch('core/notices')
884 .createErrorNotice('Kit: ' + error, {
885 id: 'convertkit-error',
886 });
887
888 // Enable refresh button.
889 if (typeof setButtonDisabled !== 'undefined') {
890 setButtonDisabled(false);
891 }
892 });
893 };
894
895 // Register Block.
896 registerBlockType('convertkit/' + block.name, {
897 apiVersion: convertkit_gutenberg.block_api_version,
898 title: block.title,
899 description: block.description,
900 category: block.category,
901 icon: getIcon,
902 keywords: block.keywords,
903 attributes: block.attributes,
904 supports: block.supports,
905 example: {
906 attributes: {
907 is_gutenberg_example: true,
908 },
909 },
910
911 // Editor.
912 edit: EditBlock,
913
914 // Output.
915 save: saveBlock,
916 });
917 })(
918 window.wp.blocks,
919 window.wp.blockEditor,
920 window.wp.element,
921 window.wp.components
922 );
923 }
924
925 /**
926 * Registers a Plugin Sidebar in Gutenberg.
927 *
928 * @since 3.3.0
929 *
930 * @param {Object} sidebar Plugin Sidebars
931 */
932 function convertKitGutenbergRegisterPluginSidebar(sidebar) {
933 (function (plugins, editor, element, components, data) {
934 // Define some constants for the various items we'll use.
935 const el = element.createElement;
936 const { registerPlugin } = plugins;
937 const { PluginSidebar } = editor;
938 const { TextControl, SelectControl, PanelBody, PanelRow } = components;
939 const { useSelect, useDispatch, select } = data;
940
941 /**
942 * Returns a PluginDocumentSettingPanel for this plugin, containing
943 * post-level settings.
944 *
945 * @since 3.3.0
946 */
947 const RenderPanel = function () {
948 const meta = useSelect(function (wpSelect) {
949 return (
950 wpSelect('core/editor').getEditedPostAttribute('meta') || {}
951 );
952 }, []);
953 const { editPost: wpEditPost } = useDispatch('core/editor');
954 const settings = meta[sidebar.meta_key] || sidebar.default_values;
955 const currentPostType = select('core/editor').getCurrentPostType();
956
957 /**
958 * Updates the Post meta meta_key object.
959 *
960 * @since 3.3.0
961 *
962 * @param {string} key Sub key within the meta_key object.
963 * @param {string} value Value to assign to the sub key.
964 */
965 const updateSetting = function (key, value) {
966 wpEditPost({
967 meta: {
968 [sidebar.meta_key]: Object.assign({}, settings, {
969 [key]: value,
970 }),
971 },
972 });
973 };
974
975 /**
976 * Return a field element for the settings panel.
977 *
978 * @since 3.3.0
979 *
980 * @param {Object} field Field properties.
981 * @param {string} key Field name.
982 * @return {Object} Field element.
983 */
984 const getField = function (field, key) {
985 // Define some field properties shared across all field types.
986 const fieldProperties = {
987 key: 'convertkit_plugin_sidebar_' + key,
988 label: field.label,
989 help: Array.isArray(field.description)
990 ? field.description.join('\n\n')
991 : field.description,
992 value: settings[key] || field.default_value || '',
993
994 // Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
995 // preventing deprecation notices in the block editor and opt in to the new styles
996 // from 7.0.
997 __next40pxDefaultSize: true,
998 __nextHasNoMarginBottom: true,
999
1000 // Save Post Meta on value change.
1001 onChange(value) {
1002 updateSetting(key, value);
1003 },
1004 };
1005
1006 // Define additional Field Properties and the Field Element,
1007 // depending on the Field Type (select, textarea, text etc).
1008 switch (field.type) {
1009 case 'select':
1010 // Check if any values are optgroups.
1011 const hasOptgroups = Object.keys(field.values).some(
1012 (subKey) =>
1013 typeof field.values[subKey] === 'object' &&
1014 field.values[subKey].label &&
1015 field.values[subKey].values
1016 );
1017
1018 if (hasOptgroups) {
1019 const children = [];
1020
1021 for (const value of Object.keys(field.values)) {
1022 if (
1023 typeof field.values[value] === 'object' &&
1024 field.values[value].label &&
1025 field.values[value].values
1026 ) {
1027 // Optgroup.
1028 const groupChildren = [];
1029 for (const groupValue of Object.keys(
1030 field.values[value].values
1031 )) {
1032 groupChildren.push(
1033 el(
1034 'option',
1035 {
1036 value: groupValue,
1037 key: groupValue,
1038 },
1039 field.values[value].values[
1040 groupValue
1041 ]
1042 )
1043 );
1044 }
1045 children.push(
1046 el(
1047 'optgroup',
1048 {
1049 label: field.values[value]
1050 .label,
1051 key: value,
1052 },
1053 ...groupChildren
1054 )
1055 );
1056 } else {
1057 // Option within optgroup.
1058 children.push(
1059 el(
1060 'option',
1061 { value, key: value },
1062 field.values[value]
1063 )
1064 );
1065 }
1066 }
1067
1068 return el(
1069 SelectControl,
1070 fieldProperties,
1071 ...children
1072 );
1073 }
1074
1075 // Options only, no optgroups.
1076 const fieldOptions = [];
1077 for (const value of Object.keys(field.values)) {
1078 fieldOptions.push({
1079 label: field.values[value],
1080 value,
1081 });
1082 }
1083
1084 // Sort options alphabetically by label.
1085 fieldOptions.sort(function (x, y) {
1086 const a = x.label.toUpperCase(),
1087 b = y.label.toUpperCase();
1088 return a.localeCompare(b);
1089 });
1090
1091 // Assign options to field properties.
1092 fieldProperties.options = fieldOptions;
1093
1094 // Return field element.
1095 return el(SelectControl, fieldProperties);
1096
1097 default:
1098 // Return field element.
1099 return el(TextControl, fieldProperties);
1100 }
1101 };
1102
1103 /**
1104 * Return an array of field elements to display in the settings panel.
1105 *
1106 * @since 3.3.0
1107 *
1108 * @param {Object} fields Fields to display.
1109 * @return {Array} Panel rows.
1110 */
1111 const getFields = function (fields) {
1112 const rows = [];
1113
1114 for (const key in fields) {
1115 // Skip if the Post Type being edited is not the same as the Post Type specified in the field's post_type property.
1116 if (
1117 typeof fields[key].post_type !== 'undefined' &&
1118 fields[key].post_type !== currentPostType
1119 ) {
1120 continue;
1121 }
1122
1123 rows.push(
1124 el(
1125 PanelRow,
1126 {
1127 key,
1128 },
1129 getField(fields[key], key)
1130 )
1131 );
1132 }
1133
1134 return el(PanelBody, {}, rows);
1135 };
1136
1137 // Return the settings sidebar panel with fields.
1138 return el(
1139 PluginSidebar,
1140 {
1141 name: sidebar.name,
1142 title: sidebar.title,
1143 className: sidebar.name,
1144 icon: element.RawHTML({
1145 children: sidebar.gutenberg_icon,
1146 }),
1147 },
1148 getFields(sidebar.fields)
1149 );
1150 };
1151
1152 // Register the plugin sidebar.
1153 registerPlugin('convertkit-' + sidebar.name.replace(/_/g, '-'), {
1154 render: RenderPanel,
1155 });
1156 })(
1157 window.wp.plugins,
1158 window.wp.editPost,
1159 window.wp.element,
1160 window.wp.components,
1161 window.wp.data
1162 );
1163 }
1164
1165 /**
1166 * Registers pre-publish actions in Gutenberg's pre-publish checks panel.
1167 *
1168 * @since 2.4.0
1169 *
1170 * @param {Object} actions Pre-publish actions.
1171 */
1172 function convertKitGutenbergRegisterPrePublishActions(actions) {
1173 (function (plugins, editPost, element, components, data) {
1174 const el = element.createElement;
1175 const { ToggleControl } = components;
1176 const { registerPlugin } = plugins;
1177 const { PluginPrePublishPanel } = editPost;
1178 const { useSelect, useDispatch, select } = data;
1179
1180 /**
1181 * Returns a PluginPrePublishPanel for this plugin, containing all
1182 * pre-publish actions.
1183 *
1184 * @since 2.4.0
1185 * @return {WPElement|null} Pre-publish panel element or null if not a post.
1186 */
1187 const RenderPanel = function () {
1188 // --- Hooks must be called first ---
1189 const { meta } = useSelect((wpSelect) => ({
1190 meta: wpSelect('core/editor').getEditedPostAttribute('meta'),
1191 }));
1192
1193 const { editPost: wpEditPost } = useDispatch('core/editor');
1194
1195 const currentPostType = select('core/editor').getCurrentPostType();
1196
1197 // Bail early if not a 'post'
1198 if (currentPostType !== 'post') {
1199 return null;
1200 }
1201
1202 // Build rows safely using .map()
1203 const rows = Object.values(actions).map((action) => {
1204 const key = '_convertkit_action_' + action.name;
1205
1206 return el(ToggleControl, {
1207 key,
1208 id: 'convertkit_action_' + action.name,
1209 label: action.label,
1210 help: action.description,
1211 value: true,
1212 checked: meta[key],
1213 onChange(value) {
1214 wpEditPost({ meta: { [key]: value } });
1215 },
1216 });
1217 });
1218
1219 // Return the pre-publish panel with rows
1220 return el(
1221 PluginPrePublishPanel,
1222 {
1223 className: 'convertkit-pre-publish-actions',
1224 title: 'Kit',
1225 initialOpen: true,
1226 },
1227 rows
1228 );
1229 };
1230
1231 // Register pre-publish actions
1232 registerPlugin('convertkit-pre-publish-actions', {
1233 render: RenderPanel,
1234 });
1235 })(
1236 window.wp.plugins,
1237 window.wp.editPost,
1238 window.wp.element,
1239 window.wp.components,
1240 window.wp.data
1241 );
1242 }
1243
1244 /**
1245 * Outputs a notice for the block. Typically used when a block's settings
1246 * have not been defined, no API key exists in the Plugin or no resources
1247 * (forms, products) exist in ConvertKit, and the user adds an e.g.
1248 * Form / Product block.
1249 *
1250 * @since 2.2.3
1251 *
1252 * @param {string} block_name Block Name.
1253 * @param {string} notice Notice to display.
1254 * @return {Object} HTMLElement
1255 */
1256 function convertKitGutenbergDisplayBlockNotice(block_name, notice) {
1257 return wp.element.createElement(
1258 'div',
1259 {
1260 // convertkit-no-content class allows resources/backend/css/gutenberg.css
1261 // to apply styling/branding to the block.
1262 className: 'convertkit-' + block_name + ' convertkit-no-content',
1263 },
1264 notice
1265 );
1266 }
1267
1268 /**
1269 * Checks if the user is editing a post in the block editor.
1270 *
1271 * @since 3.0.8
1272 *
1273 * @return {boolean} User is editing in the block editor
1274 */
1275 function convertKitEditingPostInGutenberg() {
1276 // If the user is editing a post in the block editor, wp.editPost will be defined.
1277 return typeof wp !== 'undefined' && typeof wp.editPost !== 'undefined';
1278 }
1279
1280 /**
1281 * Checks if the Gutenberg editor is loaded on screen.
1282 *
1283 * Returns true when editing a Post, Page or Custom Post Type in the block editor,
1284 * or using the site editor.
1285 *
1286 * @since 3.0.8
1287 *
1288 * @return {boolean} Block editor is loaded
1289 */
1290 function convertKitGutenbergEnabled() {
1291 return typeof wp !== 'undefined' && typeof wp.blockEditor !== 'undefined';
1292 }
1293