diff --git a/extensions/blocks/calendly/attributes.js b/extensions/blocks/calendly/attributes.js new file mode 100644 index 0000000000000..be78741617c68 --- /dev/null +++ b/extensions/blocks/calendly/attributes.js @@ -0,0 +1,90 @@ +/** + * External Dependencies + */ +import { reduce } from 'lodash'; +import { __ } from '@wordpress/i18n'; + +const hexRegex = /^#?[A-Fa-f0-9]{6}$/; + +const colourValidator = value => hexRegex.test( value ); + +const urlValidator = url => ! url || url.startsWith( 'https://calendly.com/' ); + +export default { + backgroundColor: { + type: 'string', + default: 'ffffff', + validator: colourValidator, + }, + submitButtonText: { + type: 'string', + default: __( 'Schedule time with me', 'jetpack' ), + }, + submitButtonTextColor: { + type: 'string', + validator: colourValidator, + }, + submitButtonBackgroundColor: { + type: 'string', + validator: colourValidator, + }, + submitButtonClasses: { type: 'string' }, + hideEventTypeDetails: { + type: 'boolean', + default: false, + }, + primaryColor: { + type: 'string', + default: '00A2FF', + validator: colourValidator, + }, + textColor: { + type: 'string', + default: '4D5055', + validator: colourValidator, + }, + style: { + type: 'string', + default: 'inline', + validValues: [ 'inline', 'link' ], + }, + url: { + type: 'string', + validator: urlValidator, + }, + backgroundButtonColor: { + type: 'string', + }, + textButtonColor: { + type: 'string', + }, + customBackgroundButtonColor: { + type: 'string', + validator: colourValidator, + }, + customTextButtonColor: { + type: 'string', + validator: colourValidator, + }, +}; + +export const getValidatedAttributes = ( attributeDetails, attributes ) => + reduce( + attributes, + ( ret, attribute, attributeKey ) => { + const { type, validator, validValues, default: defaultVal } = attributeDetails[ + attributeKey + ]; + if ( 'boolean' === type ) { + ret[ attributeKey ] = !! attribute; + } else if ( validator ) { + ret[ attributeKey ] = validator( attribute ) ? attribute : defaultVal; + } else if ( validValues ) { + ret[ attributeKey ] = validValues.includes( attribute ) ? attribute : defaultVal; + } else { + ret[ attributeKey ] = attribute; + } + return ret; + }, + {} + ); diff --git a/extensions/blocks/calendly/blockStylesPreviewAndSelector.js b/extensions/blocks/calendly/blockStylesPreviewAndSelector.js new file mode 100644 index 0000000000000..5248c13e0d090 --- /dev/null +++ b/extensions/blocks/calendly/blockStylesPreviewAndSelector.js @@ -0,0 +1,72 @@ +/** + * External Dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { getBlockType, cloneBlock, getBlockFromExample } from '@wordpress/blocks'; +import { BlockPreview } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { ENTER, SPACE } from '@wordpress/keycodes'; + +export default function BlockStylesPreviewAndSelector( { + attributes, + clientId, + styleOptions, + onSelectStyle, + activeStyle, +} ) { + const block = useSelect( select => { + const { getBlock } = select( 'core/block-editor' ); + return getBlock( clientId ); + } ); + + const type = getBlockType( block.name ); + + return ( +
+ { styleOptions.map( styleOption => { + return ( +
{ + onSelectStyle( { style: styleOption.name } ); + } } + onKeyDown={ event => { + if ( ENTER === event.keyCode || SPACE === event.keyCode ) { + event.preventDefault(); + onSelectStyle( { style: styleOption.name } ); + } + } } + role="button" + tabIndex="0" + aria-label={ styleOption.label } + > +
+ +
+
{ styleOption.label }
+
+ ); + } ) } +
+ ); +} diff --git a/extensions/blocks/calendly/calendly.php b/extensions/blocks/calendly/calendly.php new file mode 100644 index 0000000000000..d327d20058359 --- /dev/null +++ b/extensions/blocks/calendly/calendly.php @@ -0,0 +1,123 @@ + 'jetpack_calendly_block_load_assets' ) +); + +/** + * Calendly block registration/dependency declaration. + * + * @param array $attr Array containing the Calendly block attributes. + * @param string $content String containing the Calendly block content. + * + * @return string + */ +function jetpack_calendly_block_load_assets( $attr, $content ) { + $url = jetpack_calendly_block_get_attribute( $attr, 'url' ); + if ( empty( $url ) ) { + return; + } + + /* + * Enqueue necessary scripts and styles. + */ + Jetpack_Gutenberg::load_assets_as_required( 'calendly' ); + wp_enqueue_script( + 'jetpack-calendly-external-js', + 'https://assets.calendly.com/assets/external/widget.js', + null, + JETPACK__VERSION, + false + ); + + $style = jetpack_calendly_block_get_attribute( $attr, 'style' ); + $hide_event_type_details = jetpack_calendly_block_get_attribute( $attr, 'hideEventTypeDetails' ); + $background_color = jetpack_calendly_block_get_attribute( $attr, 'backgroundColor' ); + $text_color = jetpack_calendly_block_get_attribute( $attr, 'textColor' ); + $primary_color = jetpack_calendly_block_get_attribute( $attr, 'primaryColor' ); + $submit_button_text = jetpack_calendly_block_get_attribute( $attr, 'submitButtonText' ); + $submit_button_text_color = jetpack_calendly_block_get_attribute( $attr, 'customTextButtonColor' ); + $submit_button_background_color = jetpack_calendly_block_get_attribute( $attr, 'customBackgroundButtonColor' ); + $classes = Jetpack_Gutenberg::block_classes( 'calendly', $attr ); + + $url = add_query_arg( + array( + 'hide_event_type_details' => (int) $hide_event_type_details, + 'background_color' => sanitize_hex_color_no_hash( $background_color ), + 'text_color' => sanitize_hex_color_no_hash( $text_color ), + 'primary_color' => sanitize_hex_color_no_hash( $primary_color ), + ), + $url + ); + + if ( 'link' === $style ) { + wp_enqueue_style( 'jetpack-calendly-external-css', 'https://assets.calendly.com/assets/external/widget.css', null, JETPACK__VERSION ); + + /* + * If we have some additional styles from the editor + * (a custom text color, custom bg color, or both ) + * Let's add that CSS inline. + */ + if ( ! empty( $submit_button_text_color ) || ! empty( $submit_button_background_color ) ) { + $inline_styles = sprintf( + '.wp-block-jetpack-calendly .button{%1$s%2$s}', + ! empty( $submit_button_text_color ) + ? 'color:#' . sanitize_hex_color_no_hash( $submit_button_text_color ) . ';' + : '', + ! empty( $submit_button_background_color ) + ? 'background-color:#' . sanitize_hex_color_no_hash( $submit_button_background_color ) . ';' + : '' + ); + wp_add_inline_style( 'jetpack-calendly-external-css', $inline_styles ); + } + + $content = sprintf( + '
%3$s
', + esc_attr( $classes ), + esc_url( $url ), + esc_html( $submit_button_text ) + ); + } else { // Button style. + $content = sprintf( + '
', + esc_attr( $classes ), + esc_url( $url ) + ); + } + + return $content; +} + +/** + * Get filtered attributes. + * + * @param array $attributes Array containing the Calendly block attributes. + * @param string $attribute_name String containing the attribute name to get. + * + * @return string + */ +function jetpack_calendly_block_get_attribute( $attributes, $attribute_name ) { + if ( isset( $attributes[ $attribute_name ] ) ) { + return $attributes[ $attribute_name ]; + } + + $default_attributes = array( + 'url' => 'url', + 'style' => 'inline', + 'submitButtonText' => esc_html__( 'Schedule time with me', 'jetpack' ), + 'backgroundColor' => 'ffffff', + 'textColor' => '4D5055', + 'primaryColor' => '00A2FF', + 'hideEventTypeDetails' => false, + ); + + return $default_attributes[ $attribute_name ]; +} diff --git a/extensions/blocks/calendly/edit.js b/extensions/blocks/calendly/edit.js new file mode 100644 index 0000000000000..9eefd50951f91 --- /dev/null +++ b/extensions/blocks/calendly/edit.js @@ -0,0 +1,253 @@ +/** + * External Dependencies + */ +import 'url-polyfill'; +import { isEqual } from 'lodash'; +import queryString from 'query-string'; + +/** + * WordPress dependencies + */ +import { BlockControls, BlockIcon, InspectorControls } from '@wordpress/block-editor'; +import { + Button, + ExternalLink, + Notice, + PanelBody, + Placeholder, + ToggleControl, + Toolbar, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import icon from './icon'; +import attributeDetails, { getValidatedAttributes } from './attributes'; +import SubmitButton from '../../shared/submit-button'; +import { getAttributesFromEmbedCode } from './utils'; +import BlockStylesPreviewAndSelector from './blockStylesPreviewAndSelector'; + +export default function CalendlyEdit( { attributes, className, clientId, setAttributes } ) { + const validatedAttributes = getValidatedAttributes( attributeDetails, attributes ); + + if ( ! isEqual( validatedAttributes, attributes ) ) { + setAttributes( validatedAttributes ); + } + + const { + backgroundColor, + submitButtonText, + hideEventTypeDetails, + primaryColor, + textColor, + style, + url, + } = validatedAttributes; + const [ embedCode, setEmbedCode ] = useState( '' ); + const [ notice, setNotice ] = useState(); + + const setErrorNotice = () => + setNotice( + <> + { __( + "Your calendar couldn't be embedded. Please double check your URL or code.", + 'jetpack' + ) } + + ); + + const parseEmbedCode = event => { + if ( ! event ) { + setErrorNotice(); + return; + } + + event.preventDefault(); + + const newAttributes = getAttributesFromEmbedCode( embedCode ); + if ( ! newAttributes ) { + setErrorNotice(); + return; + } + + const newValidatedAttributes = getValidatedAttributes( attributeDetails, newAttributes ); + + setAttributes( newValidatedAttributes ); + }; + + const embedCodeForm = ( +
+ setEmbedCode( event.target.value ) } + placeholder={ __( 'Calendly web address or embed code…', 'jetpack' ) } + value={ embedCode } + className="components-placeholder__input" + /> +
+ +
+
+ + { __( 'Need help finding your embed code?', 'jetpack' ) } + +
+
+ ); + + const blockPlaceholder = ( + } + notices={ + notice && ( + + { notice } + + ) + } + > + { embedCodeForm } + + ); + + const iframeSrc = () => { + const query = queryString.stringify( { + embed_domain: 'wordpress.com', + embed_type: 'Inline', + hide_event_type_details: hideEventTypeDetails ? 1 : 0, + background_color: backgroundColor, + primary_color: primaryColor, + text_color: textColor, + } ); + return `${ url }?${ query }`; + }; + + const inlinePreview = ( + <> +
+ + + ); + + const submitButtonPreview = ( + + ); + + const linkPreview = ( + <> + + { submitButtonText } + + + ); + + const blockPreview = ( previewStyle, disabled ) => { + if ( previewStyle === 'inline' ) { + return inlinePreview; + } + + if ( disabled ) { + return linkPreview; + } + + return submitButtonPreview; + }; + + const styleOptions = [ + { name: 'inline', label: __( 'Inline', 'jetpack' ) }, + { name: 'link', label: __( 'Link', 'jetpack' ) }, + ]; + + const blockControls = ( + + { url && ( + ( { + title: styleOption.label, + isActive: styleOption.name === style, + onClick: () => setAttributes( { style: styleOption.name } ), + } ) ) } + popoverProps={ { className: 'is-calendly' } } + /> + ) } + + ); + + const inspectorControls = ( + + { url && ( + <> + + + + + ) } + + +
+ setEmbedCode( event.target.value ) } + placeholder={ __( 'Calendly web address or embed code…', 'jetpack' ) } + value={ embedCode } + className="components-placeholder__input" + /> +
+ +
+
+ + setAttributes( { hideEventTypeDetails: ! hideEventTypeDetails } ) } + /> +
+
+ ); + + return ( + <> + { inspectorControls } + { blockControls } + { url ? blockPreview( style ) : blockPlaceholder } + + ); +} diff --git a/extensions/blocks/calendly/editor.js b/extensions/blocks/calendly/editor.js new file mode 100644 index 0000000000000..d05f403942058 --- /dev/null +++ b/extensions/blocks/calendly/editor.js @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import registerJetpackBlock from '../../shared/register-jetpack-block'; +import { name, settings } from '.'; + +registerJetpackBlock( name, settings ); diff --git a/extensions/blocks/calendly/editor.scss b/extensions/blocks/calendly/editor.scss new file mode 100644 index 0000000000000..20358deff4128 --- /dev/null +++ b/extensions/blocks/calendly/editor.scss @@ -0,0 +1,31 @@ +/** + * Editor styles for Calendly + */ + +.wp-block-jetpack-calendly { + &-overlay { + position: absolute; + width: 100%; + height: 100%; + z-index: 10; + } + + &-link-editable { + cursor: text; + } + + &-embed-form-sidebar { + display: flex; + margin-bottom: 1em; + } + + &-learn-more { + margin-top: 1em; + } +} + +.is-calendly { + .is-active { + font-weight: bold; + } +} diff --git a/extensions/blocks/calendly/icon.js b/extensions/blocks/calendly/icon.js new file mode 100644 index 0000000000000..0547b1c73799d --- /dev/null +++ b/extensions/blocks/calendly/icon.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { SVG, G, Path, Rect } from '@wordpress/components'; + +export default ( + + + + + + + + +); diff --git a/extensions/blocks/calendly/index.js b/extensions/blocks/calendly/index.js new file mode 100644 index 0000000000000..98e5a40778420 --- /dev/null +++ b/extensions/blocks/calendly/index.js @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import attributes from './attributes'; +import edit from './edit'; +import icon from './icon'; + +/** + * Style dependencies + */ +import './editor.scss'; + +export const name = 'calendly'; +export const title = __( 'Calendly', 'jetpack' ); +export const settings = { + title, + description: __( 'Embed a calendar for customers to schedule appointments', 'jetpack' ), + icon, + category: 'jetpack', + keywords: [ + __( 'calendar', 'jetpack' ), + __( 'schedule', 'jetpack' ), + __( 'appointments', 'jetpack' ), + ], + supports: { + html: false, + }, + edit, + save: () => null, + attributes, + example: { + attributes: { + submitButtonText: __( 'Schedule time with me', 'jetpack' ), + hideEventTypeDetails: false, + style: 'inline', + url: 'https://calendly.com/wordpresscom/jetpack-block-example', + }, + }, +}; diff --git a/extensions/blocks/calendly/test/utils.js b/extensions/blocks/calendly/test/utils.js new file mode 100644 index 0000000000000..b95cc34b8893f --- /dev/null +++ b/extensions/blocks/calendly/test/utils.js @@ -0,0 +1,158 @@ +/** + * External dependencies + */ + +/** + * Internal dependencies + */ +import { getAttributesFromEmbedCode } from '../utils'; + +const inlineEmbedCode = '' + + '
' + + '' + + ''; + +const widgetEmbedCode = '' + + '' + + '' + + '' + + ''; + +const textEmbedCode = '' + + '' + + '' + + 'Schedule time with me' + + ''; + +const customInlineEmbedCode = '' + + '
' + + '' + + ''; + +const customWidgetEmbedCode = '' + + '' + + '' + + '' + + ''; + +const customTextEmbedCode = '' + + '' + + '' + + 'Schedule some time with me' + + ''; + +describe( 'getAttributesFromEmbedCode', () => { + test( 'URL with http', () => { + expect( + getAttributesFromEmbedCode( 'https://calendly.com/wordpresscom/jetpack-block-example' ) + ).toEqual( + { + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'URL without http', () => { + expect( + getAttributesFromEmbedCode( 'calendly.com/wordpresscom/jetpack-block-example' ) + ).toEqual( + { + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'URL with query string', () => { + expect( + getAttributesFromEmbedCode( '//calendly.com/wordpresscom/jetpack-block-example?month=2020-01' ) + ).toEqual( + { + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Inline embed code', () => { + expect( + getAttributesFromEmbedCode( inlineEmbedCode ) + ).toEqual( + { + "style": "inline", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Widget embed code', () => { + expect( + getAttributesFromEmbedCode( widgetEmbedCode ) + ).toEqual( + { + "style": "link", + "submitButtonText": "Schedule time with me", + "customBackgroundButtonColor": "#00a2ff", + "customTextButtonColor": "#ffffff", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Text embed code', () => { + expect( + getAttributesFromEmbedCode( textEmbedCode ) + ).toEqual( + { + "style": "link", + "submitButtonText": "Schedule time with me", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Customised inline embed code', () => { + expect( + getAttributesFromEmbedCode( customInlineEmbedCode ) + ).toEqual( + { + "backgroundColor": "691414", + "hideEventTypeDetails": "1", + "primaryColor": "1d6e9c", + "style": "inline", + "textColor": "2051a3", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Customised widget embed code', () => { + expect( + getAttributesFromEmbedCode( customWidgetEmbedCode ) + ).toEqual( + { + "backgroundColor": "c51414", + "customBackgroundButtonColor": "#000609", + "customTextButtonColor": "#b50000", + "primaryColor": "1d73a4", + "style": "link", + "submitButtonText": "Schedule some time with me", + "textColor": "2563ca", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ); + + test( 'Customised text embed code', () => { + expect( + getAttributesFromEmbedCode( customTextEmbedCode ) + ).toEqual( + { + "backgroundColor": "e32424", + "primaryColor": "0e425f", + "style": "link", + "submitButtonText": "Schedule some time with me", + "textColor": "2a74ef", + "url": "https://calendly.com/wordpresscom/jetpack-block-example" + } + ); + } ) +} ); diff --git a/extensions/blocks/calendly/utils.js b/extensions/blocks/calendly/utils.js new file mode 100644 index 0000000000000..b1cac47a8ac09 --- /dev/null +++ b/extensions/blocks/calendly/utils.js @@ -0,0 +1,111 @@ +export const getURLFromEmbedCode = embedCode => { + const url = embedCode.match( /(? { + let submitButtonText = embedCode.match( /(?<=false;"\>).+(?=<\/)/ ); + if ( submitButtonText ) { + return submitButtonText[ 0 ]; + } + + submitButtonText = embedCode.match( /(?<=text: ').*?(?=')/ ); + if ( submitButtonText ) { + return submitButtonText[ 0 ]; + } +}; + +const getSubmitButtonTextColorFromEmbedCode = embedCode => { + const submitButtonTextColor = embedCode.match( /(?<= textColor: ').*?(?=')/ ); + if ( submitButtonTextColor ) { + return submitButtonTextColor[ 0 ]; + } +}; + +const getSubmitButtonBackgroundColorFromEmbedCode = embedCode => { + const submitButtonBackgroundColor = embedCode.match( /(?<= color: ').*?(?=')/ ); + if ( submitButtonBackgroundColor ) { + return submitButtonBackgroundColor[ 0 ]; + } +}; + +export const getAttributesFromUrl = url => { + const attributes = {}; + const urlObject = new URL( url ); + attributes.url = urlObject.origin + urlObject.pathname; + + if ( ! urlObject.search ) { + return attributes; + } + + const searchParams = new URLSearchParams( urlObject.search ); + const backgroundColor = searchParams.get( 'background_color' ); + const primaryColor = searchParams.get( 'primary_color' ); + const textColor = searchParams.get( 'text_color' ); + const hexRegex = /^[A-Za-z0-9]{6}$/; + + if ( searchParams.get( 'hide_event_type_details' ) ) { + attributes.hideEventTypeDetails = searchParams.get( 'hide_event_type_details' ); + } + + if ( backgroundColor && backgroundColor.match( hexRegex ) ) { + attributes.backgroundColor = backgroundColor; + } + + if ( primaryColor && primaryColor.match( hexRegex ) ) { + attributes.primaryColor = primaryColor; + } + + if ( textColor && textColor.match( hexRegex ) ) { + attributes.textColor = textColor; + } + + return attributes; +}; + +const getStyleFromEmbedCode = embedCode => { + if ( embedCode.indexOf( 'data-url' ) > 0 ) { + return 'inline'; + } + + if ( embedCode.indexOf( 'initPopupWidget' ) > 0 || embedCode.indexOf( 'initBadgeWidget' ) > 0 ) { + return 'link'; + } +}; + +export const getAttributesFromEmbedCode = embedCode => { + if ( ! embedCode ) { + return; + } + + const newUrl = getURLFromEmbedCode( embedCode ); + if ( ! newUrl ) { + return; + } + + const newAttributes = getAttributesFromUrl( newUrl ); + + const newStyle = getStyleFromEmbedCode( embedCode ); + if ( newStyle ) { + newAttributes.style = newStyle; + } + + const submitButtonText = getSubmitButtonTextFromEmbedCode( embedCode ); + if ( submitButtonText ) { + newAttributes.submitButtonText = submitButtonText; + } + + const submitButtonTextColor = getSubmitButtonTextColorFromEmbedCode( embedCode ); + if ( submitButtonTextColor ) { + newAttributes.customTextButtonColor = submitButtonTextColor; + } + + const submitButtonBackgroundColor = getSubmitButtonBackgroundColorFromEmbedCode( embedCode ); + if ( submitButtonBackgroundColor ) { + newAttributes.customBackgroundButtonColor = submitButtonBackgroundColor; + } + + return newAttributes; +}; diff --git a/extensions/index.json b/extensions/index.json index 454fd3e95f9f6..551d29858d682 100644 --- a/extensions/index.json +++ b/extensions/index.json @@ -23,5 +23,5 @@ "videopress", "wordads" ], - "beta": [ "seo", "opentable" ] + "beta": [ "seo", "opentable", "calendly" ] }