diff --git a/src/block/design-library/edit.js b/src/block/design-library/edit.js index a1f62895e4..e7e39f8fc4 100644 --- a/src/block/design-library/edit.js +++ b/src/block/design-library/edit.js @@ -6,153 +6,283 @@ import { i18n, srcUrl, settings, + cdnUrl, } from 'stackable' import { Button, ModalDesignLibrary, } from '~stackable/components' import { SVGStackableIcon } from '~stackable/icons' -import { - deprecateBlockBackgroundColorOpacity, deprecateContainerBackgroundColorOpacity, deprecateTypographyGradientColor, -} from '~stackable/block-components' import { substituteCoreIfDisabled, BLOCK_STATE } from '~stackable/util' +import { usePresetControls } from '~stackable/hooks' import { substitutionRules } from '../../blocks' /** * WordPress dependencies */ import { __ } from '@wordpress/i18n' -import { dispatch } from '@wordpress/data' +import { dispatch, select } from '@wordpress/data' import { - createBlock, parse, createBlocksFromInnerBlocksTemplate, getBlockVariations, getBlockType, + createBlock, createBlocksFromInnerBlocksTemplate, getBlockVariations, getBlockType, } from '@wordpress/blocks' -import { useState } from '@wordpress/element' -import { addFilter, applyFilters } from '@wordpress/hooks' -import { Placeholder } from '@wordpress/components' +import { useRef, useState } from '@wordpress/element' +import { applyFilters } from '@wordpress/hooks' +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + Placeholder, Modal, __experimentalVStack as VStack, Flex, +} from '@wordpress/components' import { useBlockProps } from '@wordpress/block-editor' +import apiFetch from '@wordpress/api-fetch' +const convertBlocksToArray = block => { + const innerBlocks = block.innerBlocks.map( innerBlock => convertBlocksToArray( innerBlock ) ) + return [ block.name, block.attributes, innerBlocks ] +} -// Replaces the current block with a block made out of attributes. -const createBlockWithAttributes = ( blockName, attributes, innerBlocks, design ) => { - const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase - let hasSubstituted = false - - // Recursively substitute core blocks to disabled Stackable blocks - const traverseBlocksAndSubstitute = blocks => { - return blocks.map( block => { - let isDisabled = true - // Maximum attempt to error if no substitution rule for the block - let attempts = 10 - - // Check if the new substituted block is still disabled - while ( isDisabled && attempts > 0 ) { - const previousBlockName = block[ 0 ] - block = substituteCoreIfDisabled( ...block, substitutionRules ) - isDisabled = block[ 0 ] in disabledBlocks && disabledBlocks[ block[ 0 ] ] === BLOCK_STATE.DISABLED - // If the previous block is different from the new block, substitution has been made - if ( ! hasSubstituted && previousBlockName !== block[ 0 ] ) { - hasSubstituted = true - } - attempts-- - } +const checkIfImageUrl = async value => { + if ( typeof value !== 'string' ) { + return value + } - // Do a preorder traversal by subsituting first before traversing - if ( block[ 2 ] && block[ 2 ].length > 0 ) { - block[ 2 ] = traverseBlocksAndSubstitute( block[ 2 ] ) - } + let attributeUrl, libraryUrl - if ( ! Array.isArray( block[ 2 ] ) ) { - block[ 2 ] = [] - } - return block - } ) + try { + attributeUrl = new URL( value ) + libraryUrl = new URL( cdnUrl ) + } catch ( error ) { + return value + } + + if ( attributeUrl.origin !== libraryUrl.origin || ! attributeUrl.pathname.startsWith( libraryUrl.pathname ) ) { + return value } - // Substitute from the root of the design - [ blockName, attributes, innerBlocks ] = traverseBlocksAndSubstitute( [ [ blockName, attributes, innerBlocks ] ] )[ 0 ] + const matches = attributeUrl.pathname.match( /\/([^/]+\.(jpe?g|gif|png|mp4|webp))$/i ) - if ( hasSubstituted ) { - // eslint-disable-next-line no-alert - alert( 'Notice: Disabled blocks in the design will be substituted with other Stackable or core blocks' ) + if ( matches ) { + try { + const result = await apiFetch( { + path: '/stackable/v3/design_library_image', + method: 'POST', + // eslint-disable-next-line camelcase + data: { image_url: attributeUrl.href }, + } ) + if ( result.success ) { + return result.new_url + } + console.error( 'Stackable Design Library:', result.message ) // eslint-disable-line no-console + return value + } catch ( error ) { + console.error( 'Stackable Design Library:', error.message ) // eslint-disable-line no-console + return value + } } + return value +} + +const Edit = props => { + const { + clientId, + attributes, + } = props + + const [ isLibraryOpen, setIsLibraryOpen ] = useState( false ) + const [ isDialogOpen, setIsDialogOpen ] = useState( false ) + + const designsRef = useRef( [] ) + const disabledBlocksRef = useRef( [] ) + const callbackRef = useRef( null ) - // const { replaceBlock } = dispatch( 'core/block-editor' ) + const blockProps = useBlockProps( { + className: 'ugb-design-library-block', + } ) + + const presetMarks = usePresetControls( 'spacingSizes' ) + ?.getPresetMarks() || null + + const spacingSize = ! presetMarks || ! Array.isArray( presetMarks ) ? 120 : presetMarks[ presetMarks.length - 2 ].value + + // Replaces the current block with a block made out of attributes. + const createBlockWithAttributes = async ( category, blockName, attributes, innerBlocks, substituteBlocks, parentClientId ) => { + const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase - // For wireframes, we'll need to apply any default block attributes to - // the blocks. We do this by ensuring that all uniqueIds are removed, - // this prompts the block to generate a new one and give itself a - // default styling. - if ( design.uikit === 'Wireframes' ) { - const hasVariations = getBlockVariations( blockName ).length > 0 - if ( ! hasVariations ) { - attributes.uniqueId = '' + // Recursively substitute core blocks to disabled Stackable blocks + const traverseBlocksAndSubstitute = blocks => { + return blocks.map( block => { + let isDisabled = true + // Maximum attempt to error if no substitution rule for the block + let attempts = 10 + + // Check if the new substituted block is still disabled + while ( isDisabled && attempts > 0 ) { + const _blockName = block[ 1 ].originalName || block[ 0 ] + block = substituteCoreIfDisabled( _blockName, block[ 1 ], block[ 2 ], substitutionRules ) + isDisabled = block[ 0 ] in disabledBlocks && disabledBlocks[ block[ 0 ] ] === BLOCK_STATE.DISABLED + attempts-- + } + + // Do a preorder traversal by subsituting first before traversing + if ( block[ 2 ] && block[ 2 ].length > 0 ) { + block[ 2 ] = traverseBlocksAndSubstitute( block[ 2 ] ) + } + + if ( ! Array.isArray( block[ 2 ] ) ) { + block[ 2 ] = [] + } + + const _block = { + name: block[ 0 ], + attributes: block[ 1 ], + innerBlocks: block[ 2 ], + isValid: true, + } + return _block + } ) } - // Recursively remove all uniqueIds from all inner blocks. - const removeUniqueId = blocks => { - blocks.forEach( block => { - const blockName = block[ 0 ] + if ( ! Array.isArray( disabledBlocks ) && substituteBlocks ) { + let block = convertBlocksToArray( { + name: blockName, attributes, innerBlocks, + } ) + + block = traverseBlocksAndSubstitute( [ block ] )[ 0 ] + blockName = block.name + attributes = block.attributes + innerBlocks = block.innerBlocks + } + + const cleanBlockAttributes = async blocks => { + for ( const block of blocks ) { + const blockName = block.name // For blocks with variations, do not remove the uniqueId // since that will prompt the layout picker to show. const hasVariations = !! getBlockType( blockName ) && getBlockVariations( blockName ).length > 0 - if ( ! hasVariations && block[ 1 ].uniqueId ) { - delete block[ 1 ].uniqueId + if ( ! hasVariations && block.attributes.uniqueId ) { + delete block.attributes.uniqueId } - removeUniqueId( block[ 2 ] ) - } ) + const customAttributes = block.attributes.customAttributes + + const indexToDelete = customAttributes?.findIndex( attribute => attribute[ 0 ] === 'stk-design-library__bg-target' ) + if ( customAttributes && indexToDelete !== -1 ) { + block.attributes.customAttributes.splice( indexToDelete, 1 ) + } + + for ( const attributeName in block.attributes ) { + if ( typeof block.attributes[ attributeName ] === 'string' ) { + const value = String( block.attributes[ attributeName ] ) + block.attributes[ attributeName ] = await checkIfImageUrl( value ) + } + } + + block.innerBlocks = await cleanBlockAttributes( block.innerBlocks ) + } + + return blocks } - removeUniqueId( innerBlocks ) - } + const block = await cleanBlockAttributes( [ { + name: blockName, attributes, innerBlocks, + } ] ) + + blockName = block[ 0 ].name + attributes = block[ 0 ].attributes + innerBlocks = block[ 0 ].innerBlocks - const shortBlockName = blockName.replace( /^\w+\//g, '' ) - - // Recursively update the attributes of all inner blocks for the new Color Picker - const migrateToNewColorPicker = blocks => { - blocks?.forEach( block => { - try { - let newAttributes = block[ 1 ] - newAttributes = deprecateContainerBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateBlockBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateTypographyGradientColor.migrate( '%s' )( newAttributes ) - block[ 1 ] = newAttributes - migrateToNewColorPicker( block[ 2 ] ) - } catch ( error ) { + if ( category !== 'Header' ) { + if ( ! parentClientId && attributes.hasBackground ) { + attributes.blockMargin = { + top: '', + right: '', + bottom: '0', + left: '', + } + } else if ( ! parentClientId ) { + attributes.blockMargin = { + top: spacingSize, + right: '', + bottom: spacingSize, + left: '', + } + } + + const blockLayouts = select( 'stackable/global-spacing-and-borders' ).getBlockLayouts() + if ( attributes.hasBackground && typeof blockLayouts === 'object' && ! blockLayouts[ 'block-background-padding' ] ) { + attributes.blockPadding = { + top: spacingSize, + right: spacingSize, + bottom: spacingSize, + left: spacingSize, + } } - } ) + } + + return createBlock( blockName, attributes, createBlocksFromInnerBlocksTemplate( innerBlocks ) ) } - migrateToNewColorPicker( innerBlocks ) + const addDesigns = async substituteBlocks => { + const { getBlockRootClientId } = select( 'core/block-editor' ) + const parentClientId = getBlockRootClientId( clientId ) - addFilter( `stackable.${ shortBlockName }.design.filtered-block-attributes`, 'stackable.design-library.attributes--migrate-to-new-color-picker', attributes => { - let newAttributes = { ...attributes } - newAttributes = deprecateContainerBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateBlockBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateTypographyGradientColor.migrate( '%s' )( newAttributes ) - return newAttributes - } ) + if ( ! designsRef.current?.length ) { + console.error( 'Design library selection failed: No designs found' ) // eslint-disable-line no-console + } - const blockAttributes = applyFilters( `stackable.${ shortBlockName }.design.filtered-block-attributes`, attributes ) + const designs = designsRef.current + const blocks = [] - return createBlock( blockName, blockAttributes, createBlocksFromInnerBlocksTemplate( innerBlocks ) ) -} + for ( const blockDesign of designs ) { + const { designData, category } = blockDesign + const { + name, attributes, innerBlocks, + } = designData + if ( name && attributes ) { + const block = await createBlockWithAttributes( category, name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], substituteBlocks, parentClientId ) + blocks.push( block ) + } else { + console.error( 'Design library selection failed: No block data found' ) // eslint-disable-line no-console + } + } -const createBlockWithContent = serializedBlock => { - return parse( serializedBlock ) -} + if ( blocks.length ) { + dispatch( 'core/block-editor' ).replaceBlocks( clientId, blocks ) + if ( callbackRef.current ) { + callbackRef.current() + } + } + } -const Edit = props => { - const { - clientId, - attributes, - } = props + const onClickTertiary = () => { + const disabledBlocks = disabledBlocksRef.current - const [ isLibraryOpen, setIsLibraryOpen ] = useState( false ) + const settingsPageUrl = `/wp-admin/options-general.php?page=stackable` + const newWindow = window.open( settingsPageUrl, '_blank' ) - const blockProps = useBlockProps( { - className: 'ugb-design-library-block', - } ) + if ( newWindow ) { + newWindow.onload = () => { // Wait for the new page to fully load + setTimeout( () => { + try { + const blocksTab = newWindow.document.getElementById( 'stk-tab__blocks' ) + if ( blocksTab ) { + blocksTab.click() + + newWindow.postMessage( { + type: 'STK_ENABLE_BLOCKS', blocks: disabledBlocks, source: 'STK_DESIGN_LIBRARY', + }, window.location.origin ) + } + } catch ( error ) {} + }, 5 ) + } + newWindow.focus() + } + } + + const onClickSecondary = async () => { + await addDesigns( false ) + } + const onClickPrimary = async () => { + await addDesigns( true ) + } if ( attributes.previewMode ) { const src = previewImage.match( /https?:/i ) ? previewImage @@ -187,35 +317,76 @@ const Edit = props => { onClose={ () => { setIsLibraryOpen( false ) } } - onSelect={ ( _designData, _design, callback = null ) => { - const designData = ! Array.isArray( _designData ) ? [ _designData ] : _designData - const designs = ! Array.isArray( _design ) ? [ _design ] : _design + onSelect={ async ( _designs, callback ) => { + const designs = [] + let disabledBlocks = new Set() - const blocks = designData.reduce( ( blocks, designData, i ) => { - const design = designs[ i ] + _designs.forEach( design => { const { - name, attributes, innerBlocks, serialized, - } = designData - if ( name && attributes ) { - const block = createBlockWithAttributes( name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], design ) - blocks.push( block ) - } else if ( serialized ) { - blocks.push( createBlockWithContent( serialized ) ) - } else { - console.error( 'Design library selection failed: No block data found' ) // eslint-disable-line no-console - } - return blocks - }, [] ) + designData, blocksForSubstitution, category, + } = design - if ( blocks.length ) { - dispatch( 'core/block-editor' ).replaceBlocks( clientId, blocks ) - if ( callback ) { - callback() + if ( blocksForSubstitution.size ) { + disabledBlocks = disabledBlocks.union( blocksForSubstitution ) } + + designs.push( { designData, category } ) + } ) + + designsRef.current = designs + disabledBlocksRef.current = disabledBlocks + callbackRef.current = callback + + if ( disabledBlocks.size ) { + setIsDialogOpen( true ) + return } + + await addDesigns( false ) } } /> } + { isDialogOpen && + setIsDialogOpen( false ) } + > + +
+ { __( 'The designs you have selected contain the following disabled blocks:', i18n ) } +
    + { disabledBlocksRef.current && [ ...disabledBlocksRef.current ].map( ( block, i ) =>
  • { block }
  • ) } +
+ { __( 'These blocks can be enabled in the Stackable settings page. Do you want to keep the disabled blocks or substitute them with other Stackable or core blocks?', i18n ) } +
+ + + + + +
+ +
+ } ) } diff --git a/src/block/icon-list/substitute.js b/src/block/icon-list/substitute.js index ac1c1da6c1..ae4eecb46e 100644 --- a/src/block/icon-list/substitute.js +++ b/src/block/icon-list/substitute.js @@ -1,14 +1,16 @@ export const substitute = { from: 'stackable/icon-list', - transform: () => { + transform: ( oldAttributes, innerBlocks ) => { + const newInnerBlocks = innerBlocks.reduce( ( newInnerBlocks, innerBlock ) => { + const attributes = innerBlock[ 1 ] + + newInnerBlocks.push( [ 'core/list-item', { content: attributes.text } ] ) + return newInnerBlocks + }, [] ) return [ 'core/list', {}, - [ - [ 'core/list-item', { content: 'First item list' } ], - [ 'core/list-item', { content: 'Second item list' } ], - [ 'core/list-item', { content: 'Third item list' } ], - ], + newInnerBlocks, ] }, } diff --git a/src/compatibility/tove/style.scss b/src/compatibility/tove/style.scss new file mode 100644 index 0000000000..d625512032 --- /dev/null +++ b/src/compatibility/tove/style.scss @@ -0,0 +1,4 @@ +:where(.stk-has-block-style-inheritance.stk--is-tove-theme) :is(.stk-block-button, .stk-block-icon-button, .stk-block-pagination):is(.is-style-plain) .stk-button { + border-width: 0; + box-shadow: none; +} diff --git a/src/components/color-scheme-preview/editor.scss b/src/components/color-scheme-preview/editor.scss index 3960c92a55..6f7347181d 100644 --- a/src/components/color-scheme-preview/editor.scss +++ b/src/components/color-scheme-preview/editor.scss @@ -76,3 +76,15 @@ .stk-scheme--is-disabled { opacity: 0.1; } + +.stk-scheme--is-collapsed { + flex-direction: row; + gap: 4px; + align-items: center; + max-height: 30px; + + .stk-global-color-scheme__preview__typography { + margin-bottom: 0; + font-size: 12px; + } +} diff --git a/src/components/color-scheme-preview/index.js b/src/components/color-scheme-preview/index.js index 2389b92799..3dcff64a4b 100644 --- a/src/components/color-scheme-preview/index.js +++ b/src/components/color-scheme-preview/index.js @@ -14,13 +14,18 @@ export const DEFAULT_COLOR_SCHEME_COLORS = { const NOOP = () => {} const ColorSchemePreview = ( { - colors, withWrapper = false, onClick = NOOP, isDisabled = false, + colors, + withWrapper = false, + onClick = NOOP, + isDisabled = false, + isCollapsed = false, } ) => { const TagName = onClick === NOOP ? 'div' : Button const additionalProps = onClick === NOOP ? {} : { onClick } const classNames = classnames( 'stk-global-color-scheme__preview__background', { 'stk-scheme--is-disabled': isDisabled, + 'stk-scheme--is-collapsed': isCollapsed, } ) return ( diff --git a/src/components/color-schemes-help/index.js b/src/components/color-schemes-help/index.js index caeef807a1..8401d9fb9f 100644 --- a/src/components/color-schemes-help/index.js +++ b/src/components/color-schemes-help/index.js @@ -4,8 +4,20 @@ import { Link } from '~stackable/components' import { __ } from '@wordpress/i18n' import { dispatch } from '@wordpress/data' -export const ColorSchemesHelp = () => { +export const ColorSchemesHelp = props => { + const { + customText, callback, className, + } = props const onClick = () => { + let cancelOnClick = false + if ( callback ) { + cancelOnClick = callback() + } + + if ( cancelOnClick ) { + return + } + // Open the global settings sidebar. dispatch( 'core/edit-post' )?.openGeneralSidebar( 'stackable-global-settings/sidebar' ) // For Block Editor dispatch( 'core/edit-site' )?.openGeneralSidebar( 'stackable-global-settings/sidebar' ) // For Site Editor @@ -31,9 +43,10 @@ export const ColorSchemesHelp = () => { } return <> - { __( 'Change the color scheme.', i18n ) } -   - { __( 'Manage your color schemes.', i18n ) } + { customText || customText === '' ? customText + : { __( 'Change the color scheme.', i18n ) } } + { customText !== '' && <>  } + { __( 'Manage your color schemes.', i18n ) } } diff --git a/src/components/design-library-list/default.json b/src/components/design-library-list/default.json new file mode 100644 index 0000000000..10acad68b6 --- /dev/null +++ b/src/components/design-library-list/default.json @@ -0,0 +1,502 @@ +{ + "Call to Action": { + "heading_placeholder": "Transform Your Business Today", + "description_placeholder": "Unlock your full potential with our cutting-edge solutions. We empower businesses of all sizes to achieve their goals through innovative technology and unparalleled support.", + "kicker_placeholder": "Seize the Opportunity", + "btn-1_placeholder": "Get Started Now", + "btn-2_placeholder": "Learn More", + "item-1_placeholder": "Streamline your operations and boost efficiency.", + "item-2_placeholder": "Gain actionable insights with advanced analytics." + }, + "Card": { + "heading_placeholder": "Unlock Your Full Potential", + "long-description_placeholder": "At XYZ Company, we partner for your growth. Our solutions leverage cutting-edge technology, provide expert guidance, and ensure seamless integration for measurable results. We empower you with the tools and support to confidently navigate challenges, building your success on innovation and reliability.", + "description_placeholder": "Discover how our comprehensive solutions empower your journey.", + "subtitle_placeholder": "Your Path to Innovation and Growth", + "title_placeholder": "Solutions Designed to Propel You Forward", + "c-btn_placeholder": "Learn More", + "c-title-1_placeholder": "Innovative Solutions", + "c-subtitle-1_placeholder": "Cutting-Edge Technology", + "c-description-1_placeholder": "Discover how our advanced solutions can revolutionize your approach, providing efficiency and unprecedented growth.", + "c-title-2_placeholder": "Expert Guidance", + "c-subtitle-2_placeholder": "Personalized Support", + "c-description-2_placeholder": "Benefit from our team's extensive knowledge and receive tailored advice to meet your unique challenges.", + "c-title-3_placeholder": "Seamless Integration", + "c-subtitle-3_placeholder": "Effortless Setup", + "c-description-3_placeholder": "Our products are designed for easy integration, ensuring a smooth transition and immediate productivity.", + "c-title-4_placeholder": "Measurable Results", + "c-subtitle-4_placeholder": "Data-Driven Outcomes", + "c-description-4_placeholder": "Track your progress with comprehensive analytics and see the tangible impact of our services on your goals.", + "c-title-5_placeholder": "Community & Support", + "c-subtitle-5_placeholder": "Connect and Collaborate", + "c-description-5_placeholder": "Join a thriving community of users and access dedicated support to help you every step of the way.", + "c-title-6_placeholder": "Future-Proof Design", + "c-subtitle-6_placeholder": "Scalable and Adaptable", + "c-description-6_placeholder": "Invest in solutions built for tomorrow, with the flexibility to grow and evolve with your needs." + }, + "Carousel": { + "subheading_placeholder": "Learn and Grow", + "heading_placeholder": "Explore Our Featured Resources", + "description_placeholder": "Get inspired, sharpen your skills, and stay ahead with curated content from our team and industry experts.", + "kicker_placeholder": "Learn and Grow", + "btn_placeholder": "View All Resources", + "title-1_placeholder": "The Beginner's Guide to UX Design", + "text-1_placeholder": "A friendly introduction to core UX principles and design thinking.", + "title-2_placeholder": "Scaling Your App Without the Stress", + "text-2_placeholder": "Best practices for growing your platform while keeping performance sharp.", + "title-3_placeholder": "Building a Brand That Connects", + "text-3_placeholder": "Learn how to craft a brand identity that resonates with real people.", + "title-4_placeholder": "Marketing Trends You Can't Ignore", + "text-4_placeholder": "Stay updated with actionable insights from today's digital landscape." + }, + "Contact": { + "heading_placeholder": "Get in Touch With Us", + "kicker_placeholder": "Contact Us", + "description_placeholder": "Whether you have questions, need support, or want to explore opportunities—our team is just a message away.", + "btn_placeholder": "Learn More", + "title-1_placeholder": "Job Opportunities", + "text-1_placeholder": "Join our growing team and help us build the future—one line of code at a time.", + "title-2_placeholder": "Support", + "text-2_placeholder": "Reach out to our friendly support team for quick assistance with any technical or service-related concerns.", + "title-3_placeholder": "Sales", + "text-3_placeholder": "Discuss your goals with our sales team and discover how our solutions can meet your business needs.", + "cta_placeholder": "Contact Us" + }, + "FAQ": { + "heading_placeholder": "Frequently Asked Questions (FAQs)", + "description_placeholder": "Find answers to the most common questions about [Your Product/Service]. If you can't find what you're looking for here, please contact us.", + "kicker_placeholder": "Quick Answers to Your Top Questions", + + "question-1_placeholder": "What features does your service offer?", + "answer-1_placeholder": "Our service offers a comprehensive set of features designed to help you manage your tasks. Key features include Feature A, Feature B, and Feature C. You can find a full list and details on our Features Page.", + + "question-2_placeholder": "Is our Service integrated with other products?", + "answer-2_placeholder": "Yes, our Service offers integrations with various popular products and services to streamline your workflow. We currently integrate with Product A, Product B, and Product C. You can find more information about our current integrations and how to set them up on our [Integrations Page Link]. We are continuously working on adding more integrations.", + + "question-3_placeholder": "What are the pricing options involved?", + "answer-3_placeholder": "We offer several pricing options to suit different needs. You can view a detailed breakdown of our plans and features on our Pricing Page. We have a free tier, monthly subscriptions, and annual plans.", + + "question-4_placeholder": "What payment methods do you accept?", + "answer-4_placeholder": "We accept various convenient payment methods, including major credit cards (Visa, MasterCard, American Express), and in some cases, bank transfers. You can select your preferred payment method during the checkout or subscription process.", + + "question-5_placeholder": "What is your cancellation/refund policy?", + "answer-5_placeholder": "Our cancellation policy allows you to cancel your subscription at any time through your account settings. Regarding refunds, our policy is we offer a 30-day money-back guarantee. Please refer to our Terms of Service Page for complete details.", + + "question-6_placeholder": "What are your hours of operation?", + "answer-6_placeholder": "Our standard hours of operation are Monday to Friday, 9:00 AM to 5:00 PM in Pacific Standard Time (PST).", + + "question-7_placeholder": "How can I contact customer support?", + "answer-7_placeholder": "You can contact our customer support team through several channels: Submit a request via our Support Contact Form. Email us directly at support@email.com. Use our live chat feature available on our website during business hours. Our support hours are Monday-Friday, 9 AM - 5 PM Pacific Standard Time (PST).", + + "question-8_placeholder": "How can I provide feedback?", + "answer-8_placeholder": "We highly value your feedback as it helps us improve. You can provide feedback through several methods: Use the feedback form on our website, or email your suggestions or comments to feedback@email.com. You can also participate in surveys. We review all feedback submitted." + }, + "Featured Product": { + "kicker_placeholder": "Discover Innovation", + "product-name-1_placeholder": "Smart Lamp", + "product-description-1_placeholder": "An intelligent lamp that adapts to your mood and environment with customizable ambiance and smart home integration.", + "feature-1_placeholder": "Adaptive Lighting", + "feature-description-1_placeholder": "Adjusts brightness and color automatically.", + "feature-2_placeholder": "Voice Control", + "feature-description-2_placeholder": "Hands-free operation with smart assistants.", + "feature-3_placeholder": "Personalized Scenes", + "feature-description-3_placeholder": "Create and save custom lighting presets.", + "feature-4_placeholder": "Energy Efficient", + "feature-description-4_placeholder": "Low power consumption with LED technology.", + "feature-5_placeholder": "Sleek Design", + "feature-description-5_placeholder": "Modern aesthetic to complement any decor.", + "feature-6_placeholder": "Smart Connectivity", + "feature-description-6_placeholder": "Connects to Wi-Fi for remote control.", + "product-name-2_placeholder": "Ergonomic Chair", + "product-description-2_placeholder": "A chair designed for ultimate comfort and posture support, enhancing productivity.", + "product-name-3_placeholder": "Portable Projector", + "product-description-3_placeholder": "A compact and powerful projector for cinematic quality anywhere.", + "btn-1_placeholder": "Buy now", + "btn-2_placeholder": "Add to Favorites" + }, + "Footer": { + "heading_placeholder": "Contact us", + "kicker_placeholder": "Stay connected", + "company-description_placeholder": "InnovateCo creates cutting-edge solutions for modern living.", + "company_placeholder": "InnovateCo", + "tagline_placeholder": "Innovation for a Better Tomorrow", + "cta_placeholder": "Join our Community", + "cta-btn_placeholder": "Sign up now", + "btn-1_placeholder": "Subscribe", + "btn-2_placeholder": "Watch Demo", + "menu-1_placeholder": "Main", + "menu-2_placeholder": "About", + "link-1_placeholder": "Our Story", + "link-2_placeholder": "Team", + "link-3_placeholder": "Careers", + "menu-3_placeholder": "Products", + "link-4_placeholder": "Shop All", + "link-5_placeholder": "Bestsellers", + "link-6_placeholder": "New Releases", + "menu-4_placeholder": "Blog", + "link-7_placeholder": "Latest Articles", + "link-8_placeholder": "Categories", + "link-9_placeholder": "Archives", + "menu-5_placeholder": "Support", + "link-10_placeholder": "FAQs", + "link-11_placeholder": "Contact Us", + "link-12_placeholder": "Shipping & Returns" + }, + "Gallery": { + "heading_placeholder": "Nature's Grand Canvas", + "description_placeholder": "Explore breathtaking landscapes and the serene beauty of our planet. Each image unveils a unique story from the heart of nature.", + "kicker_placeholder": "Earth's Masterpieces", + "btn_placeholder": "Explore More", + "img-subtitle-1_placeholder": "Dolomite Majesty", + "img-title-1_placeholder": "Peaks of Tre Cime di Lavaredo", + "img-description-1_placeholder": "Towering rock formations of the Dolomites, including the formidable Tre Cime di Lavaredo, emerge through a misty sky, showcasing raw geological power.", + + "img-subtitle-2_placeholder": "Alpine Valleys", + "img-title-2_placeholder": "South Tyrol's Autumn Embrace", + "img-description-2_placeholder": "A tranquil scene in South Tyrol, Italy, where golden larch trees dot verdant hillsides, backed by snow-dusted mountains under a soft light.", + + "img-subtitle-3_placeholder": "Winter Wonderland", + "img-title-3_placeholder": "Phelps Lake Winter Reflection", + "img-description-3_placeholder": "A serene winter landscape featuring Phelps Lake, perfectly mirroring snow-covered mountains and frost-dusted pine trees under a clear, crisp sky.", + + "img-subtitle-4_placeholder": "Twilight Glow", + "img-title-4_placeholder": "Valley Lights at Dusk", + "img-description-4_placeholder": "As twilight settles, a valley below glows with the golden lights of a town, nestled against dark, silhouetted mountains under a streaked sky.", + + "img-subtitle-5_placeholder": "Ethereal Forest", + "img-title-5_placeholder": "Misty Woodland Pathways", + "img-description-5_placeholder": "A hauntingly beautiful forest shrouded in mist, where tall, bare trees stand solemnly above a carpet of rust-colored ferns, creating a sense of deep mystery.", + + "img-subtitle-6_placeholder": "Autumn Slopes", + "img-title-6_placeholder": "Golden Larches of the Alps", + "img-description-6_placeholder": "A stunning view across a mountain valley where vibrant golden larch trees cascade down slopes, contrasting with darker evergreens and distant peaks.", + "img-subtitle-7_placeholder": "Dolomite Grandeur", + "img-title-7_placeholder": "Pale di San Martino's Fiery Peaks", + "img-description-7_placeholder": "The dramatic, sun-kissed peaks of the Pale di San Martino in the Dolomites catch the warm light, revealing their rugged textures and imposing scale.", + "img-subtitle-8_placeholder": "Reflective Waters", + "img-title-8_placeholder": "Lago di Limides Mirror", + "img-description-8_placeholder": "The still, clear waters of Lago di Limides flawlessly reflect the cloudy sky and distant, mist-shrouded Dolomites, framed by pine trees along the shore." + }, + "Header": { + "company_placeholder": "InnovateCo", + "link-1_placeholder": "About", + "link-2_placeholder": "Products", + "link-3_placeholder": "Blog", + "link-4_placeholder": "Support", + "btn_placeholder": "Login" + }, + "Hero": { + "heading_placeholder": "Elevate Your Digital Future", + "long-description_placeholder": "At XYZ Company, we don't just build products; we forge partnerships. Our dedicated team works tirelessly to understand your unique challenges, crafting tailored strategies for sustainable growth. From concept to execution, we ensure a seamless and transformative experience.", + "description_placeholder": "Driving global business growth through innovative solutions and unparalleled expertise.", + "kicker_placeholder": "Future-Ready Solutions", + "btn-1_placeholder": "Get Started Today", + "btn-2_placeholder": "Learn More", + "item-1_placeholder": "Scalable Solutions", + "item-2_placeholder": "Expert Guidance", + "item-3_placeholder": "Proven Impact", + "metric-1_placeholder": "10M", + "metric-label-1_placeholder": "Users Engaged", + "metric-2_placeholder": "250", + "metric-label-2_placeholder": "Solutions", + "metric-3_placeholder": "20", + "metric-label-3_placeholder": "Years of Expertise" + }, + "List": { + "heading_placeholder": "Solutions for Your Growth", + "long-description_placeholder": "We offer tailored solutions designed to meet your unique needs, from initial consultation to ongoing support. Our team of experts is dedicated to delivering high-quality results and ensuring your satisfaction.", + "description_placeholder": "Here's a glimpse into the comprehensive services we provide to help you succeed. ", + "kicker_placeholder": "Our Offerings", + "btn_placeholder": "Learn More", + "group-title-1_placeholder": "Core Services", + "group-title-2_placeholder": "Specialized Solutions", + "title-1_placeholder": "Strategic Planning", + "item-1_placeholder": "Develop a clear roadmap for your business with our expert strategic planning services.", + "title-2_placeholder": "Market Research", + "item-2_placeholder": "Gain valuable insights into your target audience and industry trends through our in-depth market research.", + "title-3_placeholder": "Brand Development", + "item-3_placeholder": "Build a strong and recognizable brand identity that resonates with your customers.", + "title-4_placeholder": "Digital Marketing", + "item-4_placeholder": "Expand your online reach and engage with your audience through our comprehensive digital marketing strategies.", + "title-5_placeholder": "Financial Consulting", + "item-5_placeholder": "Optimize your financial performance and make informed decisions with our expert financial guidance.", + "title-6_placeholder": "Operational Efficiency", + "item-6_placeholder": "Streamline your processes and boost productivity with our operational efficiency consulting." + }, + "Logo Farm": { + "heading_placeholder": "Trusted by Industry Leaders", + "description_placeholder": "We're proud to collaborate with a diverse range of companies that trust us to deliver exceptional results. Our partnerships are built on mutual success and shared innovation, driving progress across various sectors.", + "kicker_placeholder": "Our Valued Partners", + "btn_placeholder": "Learn More" + }, + "Maps": { + "heading_placeholder": "Find us", + "description_placeholder": "Our main office is conveniently located and easily accessible for all your inquiries.", + "location-1_placeholder": "Main Office", + "location-2_placeholder": "Downtown Location", + "location-3_placeholder": "Headquarters Annex", + "location-4_placeholder": "Northside Hub" + }, + "Media and Text": { + "long-subheading_placeholder": "Revolutionizing how we interact with the world.", + "heading_placeholder": "Future of Connectivity", + "long-description_placeholder": "Discover seamless integration and enhanced experiences designed to elevate your daily life. Our innovations transcend traditional boundaries, creating a more interconnected and intuitive world for everyone.", + "description_placeholder": "Explore groundbreaking advancements that are shaping tomorrow's digital landscape.", + "kicker_placeholder": "Tech Spotlight", + "btn_placeholder": "Learn More", + "title-1_placeholder": "Seamless Integration", + "text-1_placeholder": "Experience unparalleled ease as our technology effortlessly blends into your existing systems, enhancing productivity without disruption.", + "title-2_placeholder": "Intuitive Design", + "text-2_placeholder": "Crafted with the user in mind, our interfaces are simple, elegant, and exceptionally easy to navigate, ensuring a smooth experience for everyone.", + "title-3_placeholder": "Robust Security", + "text-3_placeholder": "Your data's safety is our top priority. Benefit from multi-layered security protocols and advanced encryption that keep your information protected around the clock.", + "title-4_placeholder": "Scalable Solutions", + "text-4_placeholder": "Whether you're a small startup or a large enterprise, our flexible solutions are designed to grow with your needs, adapting to future demands seamlessly.", + "title-5_placeholder": "Global Reach", + "text-5_placeholder": "Connect with confidence. Our services extend worldwide, providing reliable and high-performance access no matter where you are.", + "title-6_placeholder": "Dedicated Support", + "text-6_placeholder": "Our expert team is always ready to assist you. Enjoy comprehensive support and resources designed to ensure your success." + }, + "Post Loop": { + "heading_placeholder": "Our Latest Insights", + "post-btn_placeholder": "Read more", + "btn_placeholder": "View all", + "tag_placeholder": "Editor's Picks", + "posts_placeholder": [ + { + "title_placeholder": "Future of AI in Everyday Life", + "text_placeholder": "Explore how AI is transforming daily routines and what's next." + }, + { + "title_placeholder": "Sustainable Living", + "text_placeholder": "Discover easy ways to adopt eco-friendly habits." + }, + { + "title_placeholder": "Mastering Remote Work: Tips for Productivity", + "text_placeholder": "Boost your efficiency with these essential remote work strategies." + }, + { + "title_placeholder": "Rise of Wearable Technology", + "text_placeholder": "Learn about the latest advancements in wearable devices." + }, + { + "title_placeholder": "Cybersecurity Essentials", + "text_placeholder": "Protect your digital life with these crucial security tips." + }, + { + "title_placeholder": "Mindful Photography", + "text_placeholder": "Enhance your photography skills through a mindful approach." + }, + { + "title_placeholder": "Storytelling in Brand Building", + "text_placeholder": "Learn how compelling narratives can elevate your brand." + } + ] + }, + "Pricing Table": { + "heading_placeholder": "Our Pricing Plans", + "description_placeholder": "Choose the perfect plan to fit your needs, from individuals to large organizations. All plans include dedicated support and a commitment to your success.", + "btn-1_placeholder": "Get Started", + "btn-2_placeholder": "Buy Now", + "tag_placeholder": "Best Value", + "title-1_placeholder": "Flexible Scaling", + "text-1_placeholder": "Easily upgrade or downgrade as your needs evolve.", + "title-2_placeholder": "24/7 Customer Support", + "text-2_placeholder": "Our team is always ready to assist you.", + "title-3_placeholder": "Secure and Reliable Platform", + "text-3_placeholder": "We prioritize your data's security and stability.", + "title-4_placeholder": "Regular Feature Updates", + "text-4_placeholder": "Benefit from continuous improvements and new functionalities.", + "plan-1_placeholder": "Free", + "plan-description-1_placeholder": "Perfect for individuals.", + "plan-1-item-1_placeholder": "Limited Storage (e.g., 500MB)", + "plan-1-item-2_placeholder": "Basic Analytics", + "plan-1-item-3_placeholder": "Email Support", + "plan-1-item-4_placeholder": "1 User Account", + "plan-2_placeholder": "Basic", + "plan-description-2_placeholder": "Ideal for small teams.", + "plan-2-item-1_placeholder": "2GB Storage", + "plan-2-item-2_placeholder": "Standard Analytics", + "plan-2-item-3_placeholder": "Priority Email Support", + "plan-2-item-4_placeholder": "Up to 3 User Accounts", + "plan-3_placeholder": "Pro", + "plan-description-3_placeholder": "Great for growing businesses.", + "plan-3-item-1_placeholder": "10GB Storage", + "plan-3-item-2_placeholder": "Advanced Analytics", + "plan-3-item-3_placeholder": "Chat & Phone Support", + "plan-3-item-4_placeholder": "Up to 10 User Accounts", + "plan-4_placeholder": "Premium", + "plan-description-4_placeholder": "Designed for established businesses.", + "plan-4-item-1_placeholder": "Unlimited Storage", + "plan-4-item-2_placeholder": "Real-time Advanced Analytics", + "plan-4-item-3_placeholder": "Dedicated Account Manager", + "plan-4-item-4_placeholder": "Unlimited User Accounts", + "plan-5_placeholder": "Enterprise", + "plan-description-5_placeholder": "Tailored for large organizations with unique demands.", + "plan-5-details_placeholder": "For customized solutions including specialized features, dedicated infrastructure, and personalized support, please contact us to discuss an Enterprise plan." + }, + "Quote": { + "text-1_placeholder": "The journey of a thousand miles begins with a single step.", + "name-1_placeholder": "A. B. Johnson", + "position-1_placeholder": "Thought Leader", + "text-2_placeholder": "Innovation distinguishes between a leader and a follower.", + "name-2_placeholder": "C. D. Williams", + "position-2_placeholder": "Forward Thinker" + }, + "Service Menu": { + "heading_placeholder": "Our Signature Menu", + "description_placeholder": "Crafted with fresh ingredients and bold flavors—perfect for any time of day.", + "category-1_placeholder": "Customer Favorites", + "category-2_placeholder": "Brunch & Bowls", + "category-3_placeholder": "Add-ons & Sides", + "title-1_placeholder": "Grilled Herb Chicken", + "text-1_placeholder": "Tender chicken breast marinated in herbs, served with charred vegetables.", + "title-2_placeholder": "Smoked Salmon Bowl", + "text-2_placeholder": "Cured salmon, quinoa, avocado, and lemon-dill dressing in one refreshing bowl.", + "title-3_placeholder": "Creamy Mushroom Pasta", + "text-3_placeholder": "Fresh pasta tossed in a garlic parmesan cream with sautéed mushrooms.", + "title-4_placeholder": "Avocado Toast Stack", + "text-4_placeholder": "Crispy sourdough topped with smashed avocado, egg, and house seasoning.", + "title-5_placeholder": "Spiced Beef Rice Bowl", + "text-5_placeholder": "Slow-braised beef, jasmine rice, and tangy slaw with a touch of heat.", + "title-6_placeholder": "Breakfast Burrito", + "text-6_placeholder": "Fluffy eggs, chorizo, potatoes, and cheese wrapped in a warm tortilla." + }, + "Stats": { + "heading_placeholder": "Trusted by Teams, Loved by Customers", + "description_placeholder": "Our impact speaks for itself—from satisfied clients to successful projects delivered across industries.", + "kicker_placeholder": "By the Numbers", + "btn_placeholder": "See Our Impact", + "stat-title-1_placeholder": "Active Users", + "stat-title-2_placeholder": "Installs", + "stat-title-3_placeholder": "Websites", + "stat-title-4_placeholder": "New Users" + }, + "Table of Contents": { + "heading_placeholder": "Table of Contents", + "subtitle-1_placeholder": "Setting Up Your Account", + "subtitle-2_placeholder": "Navigating the Interface", + "subtitle-3_placeholder": "Personalizing Your Profile", + "subtitle-4_placeholder": "Discovering New Tools", + "subtitle-5_placeholder": "Customizing Your Workspace", + "subtitle-6_placeholder": "Collaborating with Others", + "subtitle-7_placeholder": "Maximizing Productivity", + "subtitle-8_placeholder": "Saving Time with Shortcuts", + "title-1_placeholder": "Getting Started", + "title-2_placeholder": "Exploring Features", + "title-3_placeholder": "Tips and Tricks", + "title-4_placeholder": "Our Community", + "title-5_placeholder": "Frequently Asked Questions", + "title-6_placeholder": "Glossary of Terms" + }, + "Tabs": { + "tab-1_placeholder": "All", + "tab-2_placeholder": "News", + "tab-3_placeholder": "Trending", + "tab-4_placeholder": "For You", + "tab-5_placeholder": "Favorites", + "title-1_placeholder": "Our Vision for the Future", + "text-1_placeholder": "We envision a world where innovation drives progress, and technology empowers individuals. Our commitment lies in fostering solutions that are not only groundbreaking but also accessible and sustainable, contributing to a brighter tomorrow for everyone.", + "title-2_placeholder": "Unlocking Your Potential", + "text-2_placeholder": "Discover the tools and resources designed to help you achieve your goals. We believe in providing comprehensive support and personalized guidance, enabling you to navigate challenges and seize opportunities with confidence." + }, + "Team Members": { + "heading_placeholder": "Meet Our Team", + "description_placeholder": "We're a passionate group dedicated to innovation and excellence. Get to know the individuals who drive our success.", + "name-1_placeholder": "Eleanor Vance", + "position-1_placeholder": "CEO", + "text-1_placeholder": "leads our vision and strategy, guiding the company toward new horizons", + "name-2_placeholder": "Marcus Thorne", + "position-2_placeholder": "Chief Technology Officer", + "text-2_placeholder": "oversees all technological advancements and product development", + "name-3_placeholder": "Sophia Chen", + "position-3_placeholder": "Head of Marketing", + "text-3_placeholder": "crafts our brand story and connects us with our audience", + "name-4_placeholder": "David Miller", + "position-4_placeholder": "Lead Product Designer", + "text-4_placeholder": "responsible for the intuitive and user-friendly design of our products", + "name-5_placeholder": "Alexander Gray", + "position-5_placeholder": "Director of Operations", + "text-5_placeholder": "ensures our daily operations run smoothly and efficiently", + "name-6_placeholder": "Samuel Green", + "position-6_placeholder": "Senior Software Engineer", + "text-6_placeholder": " a key architect behind our robust software solutions" + }, + "Text": { + "heading_placeholder": "Innovating for a Sustainable Future", + "long-subtitle_placeholder": "Pioneering Global Solutions for a Greener Tomorrow's Challenges", + "long-description-1_placeholder": "We invest in cutting-edge research to create tangible solutions for environmental challenges, integrating sustainable practices across our operations. Our holistic approach aims for long-term benefits for the planet and its inhabitants. True innovation stems from understanding ecological principles and technological advancement.", + "long-description-2_placeholder": "By fostering collaborations globally, we build a robust framework for environmental stewardship. Our efforts empower communities to embrace sustainability and thrive in harmony with nature. This ensures solutions are groundbreaking, scalable, and adaptable.", + "description_placeholder": "We are dedicated to creating a sustainable future through innovative solutions and responsible practices.", + "kicker_placeholder": "Driving Global Change", + "btn_placeholder": "Learn More", + "title-1_placeholder": "Renewable Energy", + "text-1_placeholder": "Developing advanced solar, wind, and hydro technologies to reduce reliance on fossil fuels.", + "title-2_placeholder": "Circular Economy", + "text-2_placeholder": "Implementing waste reduction, resource recovery, and product lifecycle optimization strategies.", + "title-3_placeholder": "Sustainable Agriculture", + "text-3_placeholder": "Promoting eco-friendly farming methods and resilient food systems.", + "title-4_placeholder": "Biodiversity Conservation", + "text-4_placeholder": "Protecting habitats, supporting endangered species, and promoting ecological balance.", + "title-5_placeholder": "Smart Cities", + "text-5_placeholder": "Designing urban environments with green spaces and efficient infrastructure.", + "title-6_placeholder": "Environmental Education", + "text-6_placeholder": "Empowering communities with knowledge to adopt sustainable lifestyles." + }, + "Testimonials": { + "heading_placeholder": "What Our Clients Say", + "description_placeholder": "Here's a look at what people are saying about working with us. We're proud to have helped so many achieve their goals.", + "kicker_placeholder": "Testimonials", + "name-1_placeholder": "Emily Johnson", + "position-1_placeholder": "Marketing Manager", + "text-1_placeholder": "XYZ Company has been a game-changer. Our online engagement and conversions significantly increased, leading to a 30% rise in leads in three months!", + "name-2_placeholder": "David Smith", + "position-2_placeholder": "Small Business Owner", + "text-2_placeholder": "Struggling to grow online, XYZ Company provided the perfect solution. Their tailored approach made a huge difference; my sales have doubled.", + "name-3_placeholder": "Sophia Miller", + "position-3_placeholder": "E-commerce Specialist", + "text-3_placeholder": "XYZ Company transformed our platform. Their expertise boosted traffic and improved satisfaction, consistently giving us higher average order values.", + "name-4_placeholder": "William Brown", + "position-4_placeholder": "Tech Startup Founder", + "text-4_placeholder": "XYZ Company delivered exceptional value. They helped us develop a strong brand and launch our product effectively. We've gained valuable traction.", + "name-5_placeholder": "James Wilson", + "position-5_placeholder": "Non-profit Director", + "text-5_placeholder": "XYZ Company exceeded our expectations. Their dedication significantly increased our donations and volunteer sign-ups by improving our online presence.", + "name-6_placeholder": "Olivia Davis", + "position-6_placeholder": "Real Estate Broker", + "text-6_placeholder": "XYZ Company gave me the edge. Their innovative digital advertising brought consistent, qualified leads, directly leading to more property sales." + }, + "Timeline": { + "heading_placeholder": "Our Journey Through Time", + "kicker_placeholder": "A Legacy of Progress", + "description_placeholder": "Explore the key milestones that have shaped our story, from our humble beginnings to our latest achievements.", + "btn_placeholder": "Discover more", + "title-1_placeholder": "Founding Vision", + "text-1_placeholder": "Our journey began with a bold idea to revolutionize the industry.", + "title-2_placeholder": "First Product Launch", + "text-2_placeholder": "We introduced our inaugural product to critical acclaim.", + "title-3_placeholder": "Global Expansion", + "text-3_placeholder": "Our reach extended to new international markets.", + "title-4_placeholder": "Innovation Award Received", + "text-4_placeholder": "Recognized for our groundbreaking technological advancements.", + "title-5_placeholder": "Community Initiative Launched", + "text-5_placeholder": "A new program dedicated to giving back to society.", + "title-6_placeholder": "Future Forward Summit", + "text-6_placeholder": "Hosting an event to discuss the next era of innovation." + + }, + "Videos": { + "heading_placeholder": "Our Story", + "kicker_placeholder": "See Us in Action", + "description_placeholder" : "Dive deep into the heart of InnovateCo, exploring our origins, values, and the future we're building.", + "btn-1_placeholder": "Watch Later", + "btn-2_placeholder": "Find Related Videos", + "title-1_placeholder": "Our Story", + "title-2_placeholder": "Lamp Features", + "title-3_placeholder": "Behind the Scenes", + "title-4_placeholder": "Customer Success", + "title-5_placeholder": "Expert Interview", + "title-6_placeholder": "Sustainability Efforts", + "title-7_placeholder": "Future Innovations" + } +} \ No newline at end of file diff --git a/src/components/design-library-list/design-library-list-item.js b/src/components/design-library-list/design-library-list-item.js index e079b9ce86..2fa4f179e3 100644 --- a/src/components/design-library-list/design-library-list-item.js +++ b/src/components/design-library-list/design-library-list-item.js @@ -2,71 +2,292 @@ * Internal dependencies. */ import ProControl from '../pro-control' +import DEFAULT from './default.json' +import { + addBackgroundScheme, addContainerScheme, + addPlaceholderForPostsBlock, cleanParse, + getAdditionalStylesForPreview, + parseDisabledBlocks, +} from './util' /** * External dependencies. */ -import { getDesign } from '~stackable/design-library' -import { isPro, i18n } from 'stackable' +import { createRoot } from '~stackable/util' +import { + isPro, i18n, wpGlobalStylesInlineCss, +} from 'stackable' import classnames from 'classnames' +import { Tooltip } from '~stackable/components' /** * WordPress dependencies. */ -import { useState } from '@wordpress/element' -import { Spinner } from '@wordpress/components' +import { + forwardRef, useEffect, useRef, useState, +} from '@wordpress/element' +import { select, useSelect } from '@wordpress/data' +import { Dashicon } from '@wordpress/components' import { __ } from '@wordpress/i18n' +import { serialize } from '@wordpress/blocks' +import { applyFilters } from '@wordpress/hooks' +import { cloneDeep } from 'lodash' -const DesignLibraryListItem = props => { +const DEFAULT_CONTENT = { ...DEFAULT } + +const DesignLibraryListItem = forwardRef( ( props, ref ) => { const { designId, - image, label, onClick, + template = '', + category = '', plan, - isPro, - apiVersion, - isMultiSelectMode = false, selectedNum = false, + selectedData = null, + containerScheme, + backgroundScheme, + enableBackground, + cardHeight, + setCardHeight, + previewSize, + setPreviewSize, } = props - const [ isBusy, setIsBusy ] = useState( false ) - // const [ isFavorite, setIsFavorite ] = useState( props.isFavorite ) + const [ parsedBlocks, setParsedBlocks ] = useState( null ) + const [ content, setContent ] = useState( '' ) + + const hostRef = useRef( null ) + const previewRef = useRef( null ) + const blocksForSubstitutionRef = useRef( false ) + const hasBackgroundTargetRef = useRef( false ) + const initialRenderRef = useRef( null ) + + const { getEditorDom } = useSelect( 'stackable/editor-dom' ) + const editorDom = getEditorDom() + + const siteTitle = useSelect( select => select( 'core' ).getEntityRecord( 'root', 'site' )?.title || 'InnovateCo', [] ) const mainClasses = classnames( [ 'ugb-design-library-item', + 'ugb-design-library-item--toggle', ], { - 'ugb--is-busy': isBusy, [ `ugb--is-${ plan }` ]: ! isPro && plan !== 'free', - 'ugb-design-library-item--toggle': isMultiSelectMode, - 'ugb--is-toggled': isMultiSelectMode && selectedNum, + 'ugb--is-toggled': selectedNum, } ) + const STYLE_IDS = applyFilters( 'stackable.global-styles.ids', [ + 'ugb-dep-native-global-style-css-nodep-inline-css', + 'ugb-style-css-css', + 'ugb-style-css-responsive-css', + 'ugb-block-style-inheritance-nodep-inline-css', + 'ugb-style-css-premium-css', + ] ) + + const adjustScale = () => { + if ( ref.current && hostRef.current?.shadowRoot && ! selectedNum ) { + const newPreviewSize = { ...previewSize } + const newCardHeight = { ...cardHeight } + const cardRect = ref.current.getBoundingClientRect() + + const shadowBody = hostRef.current.shadowRoot.querySelector( 'body' ) + if ( shadowBody ) { + const cardWidth = cardRect.width // Get width of the card + const scaleFactor = cardWidth > 0 ? cardWidth / shadowBody.offsetWidth : 1 // Divide by 1200, which is the width of preview in the shadow DOM + newPreviewSize.scale = scaleFactor + + const _height = parseFloat( shadowBody.offsetHeight ) * scaleFactor // Also adjust the height + + if ( Object.keys( newPreviewSize ).length === 1 ) { + newPreviewSize.heightBackground = _height + newPreviewSize.heightNoBackground = _height + } else { + const heightKey = enableBackground ? 'heightBackground' : 'heightNoBackground' + newPreviewSize[ heightKey ] = _height + } + + setPreviewSize( newPreviewSize ) + } + + if ( ! Object.keys( newCardHeight ).length ) { + newCardHeight.background = cardRect.height + newCardHeight.noBackground = cardRect.height + } else { + const CardHeightKey = enableBackground ? 'background' : 'noBackground' + newCardHeight[ CardHeightKey ] = cardRect.height + } + + setTimeout( () => setCardHeight( newCardHeight ), 500 ) + } + } + + const renderPreview = ( blockContent = content ) => { + let blocks = cloneDeep( selectedData?.designData || blockContent ) + + // No need to add the color scheme attribute if the selected scheme is the default + if ( containerScheme !== '' && ! selectedNum ) { + blocks = addContainerScheme( blocks, containerScheme ) + } + + // Only add a background scheme if it is enabled + if ( enableBackground && ! selectedNum ) { + blocks = addBackgroundScheme( blocks, enableBackground, backgroundScheme ) + } + + setParsedBlocks( blocks ) + + let preview = serialize( blocks ) + + // The block `wp/site-title` is a dynamic block, so we need to manually replace it for the preview + if ( category === 'Header' ) { + preview = preview.replace( //g, siteTitle ) + } else if ( category === 'Tabs' ) { + // Add a class for the first tab to be the active tab in the preview + preview = preview.replace( '"stk-block-tabs__tab"', '"stk-block-tabs__tab stk-block-tabs__tab--active"' ) + } else if ( category === 'Post Loop' ) { + const defaultValues = DEFAULT_CONTENT[ category ] + preview = addPlaceholderForPostsBlock( preview, defaultValues[ 'posts_placeholder' ], defaultValues ) + } + + const cleanedBlock = preview.replace( //g, '' ) // removes comment + + previewRef.current.render( ) + } + + // Replace the placeholders with the default content + useEffect( () => { + const defaultValues = DEFAULT_CONTENT[ category ] + let _content = template + if ( defaultValues ) { + Object.keys( defaultValues ).forEach( key => { + _content = _content.replaceAll( key, defaultValues[ key ] ) + } ) + } + + if ( _content.includes( 'stk-design-library__bg-target="true"' ) ) { + hasBackgroundTargetRef.current = true + } + + // We need to parse the content because this is what we use to insert the blocks in the Editor + const _block = cleanParse( _content )[ 0 ] + const { block, blocksForSubstitution } = parseDisabledBlocks( _block ) + blocksForSubstitutionRef.current = blocksForSubstitution + + const shadowRoot = hostRef.current.shadowRoot || hostRef.current.attachShadow( { mode: 'open' } ) + + // Initialize the shadow DOM for the first time + if ( ! previewRef.current ) { + // Get all styles needed and make a copy for the shadow DOM + const styleNodes = STYLE_IDS.map( id => { + let style = null + let node = document?.head?.querySelector( `#${ id }` ) + if ( ! node && editorDom ) { + const editorBody = editorDom?.closest( 'body' ) + const editorHead = editorBody?.ownerDocument?.head + node = editorHead.querySelector( `#${ id }` ) + } + + if ( node ) { + style = node.cloneNode( true ) + } + + return style + } ).filter( node => node !== null ) + + // Add global and theme styles + const globalStylesNode = document.createElement( 'style' ) + globalStylesNode.setAttribute( 'id', 'global-styles-inline-css' ) + globalStylesNode.innerHTML = wpGlobalStylesInlineCss + styleNodes.push( globalStylesNode ) + + const hostStyles = document.createElement( 'style' ) + hostStyles.setAttribute( 'id', 'stk-design-library-styles' ) + + const blockLayouts = select( 'stackable/global-spacing-and-borders' ).getBlockLayouts() + hostStyles.innerHTML = ! hasBackgroundTargetRef.current ? 'body > .stk-block-columns { padding: 75px; }' : '[stk-design-library__bg-target="true"] { padding: 50px; }' + + if ( ( Array.isArray( blockLayouts ) && ! blockLayouts.length ) || + ( typeof blockLayouts === 'object' && ! blockLayouts[ 'block-background-padding' ] ) + ) { + hostStyles.innerHTML += ! hasBackgroundTargetRef.current + ? ` body > .stk-block-background:not(.stk--no-padding) { padding: calc(75px + var(--stk-block-background-padding)); }` + : ` [stk-design-library__bg-target="true"].stk-block-background:not(.stk--no-padding) { padding: calc(26px + var(--stk-block-background-padding)); }` + } + + // Additional styles for blocks to render properly in the preview + hostStyles.innerHTML += getAdditionalStylesForPreview() + + styleNodes.push( hostStyles ) + + styleNodes.forEach( node => { + if ( node.textContent ) { + // we use :host in the shadow DOM to target the root + node.textContent = node.textContent.replace( /:root/g, ':host' ) + } + shadowRoot.appendChild( node ) + } ) + + previewRef.current = createRoot( shadowRoot ) + } + + renderPreview( block ) + setContent( block ) + }, [ template ] ) + + useEffect( () => { + if ( ! initialRenderRef.current ) { + initialRenderRef.current = true + return + } + + if ( ! content || + ! previewRef.current || + selectedNum + ) { + return + } + + renderPreview() + }, [ content, containerScheme, backgroundScheme, enableBackground ] ) + + useEffect( () => { + if ( selectedNum === 0 && content && previewRef.current ) { + renderPreview() + adjustScale() + } + }, [ selectedNum ] ) + + const getDesignPreviewSize = () => { + return selectedNum && selectedData ? selectedData.selectedPreviewSize.preview + : ( enableBackground ? previewSize.heightBackground : previewSize.heightNoBackground ) + } + return ( -
{ + if ( ! isPro && plan !== 'free' ) { + return + } + const cardRect = ref.current.getBoundingClientRect() + + const selectedPreviewSize = { + preview: enableBackground ? previewSize.heightBackground : previewSize.heightNoBackground, + card: cardRect.height, + scale: previewSize.scale, + } + + onClick( designId, category, parsedBlocks, blocksForSubstitutionRef.current, selectedPreviewSize ) + } } > - { isBusy && } { ! isPro && plan !== 'free' && } - +
+ +
+
+
-
- { label } - { /* */ } +
+
+

{ label }

+ { blocksForSubstitutionRef.current !== false && blocksForSubstitutionRef.current.size !== 0 && + + + + } +
+
+ { selectedNum !== 0 && + + + + } + { selectedNum === 0 ? '' : selectedNum } +
-
+ ) -} +} ) DesignLibraryListItem.defaultProps = { designId: '', @@ -91,10 +340,27 @@ DesignLibraryListItem.defaultProps = { label: '', onClick: () => {}, plan: 'free', - isPro, premiumLabel: __( 'Go Premium', i18n ), - apiVersion: '', - // isFavorite: false, } export default DesignLibraryListItem + +const DesignPreview = ( { + blocks, adjustScale, enableBackground, +} ) => { + useEffect( () => { + // Adjust scale if the background was toggled + adjustScale() + setTimeout( adjustScale, 50 ) + }, [ blocks, enableBackground ] ) + + const shadowBodyClasses = classnames( applyFilters( 'stackable.global-styles.classnames', [ 'entry-content' ] ) ) + + return ( + + ) +} diff --git a/src/components/design-library-list/editor.scss b/src/components/design-library-list/editor.scss index 28c05411dc..68fdb585e3 100644 --- a/src/components/design-library-list/editor.scss +++ b/src/components/design-library-list/editor.scss @@ -3,18 +3,9 @@ break-inside: avoid; gap: 40px; display: grid; - grid-template-columns: 1fr; + grid-template-columns: 1fr 1fr 1fr; align-items: center; font-size: 13px; - &.ugb-design-library-items--columns-2 { - grid-template-columns: 1fr 1fr; - } - &.ugb-design-library-items--columns-3 { - grid-template-columns: 1fr 1fr 1fr; - } - &.ugb-design-library-items--columns-4 { - grid-template-columns: 1fr 1fr 1fr 1fr; - } .components-base-control__help { text-align: center; @@ -37,7 +28,12 @@ .ugb-design-library-item { display: grid; grid-template-columns: 1fr; - grid-template-rows: auto 40px; + // grid-template-rows: auto 40px; + padding: 0; + border: 0 ; + text-align: start; + cursor: pointer; + background: #fff; position: relative; transition: all 0.3s cubic-bezier(0.2, 0.6, 0.4, 1); overflow: hidden; @@ -46,20 +42,11 @@ &:hover { box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px; } - img { - display: block; - pointer-events: none; // Prevent dragging the image. - width: 100%; - height: auto; - min-height: 50px; - background: #fff; - } footer { - padding-inline: 20px; line-height: 18px; - display: flex; - align-items: center; - justify-content: space-between; + // display: flex; + // align-items: center; + // justify-content: space-between; } .ugb-design-library-item__spinner { position: absolute; @@ -95,42 +82,81 @@ background: #fff; opacity: 0; transition: all 0.3s cubic-bezier(0.2, 0.6, 0.4, 1); - padding: 0 24px; // This is for v2 blocks where the design library can be seen in the inspector. + padding: 0 16px; // This is for v2 blocks where the design library can be seen in the inspector. + display: grid; + grid-template-columns: 1fr 1fr; + align-content: center; + + h4 { + font-size: 12px !important; + } + + .ugb-design-control-pro-note__description { + grid-row: 1/3; + grid-column: 2/3; + margin-bottom: 0 !important; + font-size: 11px !important; + } + a.button { + font-size: 11px !important; + padding: 8px; + + svg { + height: 12px; + width: 12px; + } + } } &[class*="ugb--is-premium"]:hover { .ugb-design-control-pro-note { opacity: 1; + z-index: 2; + } + .stk-block-design__host { + opacity: 0; } } // Toggle mode - &.ugb-design-library-item--toggle { + &.ugb-design-library-item--toggle footer { position: relative; - &::after { - content: ""; - position: absolute; - top: 16px; - left: 16px; + padding: 16px 16px 24px; + display: flex; + justify-content: space-between; + > div { + display: flex; + gap: 8px; + align-items: center; + } + h4 { + margin: 0; + } + .stk-block-design__selected-num { + display: inline-block; height: 24px; width: 24px; background: #fff; border: 1px solid #777; + border-radius: 24px; } + .dashicon { + color: #bbb; + } + } + &.ugb--is-premium footer .stk-block-design__selected-num { + border: 1px solid #ddd; + } - &.ugb--is-toggled { - &::after { - content: attr(data-selected-num); - height: 24px; - width: 24px; - background: #f00069; - color: #fff; - font-size: 14px; - display: flex; - align-items: center; - justify-content: center; - border: none; - font-weight: bold; - } + &.ugb--is-toggled footer { + .stk-block-design__selected-num { + background: #f00069; + color: #fff; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + border: none; + font-weight: bold; } } @@ -141,16 +167,12 @@ z-index: 9; } } -.ugb-design-library-item__image { - padding: 0 !important; - border: 0 !important; - text-align: start !important; - width: 100% !important; - margin: 0 !important; - outline: none !important; - cursor: pointer; - display: block; -} +// .ugb-design-library-item__image { +// width: 100% !important; +// margin: 0 !important; +// outline: none !important; +// display: block; +// } .ugb-design-library-item__favorite { border: 0; @@ -184,3 +206,31 @@ // } // } } +// new design library styles +.stk-block-design__host { + min-width: 1200px; + position: absolute; + top: 0; + left: 0; + opacity: 1; + transition: all 0.3s cubic-bezier(0.2, 0.6, 0.4, 1); +} + +.stk-block-design__host-container { + position: relative; +} + +:host body { + min-height: 500px; +} + +.stk--design-preview-large { + .ugb-design-control-pro-note { + grid-template-columns: 1fr; + + .ugb-design-control-pro-note__description { + grid-row: 2/3; + grid-column: 1/2; + } + } +} \ No newline at end of file diff --git a/src/components/design-library-list/index.js b/src/components/design-library-list/index.js index a52b3d4fc6..883a02ac00 100644 --- a/src/components/design-library-list/index.js +++ b/src/components/design-library-list/index.js @@ -14,67 +14,116 @@ import classnames from 'classnames' */ import { Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' +import { + useState, useEffect, useRef, +} from '@wordpress/element' const DesignLibraryList = props => { const { className = '', designs, isBusy, - onSelect, onSelectMulti, - apiVersion, - isMultiSelectMode = false, selectedDesigns = [], + selectedDesignData = [], } = props + const containerRef = useRef( null ) + + const [ scrollTop, setScrollTop ] = useState( 0 ) const listClasses = classnames( [ 'ugb-design-library-items', className, - ], { - [ `ugb-design-library-items--columns-${ props.columns }` ]: ! isBusy && props.columns, - } ) - - return
- { ( designs || [] ).map( ( design, i ) => { - const selectedNum = isMultiSelectMode ? selectedDesigns.indexOf( design.id ) + 1 : false - return ( - { - if ( ! isMultiSelectMode ) { - onSelect( designData, design, callback ) - } else if ( onSelectMulti ) { - onSelectMulti( designData, callback ) - } - } } - /> - ) - } ) } - - { isBusy &&
} - - { ! isBusy && ! ( designs || [] ).length && -

{ __( 'No designs found', i18n ) }

- } + ] ) + + useEffect( () => { + containerRef.current.scrollTop = 0 + }, [ designs ] ) + + return
{ + setScrollTop( e.currentTarget.scrollTop ) + } } + > + { isBusy && } + { ! isBusy && <> +
+ { ( designs || [] ).map( ( design, i ) => { + const selectedNum = selectedDesigns.indexOf( design.designId ) + 1 + const selectedData = selectedNum ? selectedDesignData[ selectedNum - 1 ] : null + return ( + + ) + } ) } + + { ! ( designs || [] ).length && +

{ __( 'No designs found', i18n ) }

+ } +
+ }
} DesignLibraryList.defaultProps = { designs: [], - columns: 1, + columns: 3, onSelect: () => {}, isBusy: false, - apiVersion: '', } export default DesignLibraryList + +const DesignLibraryItem = props => { + const { scrollTop, ...propsToPass } = props + const itemRef = useRef( null ) + const [ cardHeight, setCardHeight ] = useState( {} ) + const [ previewSize, setPreviewSize ] = useState( {} ) + const [ shouldRender, setShouldRender ] = useState( props.testKey < 9 ) + + useEffect( () => { + if ( ! itemRef.current ) { + return + } + + const containerRect = document.querySelector( '.ugb-modal-design-library__designs' ).getBoundingClientRect() + const itemRect = itemRef.current.getBoundingClientRect() + + const render = ( itemRect.top > containerRect.top - 250 && itemRect.top < containerRect.bottom + 250 ) || + ( itemRect.bottom > containerRect.top - 250 && itemRect.bottom < containerRect.bottom + 250 ) + + setShouldRender( render ) + }, [ scrollTop, props.enableBackground ] ) + + const getCardHeight = () => { + const key = props.enableBackground ? 'background' : 'noBackground' + return cardHeight?.[ key ] || 250 + } + + if ( ! shouldRender && ! props.selectedNum ) { + return
+ } + + return setPreviewSize( previewSize ) } + setCardHeight={ height => setCardHeight( height ) } + { ...propsToPass } + /> +} diff --git a/src/components/design-library-list/util.js b/src/components/design-library-list/util.js new file mode 100644 index 0000000000..3f725d8528 --- /dev/null +++ b/src/components/design-library-list/util.js @@ -0,0 +1,297 @@ +/* eslint-disable no-console */ +import DEFAULT from './default.json' +import { settings, isPro } from 'stackable' +import { parse, serialize } from '@wordpress/blocks' + +const DEFAULT_CONTENT = { ...DEFAULT } +const PARSER = new DOMParser() + +export const cleanParse = content => { + const originalConsoleError = console.error + const originalConsoleWarn = console.warn + + console.error = ( ...args ) => { + if ( args.length && typeof args[ 0 ] === 'string' && + args[ 0 ].includes( 'Block validation failed' ) + ) { + return + } + originalConsoleError( ...args ) + } + + console.warn = ( ...args ) => { + if ( args.length && typeof args[ 0 ] === 'string' && + args[ 0 ].includes( 'Block validation' ) + ) { + return + } + originalConsoleWarn( ...args ) + } + + let result + + try { + result = parse( content ) + } finally { + console.error = originalConsoleError + console.warn = originalConsoleWarn + } + + return result +} + +const replaceInnerContent = ( originalContent, newInnerHTML ) => { + const openTag = '', secondLastClose ) + + if ( openIndex === -1 || secondLastClose === -1 || closeEnd === -1 ) { + return originalContent + } + + const before = originalContent.slice( 0, openIndex ) + const after = originalContent.slice( closeEnd + 3 ) + + return `${ before }${ newInnerHTML }${ after }` +} + +export const addContainerScheme = ( + block, + containerScheme +) => { + const addScheme = blocks => { + return blocks.map( block => { + if ( block.innerBlocks?.length ) { + block.innerBlocks = addScheme( block.innerBlocks ) + } + + if ( block.attributes.hasContainer ) { + block.attributes.containerColorScheme = containerScheme + } + + if ( block.name === 'core/missing' && block.attributes.originalName ) { + const { originalAttributes } = block.attributes + + let innerHTML = '' + if ( block.innerBlocks.length ) { + innerHTML = serialize( block.innerBlocks ) + } else if ( originalAttributes.text ) { + innerHTML = originalAttributes.text + } + + const updatedContent = replaceInnerContent( block.attributes.originalContent, innerHTML ) + + block.attributes.originalContent = updatedContent + } + + return block + } ) + } + + const newBlock = addScheme( [ block ] )[ 0 ] + + return newBlock +} + +export const addBackgroundScheme = ( + block, + enableBackground, + backgroundScheme, +) => { + const addBackground = block => { + block.attributes.hasBackground = true + if ( backgroundScheme !== '' ) { + block.attributes.backgroundColorScheme = backgroundScheme + } + + return block + } + + const getTargetBlock = blocks => { + blocks.forEach( _block => { + const customAttributes = _block.attributes.customAttributes + + const isTarget = customAttributes?.find( attribute => attribute[ 0 ] === 'stk-design-library__bg-target' && attribute[ 1 ] === 'true' ) + if ( isTarget ) { + _block = addBackground( _block ) + } else if ( _block.innerBlocks.length ) { + getTargetBlock( _block.innerBlocks ) + } + } ) + } + + if ( ! enableBackground ) { + return block + } + + const customAttributes = block.attributes.customAttributes + + if ( ! customAttributes?.length ) { + block = addBackground( block ) + return block + } + + getTargetBlock( block.innerBlocks ) + + return block +} + +export const replacePlaceholders = ( block, category ) => { + const key = block.attributes.text + const defaultValues = DEFAULT_CONTENT[ category ] + if ( key && defaultValues ) { + const newValue = defaultValues[ key ] + block.attributes.text = newValue + } + + block.innerBlocks.forEach( innerBlock => { + replacePlaceholders( innerBlock, category ) + } ) + + return block +} + +const retrieveAttributes = ( blockName, hasNoInnerBlocks, content ) => { + const attrs = content.match( /{.*}/ ) + let parsedAttrs = {} + if ( attrs ) { + try { + parsedAttrs = JSON.parse( attrs[ 0 ] ) + if ( hasNoInnerBlocks ) { + content = content.replace( /]*>[\s\S]*?<\/style>/gi, '' ) + const doc = PARSER.parseFromString( content, 'text/html' ) + parsedAttrs.text = doc.body.textContent.trim() + } + } catch ( err ) {} + } + return parsedAttrs +} + +export const parseDisabledBlocks = parsedBlock => { + const disabledBlocks = settings.stackable_block_states || [] + const blocksForSubstitution = new Set() + + if ( Array.isArray( disabledBlocks ) && ! disabledBlocks.length ) { + return { block: parsedBlock, blocksForSubstitution } + } + + const addOriginalAttributes = blocks => { + return blocks.map( block => { + if ( block.name === 'core/missing' ) { + blocksForSubstitution.add( block.attributes.originalName ) + const newAttrs = retrieveAttributes( block.attributes.originalName, block.innerBlocks.length === 0, block.attributes.originalContent ) + const attrs = block.attributes + block.attributes = { + ...attrs, ...newAttrs, originalAttributes: newAttrs, + } + } + + block.innerBlocks = addOriginalAttributes( block.innerBlocks ) + return block + } ) + } + + const block = addOriginalAttributes( [ parsedBlock ] )[ 0 ] + return { block, blocksForSubstitution } +} + +const IMAGE_STORAGE = 'https://storage.googleapis.com/stackable-plugin-assets/library-v4/images/' + +export const addPlaceholderForPostsBlock = ( content, postsPlaceholder, defaultValues ) => { + const remainingPosts = [ ...postsPlaceholder ] + + // Normalize special characters + const normalized = content + .replace( /</g, '<' ) + .replace( />/g, '>' ) + .replace( /–/g, '-' ) + .replace( /\u2013|\u2014/g, '-' ) + + // Regex to match all stackable/posts blocks + const postBlockRegex = /([\s\S]*?)/g + + return normalized.replace( postBlockRegex, ( match, jsonStr, innerHtml ) => { + let attrs + try { + attrs = JSON.parse( jsonStr ) + } catch { + return match // Skip if JSON is invalid + } + + const numItems = attrs.numberOfItems ?? 6 + const width = attrs.imageWidth ? attrs.imageWidth + ( attrs.imageWidthUnit ?? 'px' ) : 'auto' + + // Get the post template inside the block + const templateMatch = innerHtml.match( /([\s\S]*?)/ ) + if ( ! templateMatch ) { + return match // Skip if template is missing + } + + const template = templateMatch[ 1 ].trim() + const currentPosts = remainingPosts.splice( 0, numItems ) // Slice the posts for this block + + const renderedPosts = currentPosts.map( ( post, index ) => + template + .replace( /!#title!#/g, post.title_placeholder ) + .replace( /!#excerpt!#/g, post.text_placeholder ) + .replace( /!#date!#/g, 'March 1, 2025' ) + .replace( /!#readmoreText!#/g, defaultValues[ 'post-btn_placeholder' ] ) + .replace( /!#category!#/g, defaultValues.tag_placeholder ) + .replace( /img class="stk-img"/g, `img class="stk-img" src="${ IMAGE_STORAGE }stk-design-library-image-${ index + 1 }.jpeg" width="${ width }" style="width: ${ width } !important;"` ) + ).join( '\n' ) + + // Replace just the template portion, keep rest of the block + const updatedInnerHtml = innerHtml.replace( + /([\s\S]*?)/, + renderedPosts + ) + + return `${ updatedInnerHtml }` + } ) +} + +// Additional styles for blocks to render properly in the preview +export const getAdditionalStylesForPreview = () => { + let styles = '' + + // Make sure count up block numbers are visible + styles += `.stk-block-count-up__text:not(.stk--count-up-active) { opacity: 1; }` + + // Fill the vertical line in timeline blocks + styles += `.stk-block-timeline { --line-bg-color: var(--line-accent-bg-color, #000); }` + + // Display correctly the progress in progress bar and progress circle blocks + styles += `.stk-progress-bar .stk-progress-bar__bar { width: var(--progress-percent, 0px); }` + styles += `.stk-progress-circle .stk-progress-circle__bar { stroke-dashoffset: var(--progress-dash-offset); }` + + // Display correctly the styles for posts block. + // Do this only if in Free version, since we will be able to load the correct CSS for Premium. + if ( ! isPro ) { + styles += `.stk-block-posts.is-style-horizontal-2 { + .stk-block-posts__item > .stk-container { + padding: 0; + display: flex; + flex-direction: row; + + > .stk-block-posts__image-link:empty ~ .stk-container-padding, + .stk-container-padding:only-child { + width: 100%; + } + } + .stk-img-wrapper { + height: 100%; + margin-top: 0; + margin-bottom: 0; + } + .stk-block-posts__image-link { + margin-bottom: 0; + } +}` + } + + return styles +} diff --git a/src/components/modal-design-library/block-list.js b/src/components/modal-design-library/block-list.js index acd679dd30..d4ba353e3a 100644 --- a/src/components/modal-design-library/block-list.js +++ b/src/components/modal-design-library/block-list.js @@ -10,215 +10,75 @@ * External dependencies */ import { sortBy } from 'lodash' -import { fetchDesignLibrary } from '~stackable/design-library' -import { isPro, i18n } from 'stackable' +import { i18n } from 'stackable' import classnames from 'classnames' /** * WordPress dependencies */ -import { - useEffect, useState, -} from '@wordpress/element' +import { useEffect, useState } from '@wordpress/element' import { __ } from '@wordpress/i18n' -import { useLocalStorage } from '~stackable/util' - -const isWireframe = design => design.uikit.toLowerCase() === 'wireframes' +import { Spinner } from '@wordpress/components' const BlockList = props => { - const [ uiKitList, setUiKitList ] = useState( [] ) const [ categoryList, setCategoryList ] = useState( [] ) - const [ wireframeList, setWireframeList ] = useState( [] ) - const [ selected, setSelected ] = useLocalStorage( 'stk__design_library__block-list__selected', '' ) - const { viewBy, apiVersion } = props + const { + viewBy, designs, plan, selected, onSelect, + } = props - // Create our block list. useEffect( () => { - fetchDesignLibrary( apiVersion ).then( designs => { - const designList = Object.keys( designs ).reduce( ( output, name ) => { - const design = designs[ name ] - const { categories, uikit } = design - - // Get all UI Kits. Don't include the Wireframe UI Kit. - if ( typeof output.uikits[ uikit ] === 'undefined' && ! isWireframe( design ) ) { - output.uikits[ uikit ] = { - id: uikit, - label: design.uikit, - plan: design.plan, - count: 0, - } - } - - // Get all wireframe categories. - if ( isWireframe( design ) ) { - categories.forEach( category => { - if ( typeof output.wireframes[ category ] === 'undefined' ) { - output.wireframes[ category ] = { - id: category, - label: category, - count: 0, - } - } - } ) - } else { - // Get all block design categories. - categories.forEach( category => { - if ( typeof output.categories[ category ] === 'undefined' ) { - output.categories[ category ] = { - id: category, - label: category, - count: 0, - } - } - } ) - } + let total = 0 + const _categories = designs.reduce( ( output, design ) => { + const category = design.category + if ( plan && plan !== design.plan ) { return output - }, { - uikits: {}, - categories: {}, - wireframes: {}, - } ) - - let uikitSort = [ 'label' ] - if ( ! isPro ) { - uikitSort = [ 'plan', 'label' ] - } - - const uikits = sortBy( Object.values( designList.uikits ), uikitSort ) - const categories = sortBy( Object.values( designList.categories ), 'label' ) - categories.unshift( { - id: 'all', - label: __( 'All', i18n ), - count: 0, - } ) - const wireframes = sortBy( Object.values( designList.wireframes ), 'label' ) - wireframes.unshift( { - id: 'all', - label: __( 'All', i18n ), - count: 0, - } ) - - setUiKitList( uikits ) - setCategoryList( categories ) - setWireframeList( wireframes ) - } ) - }, [ apiVersion ] ) - - // Update the counts of the designs, but don't update the list, only the counts. - useEffect( () => { - // If these are empty, then our component hasn't finished initializing. - if ( ! uiKitList.length || ! categoryList.length || ! wireframeList.length ) { - return - } - - const newUiKits = uiKitList.reduce( ( uiKits, uiKit ) => { - uiKits[ uiKit.id ] = { - ...uiKit, - count: 0, - } - return uiKits - }, {} ) - const newCategories = categoryList.reduce( ( categories, category ) => { - categories[ category.id ] = { - ...category, - count: 0, - } - return categories - }, {} ) - const newWireframes = wireframeList.reduce( ( categories, category ) => { - categories[ category.id ] = { - ...category, - count: 0, } - return categories - }, {} ) - props.designs.forEach( design => { - // Gather all wireframe designs. - if ( isWireframe( design ) ) { - design.categories.forEach( category => { - if ( category && newWireframes[ category ] ) { - newWireframes[ category ].count++ - } - } ) - return - } - - // Gather all ui kit designs. - if ( design.uikit && newUiKits[ design.uikit ] ) { - newUiKits[ design.uikit ].count++ - } - - // Gather all block design categories. - design.categories.forEach( category => { - if ( category && newCategories[ category ] ) { - newCategories[ category ].count++ + if ( category in output ) { + output[ category ].count++ + } else { + output[ category ] = { + id: category, + label: category, + count: 1, } - } ) - } ) - - let uikitSort = [ 'label' ] - if ( ! isPro ) { - uikitSort = [ 'plan', 'label' ] - } - setUiKitList( sortBy( Object.values( newUiKits ), uikitSort ) ) - - // Sort the categories so that the "All" is first. - if ( newCategories.all ) { - newCategories.all.count = props.designs.filter( design => ! isWireframe( design ) ).length - newCategories.all.label = ' ' // Spaces so we will be first when sorting. - } - const sorted = sortBy( Object.values( newCategories ), 'label' ) - if ( sorted[ 0 ] ) { - sorted[ 0 ].label = __( 'All', i18n ) - } - setCategoryList( sorted ) + } - // Sort the wireframes so that the "All" is first. - if ( newWireframes.all ) { - newWireframes.all.count = props.designs.filter( isWireframe ).length - newWireframes.all.label = ' ' // Spaces so we will be first when sorting. - } - const sortedWireframes = sortBy( Object.values( newWireframes ), 'label' ) - if ( sortedWireframes[ 0 ] ) { - sortedWireframes[ 0 ].label = __( 'All', i18n ) - } - setWireframeList( sortedWireframes ) - }, [ props.designs.length, JSON.stringify( uiKitList ), JSON.stringify( categoryList ), JSON.stringify( wireframeList ) ] ) + total++ + return output + }, {} ) - useEffect( () => { - // If these are empty, then our component hasn't finished initializing. - if ( ! uiKitList.length || ! categoryList.length || ! wireframeList.length ) { - return + _categories.all = { + id: '', + label: ' ', + count: total, } - setSelected( viewBy === 'uikit' ? uiKitList[ 0 ].id : 'all' ) - }, [ viewBy ] ) + const sortedCategories = sortBy( Object.values( _categories ), 'label' ) + sortedCategories[ 0 ].label = __( 'All', i18n ) - useEffect( () => { - props.onSelect( selected ) - }, [ selected ] ) + setCategoryList( sortedCategories ) + }, [ designs, viewBy, plan ] ) - const list = viewBy === 'uikit' - ? uiKitList : viewBy === 'category' - ? categoryList : wireframeList + if ( props.isBusy ) { + return + } return (
    - { list.reduce( ( list, itemData ) => { + { categoryList.reduce( ( list, itemData ) => { const { id, label, count, - plan, } = itemData const classes = classnames( [ 'stk-design-library__sidebar-item', ], { 'is-active': selected === id, - 'is-disabled': ! isPro && plan === 'premium', + // 'is-disabled': ! isPro && plan === 'premium', } ) list.push( @@ -226,7 +86,7 @@ const BlockList = props => {
    setSelected( id ) } + onClick={ () => onSelect( id ) } onKeyPress={ e => { if ( e.keyCode === 13 ) { this.click() diff --git a/src/components/modal-design-library/color-list.js b/src/components/modal-design-library/color-list.js deleted file mode 100644 index 5994bb0536..0000000000 --- a/src/components/modal-design-library/color-list.js +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from '@wordpress/element' -import { BaseControl, ColorPalette } from '@wordpress/components' - -const COLORS = { - red: { - color: '#f44336', - name: 'red', - }, - green: { - color: '#4caf50', - name: 'green', - }, - yellow: { - color: '#ffeb3b', - name: 'yellow', - }, - blue: { - color: '#2196f3', - name: 'blue', - }, - pink: { - color: '#e91e63', - name: 'pink', - }, - gray: { - color: '#9e9e9e', - name: 'gray', - }, - brown: { - color: '#795548', - name: 'brown', - }, - orange: { - color: '#ff9800', - name: 'orange', - }, - purple: { - color: '#9c27b0', - name: 'purple', - }, - black: { - color: '#212121', - name: 'black', - }, - white: { - color: '#fff', - name: 'white', - }, -} - -const ColorList = props => { - const [ selectedColors, setSelectedColors ] = useState( [] ) - - return ( - -
    - { Object.keys( props.colors ).map( id => { - const { color, name } = COLORS[ id ] - return { - let newColors - if ( ! color ) { - newColors = selectedColors.filter( n => n !== name ) - } else { - newColors = [ ...selectedColors, name ] - } - setSelectedColors( newColors ) - props.onSelect( newColors ) - } } - clearable={ false } - disableCustomColors={ true } - /> - } ) } -
    -
    - ) -} - -ColorList.defaultProps = { - onSelect: () => {}, - colors: COLORS, -} - -export default ColorList diff --git a/src/components/modal-design-library/editor.scss b/src/components/modal-design-library/editor.scss index 78642d9578..d3d2abfe3d 100644 --- a/src/components/modal-design-library/editor.scss +++ b/src/components/modal-design-library/editor.scss @@ -2,6 +2,7 @@ width: 100%; max-width: 90%; height: 90%; + max-height: 90%; min-height: 500px; --wp-admin-theme-color: #f00069; @@ -26,20 +27,19 @@ margin: 0; height: 58px; border-bottom: 1px solid #ddd; - .stk-control-content { - padding-left: 14px; + + .components-modal__header-heading-container { + flex-grow: 0; + width: 300px; } - .components-button { - left: 0 !important; - &.has-icon { - margin: 10px; - } + > .components-button.has-icon { + margin: 10px; } } .ugb-modal-design-library__wrapper { display: grid; grid-template-columns: 300px auto; - grid-template-rows: 80px auto; + grid-template-rows: auto auto 80px; flex: 1; height: 100%; } @@ -50,6 +50,7 @@ grid-row: 1 / 3; display: flex; flex-direction: column; + justify-content: space-between; overflow: hidden; z-index: 1; .ugb-control-separator { @@ -57,21 +58,44 @@ width: 100% !important; } } - .ugb-modal-design-library__topbar { + .ugb-modal-design-library__style-options { display: flex; - justify-content: flex-end; - padding: 0 24px; - height: 64px; - align-items: center; - grid-column: 2 / 3; - grid-row: 1 / 2; + flex-direction: column; + padding: 24px; + gap: 12px; background: #fff; - border-bottom: 1px solid #eee; + border-top: 1px solid #eee; + + > div:first-child { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 1em; + h4 { + margin: 0; + } + svg { + fill: #bbb; + cursor: pointer; + } + } + + .components-base-control__field { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0; + + .components-base-control__label { + margin-bottom: 0; + text-transform: none; + } + } } .ugb-modal-design-library__designs { padding: 24px; grid-column: 2 / 3; - grid-row: 2 / 3; + grid-row: 1 / 3; } .ugb-modal-design-library__search { @@ -123,7 +147,7 @@ margin-left: auto; } .ugb-modal-design-library__footer { - padding: 0 24px; + padding: 24px; grid-column: 2 / 3; grid-row: 3 / 4; display: flex; @@ -134,9 +158,6 @@ } // Make room for the footer buttons. &.ugb-modal-design-library--is-multiselect { - .ugb-modal-design-library__wrapper { - grid-template-rows: 80px auto 80px; - } .ugb-modal-design-library__sidebar { grid-row: 1 / 4; } @@ -160,6 +181,16 @@ margin: 0 0 0 18px !important; } } + + ::-webkit-scrollbar { + width: 10px; + } + ::-webkit-scrollbar-thumb { + background: #e0e0e0; + border-radius: 8px; + border: 2px solid rgba(0, 0, 0, 0); + background-clip: padding-box; + } } .ugb-modal-design-library__filters .ugb-block-list { @@ -182,6 +213,7 @@ [aria-pressed="true"] { font-weight: bold; color: #f00069; + background: #f1f1f1; } .is-disabled { opacity: 0.3; @@ -242,10 +274,24 @@ } } -.ugb-modal-design-library__refresh { - margin-right: 10px; +.ugb-modal-design-library__style-options div.components-base-control { + margin-bottom: 0; } +div.ugb-modal-design-library__enable-background { + .components-flex { + justify-content: space-between; + } + .components-toggle-control__label { + font-size: 11px; + font-weight: 500; + line-height: 1.4; + order: 1; + } + .components-form-toggle { + order: 2; + } +} .ugb-modal-design-library__dev-mode .components-base-control__field { margin: 2px 24px 0 !important; } @@ -271,22 +317,166 @@ // Design library top tabs, switch between block categories, ui kit and wireframe. .stk-design-library-tabs { margin-bottom: 0 !important; + margin: 0 auto; + + .components-base-control__field, .stk-control-label { + margin: 0; + } + .components-button-group { + background: #e0e0e08a; + border-radius: 6px !important; + box-shadow: none !important; + + } .components-button { - height: 60px; + height: fit-content; border: none !important; - border-left: 1px solid #ddd !important; - box-shadow: none !important; - padding: 0 24px; + box-shadow: none; + padding: 4px 24px; + margin: 6px; + border-radius: 4px !important; background: transparent; &:hover, + &.is-primary:hover, &.is-primary { color: #f00069 !important; - background: rgba(229, 67, 118, 0.1) !important; + background: #fff; + box-shadow: 1px 1px 3px 0px #1e1e1e1e; } } } +.ugb-modal-design-library .stk-design-library-tabs { + padding-right: 0 !important; +} + // Fix WP 6.3 a div wraps the .ugb-modal-design-library__wrapper .ugb-modal-design-library .components-modal__content > div:not(.components-modal__header) { height: 100%; } + + +.ugb-modal-design-library__stk-color-scheme { + display: flex; + align-items: center; + gap: 8px; + outline: 1px solid #e0e0e0; + border-radius: 2px; + margin-bottom: 8px; + padding: 0; + height: auto; + width: 100%; + &:hover { + &.stk-color-scheme__toggle .stk-color-scheme__none, + .stk-global-color-scheme__preview { + color: initial; + } + } + &:last-child { + margin-bottom: 0; + } + + &.stk-color-scheme__selected { + &, &:active { + outline: 2px solid var(--wp-admin-theme-color, #f00069); + color: var(--wp-admin-theme-color, #f00069); + } + .stk-global-color-scheme__preview { + color: initial; + } + } + + .stk-color-scheme-name { + text-wrap: nowrap; + margin-inline-end: 8px; + } + + &.stk-color-scheme__toggle .stk-color-scheme__none { + position: absolute; + right: 0; + left: 0; + top: 50%; + transform: translate(0, -50%); + font-size: 12px; + margin-inline-end: 0; + } +} + +.stk-design-library__header-settings { + position: absolute; + right: 60px; + display: flex; + gap: 10px; + align-items: center; +} +.stk-design-library__plan-dropdown, .ugb-design-library__color-scheme-popover { + --wp-admin-theme-color: #f00069; + --wp-admin-theme-color-darker-10: #{ darken(#f00069, 10%) }; + --wp-admin-theme-color-darker-20: #{ darken(#f00069, 20%) }; +} + +.ugb-modal-design-library__stk-color-scheme-list { + padding: 8px; + .stk-color-scheme__none { + padding: 8px; + min-height: 30px; + margin: 0 auto; + min-width: 200px; + } +} +.components-dropdown__content:has(.ugb-modal-design-library__stk-color-scheme-list) { + ::-webkit-scrollbar { + width: 10px; + } + ::-webkit-scrollbar-thumb { + background: #e0e0e0; + border-radius: 8px; + border: 2px solid rgba(0, 0, 0, 0); + background-clip: padding-box; + } +} + +.stk-design-library__plan-dropdown .ugb-button-component { + width: 100%; +} + +.ugb-design-library__confirm-dialog { + max-width: 800px; + ul { + list-style: circle; + margin-inline-start: 20px; + } +} + +.ugb-design-library__manage-scheme { + display: block; + text-align: center; +} + +.ugb-modal-design-library__style-options-tooltip .stk-tooltip__text a { + display: inline; +} + +.ugb-design-library__color-scheme-popover .components-popover__content { + padding: 0; +} + +.ugb-modal-design-library__stk-color-scheme-list-header { + display: flex; + justify-content: space-between; + padding: 0; + margin: 0 0 4px; + border-bottom: 1px solid #e0e0e0; + + p { + margin: 6px 6px 4px; + font-size: 11px; + font-weight: 500; + } + + .components-button.has-icon { + padding: 0; + height: unset; + min-width: unset; + } +} diff --git a/src/components/modal-design-library/images/help.svg b/src/components/modal-design-library/images/help.svg new file mode 100644 index 0000000000..eb736bd38d --- /dev/null +++ b/src/components/modal-design-library/images/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/modal-design-library/modal.js b/src/components/modal-design-library/modal.js index d09346e5b4..2d56e3c028 100644 --- a/src/components/modal-design-library/modal.js +++ b/src/components/modal-design-library/modal.js @@ -1,21 +1,17 @@ /** * Internal deprendencies */ -import SVGViewSingle from './images/view-single.svg' -import SVGViewMany from './images/view-many.svg' -import SVGViewFew from './images/view-few.svg' +import HelpSVG from './images/help.svg' import BlockList from './block-list' import Button from '../button' -import AdvancedToolbarControl from '../advanced-toolbar-control' +// import AdvancedToolbarControl from '../advanced-toolbar-control' import DesignLibraryList from '~stackable/components/design-library-list' -import { - getDesign, getDesigns, setDevModeDesignLibrary, -} from '~stackable/design-library' +import { getDesigns, filterDesigns } from '~stackable/design-library' /** * External deprendencies */ -import { i18n, devMode } from 'stackable' +import { i18n, isPro } from 'stackable' import classnames from 'classnames' import { useLocalStorage } from '~stackable/util' @@ -23,261 +19,380 @@ import { useLocalStorage } from '~stackable/util' * WordPress deprendencies */ import { - Modal, Spinner, TextControl, ToggleControl, + BaseControl, + Dashicon, + Dropdown, + Modal, + Spinner, + ToggleControl, } from '@wordpress/components' import { useEffect, useState } from '@wordpress/element' import { sprintf, __ } from '@wordpress/i18n' +import { useBlockColorSchemes } from '~stackable/hooks' +import ColorSchemePreview from '../color-scheme-preview' +import { ColorSchemesHelp } from '../color-schemes-help' +import Tooltip from '../tooltip' + +const PLAN_OPTIONS = [ { key: '', label: __( 'All', i18n ) }, { key: 'free', label: __( 'Free', i18n ) }, { key: 'premium', label: __( 'Premium', i18n ) } ] +const popoverProps = { + className: 'ugb-design-library__color-scheme-popover', + placement: 'right-start', + shift: true, +} export const ModalDesignLibrary = props => { - const [ search, setSearch ] = useState( props.search ) - const [ columns, setColumns ] = useState( 3 ) + const { + backgroundModeColorScheme, containerModeColorScheme, colorSchemesCollection, + } = useBlockColorSchemes() const [ isBusy, setIsBusy ] = useState( true ) const [ doReset, setDoReset ] = useState( false ) - const [ isMultiSelectMode, setIsMultiSelectMode ] = useState( false ) + const [ selectedDesignIds, setSelectedDesignIds ] = useState( [] ) const [ selectedDesignData, setSelectedDesignData ] = useState( [] ) + const [ isMultiSelectBusy, setIsMultiSelectBusy ] = useState( false ) - const [ selectedId, setSelectedId ] = useState( '' ) - const [ selectedType, setSelectedType ] = useLocalStorage( 'stk__design_library__block-list__view_by', 'uikit' ) - const [ isDevMode, setIsDevMode ] = useLocalStorage( 'stk__design_library_dev_mode', false ) + + const [ selectedTab, setSelectedTab ] = useLocalStorage( 'stk__design_library__block-list__view_by', 'patterns' ) + const [ selectedCategory, setSelectedCategory ] = useLocalStorage( 'stk__design_library__block-list__selected', '' ) + const [ selectedPlan, setSelectedPlan ] = useLocalStorage( 'stk__design_library__view-plan', PLAN_OPTIONS[ 0 ] ) + // The sidebar designs are used to update the list of blocks in the sidebar. const [ sidebarDesigns, setSidebarDesigns ] = useState( [] ) // The display designs are used to list the available designs the user can choose. const [ displayDesigns, setDisplayDesigns ] = useState( [] ) - const [ searchDebounced, setSearchDebounced ] = useState( search ) - const [ debounceTimeout, setDebounceTimeout ] = useState( null ) + const [ enableBackground, setEnableBackground ] = useState( false ) + const [ selectedContainerScheme, setSelectedContainerScheme ] = useState( '' ) + const [ selectedBackgroundScheme, setSelectedBackgroundScheme ] = useState( '' ) + // For version 4, the default tab is now 'patterns' and for category, we use '' instead of 'All'. + // So we need to update the local storage values here. useEffect( () => { - if ( debounceTimeout ) { - clearTimeout( debounceTimeout ) - setDebounceTimeout( null ) - } - setDebounceTimeout( setTimeout( () => { - setSearchDebounced( search ) - }, 500 ) ) - }, [ search ] ) - - // Select the input field on open. - // Use this method since useRef isn't working. - useEffect( () => { - const input = document.querySelector( '.ugb-modal-design-library__search input' ) - if ( input ) { - input.focus() + const version = window.localStorage.getItem( 'stk__design_library__version' ) + if ( ! version ) { + window.localStorage.setItem( 'stk__design_library__version', 'v4' ) + setSelectedTab( 'patterns' ) + setSelectedCategory( '' ) } }, [] ) // Update the designs on the sidebar. (this will trigger the display designs update next) useEffect( () => { + setIsBusy( true ) if ( doReset ) { setSidebarDesigns( [] ) - setDisplayDesigns( [] ) + // setDisplayDesigns( [] ) } getDesigns( { - search: searchDebounced, reset: doReset, - apiVersion: props.apiVersion, + tab: selectedTab, } ).then( designs => { setSidebarDesigns( designs ) } ).finally( () => { setDoReset( false ) + setIsBusy( false ) } ) - }, [ searchDebounced, doReset, props.apiVersion ] ) + }, [ doReset, selectedTab ] ) // This updates the displayed designs the user can pick. useEffect( () => { - setIsBusy( true ) - getDesigns( { - apiVersion: props.apiVersion, - search: searchDebounced, - uikit: selectedType === 'wireframe' ? 'Wireframes' : ( selectedType === 'uikit' ? selectedId : '' ), - categories: [ 'category', 'wireframe' ].includes( selectedType ) && selectedId !== 'all' ? [ selectedId ] : [], + filterDesigns( { + library: sidebarDesigns, + category: selectedCategory, + plan: selectedPlan.key, } ).then( designs => { setDisplayDesigns( designs ) - } ).finally( () => { - setIsBusy( false ) } ) - }, [ selectedId, selectedType, doReset, searchDebounced, props.apiVersion ] ) + }, [ sidebarDesigns, selectedPlan, selectedCategory ] ) + + const colorSchemeHelpCallback = () => { + if ( selectedDesignIds.length ) { + // eslint-disable-next-line no-alert + const confirmClose = window.confirm( sprintf( __( 'You have one or more designs selected. Navigating to %s will close the Design Library and your current selection will be lost. Do you want to continue?', i18n ), __( 'Color Schemes', i18n ) ) ) + if ( ! confirmClose ) { + return true + } + } + props.onClose() + return false + } return ( - { __( 'Stackable Design Library', i18n ) } + { /* DEV NOTE: hide for now - { props.hasVersionSwitcher && ( - */ } + +
    + + ) } + renderContent={ ( { onClose } ) => ( +
    + { PLAN_OPTIONS.map( ( plan, i ) => { + return + } ) } +
    + ) } + /> } +
    ) } - className={ classnames( 'ugb-modal-design-library', { 'ugb-modal-design-library--is-multiselect': isMultiSelectMode } ) } + className={ classnames( 'ugb-modal-design-library', 'ugb-modal-design-library--is-multiselect' ) } onRequestClose={ props.onClose } >
    - - -
    - { - const newSelectedDesigns = [ ...selectedDesignIds ] - // We also get the design data from displayDesigns - // already instead of after clicking the "Add - // Designs" button since displayDesigns can change - // when the user is switching tabs (block/ui - // kits/wireframes) and the data can be lost. - const newSelectedDesignData = [ ...selectedDesignData ] + { + const newSelectedDesigns = [ ...selectedDesignIds ] + // We also get the design data from displayDesigns + // already instead of after clicking the "Add + // Designs" button since displayDesigns can change + // when the user is switching tabs (block/ui + // kits/wireframes) and the data can be lost. + const newSelectedDesignData = [ ...selectedDesignData ] - if ( newSelectedDesigns.includes( designId ) ) { - const i = newSelectedDesigns.indexOf( designId ) - newSelectedDesigns.splice( i, 1 ) - setSelectedDesignIds( newSelectedDesigns ) - newSelectedDesignData.splice( i, 1 ) - setSelectedDesignData( newSelectedDesignData ) - } else { - newSelectedDesigns.push( designId ) - setSelectedDesignIds( newSelectedDesigns ) - newSelectedDesignData.push( displayDesigns.find( design => design.id === designId ) ) - setSelectedDesignData( newSelectedDesignData ) - - // Pre-cache the selected design so that when - // the user decides to add it, it won't take too - // long to get. - getDesign( designId, props.apiVersion ) - } - } } - /> -
    + if ( newSelectedDesigns.includes( designId ) ) { + const i = newSelectedDesigns.indexOf( designId ) + newSelectedDesigns.splice( i, 1 ) + setSelectedDesignIds( newSelectedDesigns ) + newSelectedDesignData.splice( i, 1 ) + setSelectedDesignData( newSelectedDesignData ) + } else { + newSelectedDesigns.push( designId ) + setSelectedDesignIds( newSelectedDesigns ) + newSelectedDesignData.push( { + designId, category, designData: parsedBlocks, blocksForSubstitution, selectedPreviewSize, + } ) + setSelectedDesignData( newSelectedDesignData ) + } + } } + /> - { isMultiSelectMode &&
    ) @@ -315,3 +422,10 @@ ModalDesignLibrary.defaultProps = { apiVersion: '', onChangeApiVersion: () => {}, } + +const ColorSchemeTextItem = props => { + return
    +
    + { props.label } +
    +} diff --git a/src/deprecated/v2/modules/block-designs/index.js b/src/deprecated/v2/modules/block-designs/index.js index 446343f994..a3307ebd77 100644 --- a/src/deprecated/v2/modules/block-designs/index.js +++ b/src/deprecated/v2/modules/block-designs/index.js @@ -1,25 +1,23 @@ /** * Internal dependencies */ -import { DesignLibraryControl } from '../../components' +// import { DesignLibraryControl } from '../../components' /** * External dependencies */ import { PanelAdvancedSettings } from '~stackable/components' -import { applyBlockDesign } from '~stackable/util' +// import { applyBlockDesign } from '~stackable/util' import { i18n } from 'stackable' /** * WordPress dependencies */ -import { - addFilter, doAction, -} from '@wordpress/hooks' +import { addFilter, doAction } from '@wordpress/hooks' import { __ } from '@wordpress/i18n' import { Fragment } from '@wordpress/element' -const addDesignPanel = blockName => output => { +const addDesignPanel = () => output => { return ( { output } @@ -27,13 +25,14 @@ const addDesignPanel = blockName => output => { title={ __( 'Designs', i18n ) } initialOpen={ true } > -

    { __( 'You will not lose your block content when changing designs.', i18n ) }

    +

    { __( 'The designs for v2 blocks have been replaced with the new design library.', i18n ) }

    + { /*

    { __( 'You will not lose your block content when changing designs.', i18n ) }

    { applyBlockDesign( designData.attributes ) } } - /> + /> */ }
    ) diff --git a/src/design-library/index.js b/src/design-library/index.js index 37e1d6f371..a98dd58ff5 100644 --- a/src/design-library/index.js +++ b/src/design-library/index.js @@ -1,7 +1,7 @@ import apiFetch from '@wordpress/api-fetch' import { doAction, applyFilters } from '@wordpress/hooks' -const LATEST_API_VERSION = 'v3' +const LATEST_API_VERSION = 'v4' let designLibrary = null let designs = [] @@ -26,6 +26,7 @@ export const fetchDesignLibrary = async ( forceReset = false, version = '' ) => return designLibrary[ version || LATEST_API_VERSION ] } +// TODO: to remove export const fetchDesign = async ( designId, version = '' ) => { if ( ! designs[ designId ] ) { const results = await apiFetch( { @@ -37,6 +38,7 @@ export const fetchDesign = async ( designId, version = '' ) => { return designs[ designId ] } +// TODO: to remove export const setDevModeDesignLibrary = async ( devMode = false ) => { const results = await apiFetch( { path: `/stackable/v2/design_library_dev_mode/`, @@ -49,64 +51,23 @@ export const setDevModeDesignLibrary = async ( devMode = false ) => { } export const getDesigns = async ( { - type: isType = '', - block: isBlock = '', - mood: isMood = '', - plan: isPlan = '', - colors: hasColors = [], - categories: hasCategories = [], - uikit: isUiKit = '', - search = '', reset = false, - apiVersion = '', } ) => { - let library = Object.values( await fetchDesignLibrary( reset, apiVersion ) ) - - if ( isType ) { - library = library.filter( ( { type } ) => type === isType ) - } - - if ( isBlock ) { - const blockName = isBlock.replace( /^\w+\//, '' ) - library = library.filter( ( { block } ) => block.endsWith( `/${ blockName }` ) ) - } - - if ( isMood ) { - library = library.filter( ( { mood } ) => mood === isMood ) - } + const library = Object.values( await fetchDesignLibrary( reset ) ) + return library +} +export const filterDesigns = async ( { + library = [], + plan: isPlan = '', + category: isCategory = '', +} ) => { if ( isPlan ) { library = library.filter( ( { plan } ) => plan === isPlan ) } - if ( hasColors && hasColors.length ) { - library = library.filter( ( { colors } ) => colors.some( color => hasColors.includes( color ) ) ) - } - - if ( hasCategories && hasCategories.length ) { - library = library.filter( ( { categories } ) => categories.some( category => hasCategories.includes( category ) ) ) - } - - if ( isUiKit ) { - library = library.filter( ( { uikit } ) => uikit === isUiKit ) - } - - if ( search ) { - const terms = search.toLowerCase().replace( /\s+/, ' ' ).trim().split( ' ' ) - - // Every search term should match a property of a design. - terms.forEach( searchTerm => { - library = library.filter( design => { - // Our search term needs to match at least one of these properties. - const propertiesToSearch = applyFilters( 'stackable.design-library.search-properties', - [ 'label', 'plan', 'tags', 'categories', 'colors' ], apiVersion ) - - return propertiesToSearch.some( designProp => { - // Search whether the term matched. - return design[ designProp ].toString().toLowerCase().indexOf( searchTerm ) !== -1 - } ) - } ) - } ) + if ( isCategory ) { + library = library.filter( ( { category } ) => category === isCategory ) } return library diff --git a/src/design-library/init.php b/src/design-library/init.php index 9bb305e90d..98b31a1106 100644 --- a/src/design-library/init.php +++ b/src/design-library/init.php @@ -11,6 +11,16 @@ exit; } +if ( ! function_exists( 'media_handle_sideload' ) ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; +} +if ( ! function_exists( 'download_url' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; +} +if ( ! function_exists( 'wp_read_image_metadata' ) ) { + require_once ABSPATH . 'wp-admin/includes/image.php'; +} + if ( ! class_exists( 'Stackable_Design_Library' ) ) { /** * Class Stackable Design Library @@ -21,14 +31,18 @@ class Stackable_Design_Library { * The current version of the API we're using. * @var String */ - const API_VERSION = 'v3'; + const API_VERSION = 'v4'; /** * Constructor */ public function __construct() { add_action( 'rest_api_init', array( $this, 'register_route' ) ); - add_filter( 'stackable_design_library_retreive_body', array( $this, 'replace_dev_mode_urls' ) ); + + add_filter( 'stackable_design_library_get_premium_designs', array( $this, 'get_designs_with_disabled_blocks' ) ); + add_filter( 'stackable_design_library_get_premium_designs', array( $this, 'get_premium_designs' ) ); + add_action( 'init', array( $this, 'register_design_pattern' ) ); + add_action( 'stackable_delete_design_library_cache', array( $this, 'delete_cache_v3' ) ); } public static function validate_string( $value, $request, $param ) { @@ -45,6 +59,13 @@ public static function validate_boolean( $value, $request, $param ) { return true; } + public static function validate_url( $value, $request, $param ) { + if ( ! filter_var( $value, FILTER_VALIDATE_URL ) || ! wp_http_validate_url( $value ) ) { + return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a valid URL.', STACKABLE_I18N ), $param ) ); + } + return true; + } + /** * Register Rest API routes for the design library. */ @@ -61,41 +82,26 @@ public function register_route() { ), ), ) ); - - register_rest_route( 'stackable/v2', '/design/(?P[\w\d-]*)/(?P[\w\d-]+)', array( - 'methods' => 'GET', - 'callback' => array( $this, 'get_design' ), - 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); - }, - 'args' => array( - 'version' => array( - 'validate_callback' => __CLASS__ . '::validate_string' - ), - 'design' => array( - 'validate_callback' => __CLASS__ . '::validate_string' - ), - ), - ) ); - - register_rest_route( 'stackable/v2', '/design_library_dev_mode', array( + register_rest_route( 'stackable/v3', '/design_library_image', array( 'methods' => 'POST', - 'callback' => array( $this, 'set_dev_mode' ), + 'callback' => array( $this, 'get_design_library_image' ), 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, 'args' => array( - 'devmode' => array( - 'validate_callback' => __CLASS__ . '::validate_boolean' + 'image_url' => array( + 'required' => true, + 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => __CLASS__ . '::validate_url' ), ), ) ); } /** - * Deletes all caches. + * Deletes all design library v3 caches. */ - public function delete_cache() { + public function delete_cache_v3() { // Delete design library. delete_transient( 'stackable_get_design_library' ); @@ -110,19 +116,144 @@ public function delete_cache() { delete_transient( $transient ); } } + } + + public function delete_cache() { + $designs = $this->get_design_library_from_cloud(); + + $library = $designs[ self::API_VERSION ]; + foreach ( $library as $design_id => $design ) { + if ( WP_Block_Patterns_Registry::get_instance()->is_registered( 'stackable/' . $design_id ) ) { + $res = unregister_block_pattern( 'stackable/' . $design_id ); + } + + if ( WP_Block_Pattern_Categories_Registry::get_instance()->is_registered( 'stackable/' . $design[ 'category' ] ) ) { + $res = unregister_block_pattern_category( 'stackable/' . $design[ 'category' ] ); + } + } + // Delete design library. + delete_transient( 'stackable_get_design_library_v4' ); + delete_transient( 'stackable_get_design_library_json_v4' ); + + $this->register_design_pattern(); do_action( 'stackable_delete_design_library_cache' ); } - public function _get_design_library() { - $designs = get_transient( 'stackable_get_design_library_v3' ); + public function get_design_library_image( $request ) { + $url = $request->get_param( 'image_url' ); + + $basename = sanitize_file_name( wp_basename( parse_url( $url, PHP_URL_PATH ) ) ); + + $args = array( + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'posts_per_page' => 1, + 'meta_query' => array( + array( + 'key' => '_wp_attached_file', + 'value' => $basename, + 'compare' => 'LIKE' + ) + ) + ); + + $attachments = new WP_Query( $args ); + + if ( $attachments->have_posts() ) { + $attachments->the_post(); + $media_id = get_the_ID(); + $media_url = wp_get_attachment_url( $media_id ); + + wp_reset_postdata(); + + return new WP_REST_Response( array( + 'success' => true, + 'new_url' => $media_url, + 'old_url' => $url + ), 200 ); + } + + $temp_filepath = download_url( $url ); + + if ( is_wp_error( $temp_filepath ) ) { + return new WP_REST_Response( array( + 'success' => false, + 'message' => $temp_filepath->get_error_message() + ), 500 ); + } + + if ( ! file_exists( $temp_filepath ) || ! wp_filesize( $temp_filepath ) ) { + wp_delete_file( $temp_filepath ); + return new WP_REST_Response( array( + 'success' => false, + // This is a custom check so we return a custom error message. + 'message' => 'Invalid file content retrieved from the provided URL.' + ), 400 ); + } + + $valid_mimes = [ 'image/jpeg' => 1, 'image/jpg' => 1, 'image/png' => 1, 'image/gif' => 1, 'image/webp' => 1, 'video/mp4' => 1 ]; + + $file_array = array( + 'name' => $basename, + 'type' => mime_content_type( $temp_filepath ), + 'tmp_name' => $temp_filepath, + 'size' => wp_filesize( $temp_filepath ) + ); + + if ( ! isset( $valid_mimes[ $file_array[ 'type' ] ] ) + || ( strpos( $file_array[ 'type' ], 'image/' ) === 0 + && ! wp_getimagesize( $temp_filepath ) + ) + ) { + wp_delete_file( $temp_filepath ); + return new WP_REST_Response( array( + 'success' => false, + // This is a custom check so we return a custom error message. + 'message' => 'The file is not a valid image/video.' + ), 400 ); + } + + $media_id = media_handle_sideload( $file_array, 0, null, array( + 'post_mime_type' => $file_array[ 'type' ], + 'post_title' => sanitize_text_field( pathinfo( $file_array[ 'name' ], PATHINFO_FILENAME ) ), + 'post_status' => 'inherit' + ) ); + + if ( file_exists( $temp_filepath ) ) { + wp_delete_file( $temp_filepath ); + } + + if ( is_wp_error( $media_id ) ) { + return new WP_REST_Response( array( + 'success' => false, + 'message' => $media_id->get_error_message() + ), 500 ); + } + + $media_url = wp_get_attachment_url( $media_id ); + + return new WP_REST_Response( array( + 'success' => true, + 'new_url' => $media_url, + 'old_url' => $url + ), 200 ); + } + + public function filter_patterns( $pattern ) { + return strpos( $pattern[ 'name' ], 'stackable/' ) !== false; + } + + public function get_design_library_from_cloud() { + $designs = get_transient( 'stackable_get_design_library_json_v4' ); // Fetch designs. if ( empty( $designs ) ) { $designs = array(); $content = null; - $response = wp_remote_get( self::get_cdn_url() . 'library-v3/library.json' ); + // Add ignoreCache to retrieve the updated file + $response = wp_remote_get( self::get_cdn_url() . 'library-v4/library.json?ignoreCache=1' ); if ( is_wp_error( $response ) ) { // Add our error message so we can see it in the network tab. @@ -144,14 +275,46 @@ public function _get_design_library() { } - // We add the latest designs in the `v3` area. + // We add the latest designs in the `v4` area. $designs[ self::API_VERSION ] = $content; // Allow deprecated code to fetch other designs $designs = apply_filters( 'stackable_fetch_design_library', $designs ); // Cache results. - set_transient( 'stackable_get_design_library_v3', $designs, DAY_IN_SECONDS ); + set_transient( 'stackable_get_design_library_json_v4', $designs, DAY_IN_SECONDS ); + } + + return apply_filters( 'stackable_design_library', $designs ); + } + + public function _get_design_library( $outside_init = false ) { + $designs = get_transient( 'stackable_get_design_library_v4' ); + // Fetch designs. + if ( empty( $designs ) ) { + $designs = array(); + $content = array(); + + $block_patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered( $outside_init ); + foreach ( $block_patterns as $pattern ) { + if ( strpos( $pattern[ 'name' ], 'stackable/' ) !== false ) { + $pattern[ 'title' ] = str_replace( sprintf( __( 'Stackable ', STACKABLE_I18N ) ), '', $pattern[ 'title' ] ); + $content[ $pattern[ 'designId' ] ] = $pattern; + } + } + + // Get premium designs for v4 + $content = apply_filters( 'stackable_design_library_get_premium_designs', $content ); + + // We add the latest designs in the `v4` area. + $designs[ self::API_VERSION ] = $content; + + // Allow deprecated code to fetch other designs + $designs = apply_filters( 'stackable_fetch_design_library', $designs ); + + // Cache results. + set_transient( 'stackable_get_design_library_v4', $designs, DAY_IN_SECONDS ); + } return apply_filters( 'stackable_design_library', $designs ); @@ -161,85 +324,162 @@ public function _get_design_library() { * Gets and caches library designs. */ public function get_design_library( $request ) { - if ( $request->get_param( 'reset' ) ) { + $reset = $request->get_param( 'reset' ); + if ( $reset ) { $this->delete_cache(); } - return rest_ensure_response( $this->_get_design_library() ); + return rest_ensure_response( $this->_get_design_library( $reset ) ); } - public function get_design( $request ) { - $design_id = $request->get_param( 'design' ); - // We don't do anything with this one yet. - $library_version = $request->get_param( 'version' ); + public function get_disabled_blocks() { + $disabled_blocks = get_option( 'stackable_block_states' ); - $design = get_transient( 'stackable_get_design_' . $design_id ); + if ( $disabled_blocks == false ) { + return false; + } - // Fetch designs. - if ( empty( $design ) ) { + $disabled_blocks = array_filter( $disabled_blocks, function ( $block_state ) { return $block_state == 3; } ); + if ( count( $disabled_blocks ) ) { + $disabled_blocks = array_keys( $disabled_blocks ); + $disabled_blocks = array_map( function ( $block ) { return preg_quote( $block, '/' ); }, $disabled_blocks ); + $disabled_blocks = '/' . implode( '|', $disabled_blocks ) . '/i'; + return $disabled_blocks; + } - // Get the template Url. - $designs = $this->_get_design_library(); - $template_url = $designs[ self::API_VERSION ][ $design_id ]['template']; + return false; + } - $response = wp_remote_get( $template_url ); - $content = wp_remote_retrieve_body( $response ); - $content = apply_filters( 'stackable_design_library_retreive_body', $content ); - $design = json_decode( $content, true ); + public function check_for_disabled_block( $design, $disabled_blocks ) { + if ( preg_match( $disabled_blocks, $design ) ) { + return true; + } - // Cache results. - set_transient( 'stackable_get_design_' . $design_id, $design, DAY_IN_SECONDS ); + return false; + } + + public function get_premium_designs( $content ) { + $designs = $this->get_design_library_from_cloud(); + + $library = $designs[ self::API_VERSION ]; + + $premium_designs = array(); + foreach ( $library as $design_id => $design ) { + if ( $design[ 'plan' ] === 'premium' && sugb_fs()->can_use_premium_code() && STACKABLE_BUILD === 'premium' ) { + continue; + } + + $premium_designs[ $design_id ] = array( + 'title' => $design[ 'label' ], + 'content' => $design[ 'template' ], + 'category' => $design[ 'category' ], + 'description' => $design[ 'description' ], + 'plan' => $design[ 'plan' ], + 'designId' => $design_id + ); } - $design = apply_filters( 'stackable_design_' . $design_id, $design ); + $merged = array_merge( $content, $premium_designs ); - return rest_ensure_response( $design ); + uasort($merged, function( $design_1, $design_2 ) { + return strnatcmp( $design_1[ 'title' ], $design_2[ 'title' ] ); + }); + + return $merged; } - /** - * Replaces all the URLs that use the CDN with local ones if dev mode is turned on. - */ - public function replace_dev_mode_urls( $content ) { - if ( self::is_dev_mode() ) { - $content = str_replace( - trailingslashit( STACKABLE_DESIGN_LIBRARY_URL ), - trailingslashit( plugin_dir_url( STACKABLE_DLH_LIBRARY_FILE ) ), - $content + public function get_designs_with_disabled_blocks( $content ) { + $designs = $this->get_design_library_from_cloud(); + + $library = $designs[ self::API_VERSION ]; + + $designs_with_disabled = array(); + foreach ( $library as $design_id => $design ) { + if ( isset( $content[ $design_id ] ) ) { + continue; + } + + $designs_with_disabled[ $design_id ] = array( + 'title' => $design[ 'label' ], + 'content' => $design[ 'template' ], + 'category' => $design[ 'category' ], + 'description' => $design[ 'description' ], + 'plan' => $design[ 'plan' ], + 'designId' => $design_id, ); } - return $content; + + $merged = array_merge( $content, $designs_with_disabled ); + + uasort($merged, function( $design_1, $design_2 ) { + return strnatcmp( $design_1[ 'title' ], $design_2[ 'title' ] ); + }); + + return $merged; } - /** - * Gets the URL of the CDN where to load our design library data. When - * developer mode for the design library is turned on, the URL of the - * design library internal exporter tool will be used instead. - */ - public static function get_cdn_url() { - if ( self::is_dev_mode() ) { - return trailingslashit( plugin_dir_url( STACKABLE_DLH_LIBRARY_FILE ) ); - } else { - return trailingslashit( STACKABLE_DESIGN_LIBRARY_URL ); - } + public function get_category_kebab_case( $category ) { + $category = trim( strtolower( $category ) ); + return preg_replace( '/[^a-z0-9-]+/', '-', $category ); } - public static function is_dev_mode() { - $dev_env = defined( 'WP_ENV' ) ? WP_ENV === 'development' : false; - $export_tool_installed = defined( 'STACKABLE_DLH_LIBRARY_FILE' ); - $library_dev_mode_toggled = !! get_option( 'stackable_library_dev_mode' ); - return $dev_env && $export_tool_installed && $library_dev_mode_toggled; + public function register_design_pattern() { + $designs = $this->get_design_library_from_cloud(); + + $library = $designs[ self::API_VERSION ]; + + if ( ! $library ) { + return; + } + + + $disabled_blocks = $this->get_disabled_blocks(); + + + if ( ! WP_Block_Pattern_Categories_Registry::get_instance()->is_registered( 'stackable' ) ) { + register_block_pattern_category( 'stackable', [ + 'label' => __( 'Stackable', STACKABLE_I18N ), + 'description' => __( 'Patterns for Stackable Design Library', STACKABLE_I18N ), + ] ); + } + + foreach ( $library as $design_id => $design ) { + if ( $design[ 'plan' ] === 'premium' && ( ! sugb_fs()->can_use_premium_code() || STACKABLE_BUILD === 'free' ) ) { + continue; + } + + if ( $disabled_blocks ) { + $has_disabled = $this->check_for_disabled_block( $design[ 'template' ], $disabled_blocks ); + if ( $has_disabled ) continue; + } + + register_block_pattern_category( 'stackable/' . $this->get_category_kebab_case( $design[ 'category' ] ), [ + 'label' => sprintf( __( 'Stackable %s', STACKABLE_I18N ), $design[ 'category' ] ), + 'description' => sprintf( __( '%s patterns for Stackable Design Library', STACKABLE_I18N ), $design[ 'category' ] ), + ] ); + + register_block_pattern( + 'stackable/' . $design_id, + array( + 'title' => sprintf( __( 'Stackable %s', STACKABLE_I18N ), $design[ 'label' ] ), + 'content' => $design[ 'template' ], + 'categories' => array( 'stackable/' . $this->get_category_kebab_case( $design[ 'category' ] ), 'stackable' ), // used in Patterns + 'category' => $design[ 'category' ], // used in Design Library + 'description' => $design[ 'description' ], + 'plan' => $design[ 'plan' ], + 'designId' => $design_id + ) + ); + } } /** - * Set developer mode for the design library. When in developer mode, - * the library that will be loaded will be from the design library - * internal exporter tool. + * Gets the URL of the CDN where to load our design library data. When + * developer mode for the design library is turned on, the URL of the + * design library internal exporter tool will be used instead. */ - public function set_dev_mode( $request ) { - $dev_mode = $request->get_param( 'devmode' ); - update_option( 'stackable_library_dev_mode', $dev_mode, 'no' ); - - return rest_ensure_response( true ); + public static function get_cdn_url() { + return trailingslashit( STACKABLE_DESIGN_LIBRARY_URL ); } } diff --git a/src/hooks/use-block-color-schemes.js b/src/hooks/use-block-color-schemes.js index 4968620f05..6241d459f1 100644 --- a/src/hooks/use-block-color-schemes.js +++ b/src/hooks/use-block-color-schemes.js @@ -1,5 +1,5 @@ import { i18n } from 'stackable' -import { kebabCase } from 'lodash' +import { cloneDeep, kebabCase } from 'lodash' import { useSelect } from '@wordpress/data' import { applyFilters } from '@wordpress/hooks' @@ -19,6 +19,7 @@ export const useBlockColorSchemes = () => { getScheme, getColorGroups, allColorSchemes, + colorSchemesCollection, COLOR_SCHEME_OPTIONS, baseColorScheme, backgroundModeColorScheme, @@ -44,6 +45,26 @@ export const useBlockColorSchemes = () => { disabled: ! schemeHasValue( scheme.colorScheme ), } ) ) ] + const colorSchemesCollection = allColorSchemes.reduce( ( obj, _scheme ) => { + const scheme = cloneDeep( _scheme ) + + if ( ! schemeHasValue( scheme.colorScheme ) ) { + return obj + } + // if ( scheme.key === 'scheme-default-2' && ! schemeHasValue( scheme.colorScheme ) ) { + // scheme.colorScheme.backgroundColor.desktop = 'var(--stk-block-background-color)' + // } + + const desktopColors = Object.fromEntries( + Object.entries( scheme.colorScheme ).map( ( [ key, value ] ) => [ key, value.desktop ] ) + ) + + obj[ scheme.key ] = scheme + obj[ scheme.key ].desktopColors = desktopColors + + return obj + }, {} ) + // Returns the color scheme slug if it exists, otherwise return a fallback value const getScheme = ( key, { mode = '', returnFallback = true } = {} ) => { const fallback = mode === 'background' ? 'scheme-default-2' : 'scheme-default-1' @@ -131,6 +152,7 @@ export const useBlockColorSchemes = () => { getScheme, getColorGroups, allColorSchemes, + colorSchemesCollection, COLOR_SCHEME_OPTIONS, baseColorScheme: getScheme( _baseColorScheme ), backgroundModeColorScheme: getScheme( _backgroundModeColorScheme, 'background' ), @@ -142,6 +164,7 @@ export const useBlockColorSchemes = () => { getScheme, getColorGroups, allColorSchemes, + colorSchemesCollection, COLOR_SCHEME_OPTIONS, baseColorScheme, backgroundModeColorScheme, diff --git a/src/init.php b/src/init.php index b1f91dae3e..a01bb0850a 100644 --- a/src/init.php +++ b/src/init.php @@ -334,6 +334,8 @@ public function register_block_editor_assets() { $version_parts = explode( '-', STACKABLE_VERSION ); + $wp_global_styles = wp_get_global_stylesheet(); + global $content_width; global $wp_version; $args = apply_filters( 'stackable_localize_script', array( @@ -369,6 +371,9 @@ public function register_block_editor_assets() { 'settings' => apply_filters( 'stackable_js_settings', array() ), 'isContentOnlyMode' => apply_filters( 'stackable_editor_role_is_content_only', false ), 'blockCategoryIndex' => apply_filters( 'stackable_block_category_index', 0 ), + + // Global Styles for Design Library + 'wpGlobalStylesInlineCss' => $wp_global_styles, ) ); wp_localize_script( 'wp-blocks', 'stackable', $args ); } @@ -462,6 +467,8 @@ public function add_body_class_theme_compatibility( $classes ) { $classes[] = 'stk--is-twentytwentyfive-theme'; } else if ( function_exists( 'hello_elementor_setup' ) ) { // Taken from https://github.com/elementor/hello-theme/blob/master/functions.php $classes[] = 'stk--is-helloelementor-theme'; + } else if ( function_exists( 'tove_setup' ) ) { + $classes[] = 'stk--is-tove-theme'; } return $convert_to_string ? implode( ' ', $classes ) : $classes; diff --git a/src/plugins/editor-device-preview-class/index.js b/src/plugins/editor-device-preview-class/index.js index f32c3fc7c5..06cf13c5d6 100644 --- a/src/plugins/editor-device-preview-class/index.js +++ b/src/plugins/editor-device-preview-class/index.js @@ -15,6 +15,7 @@ import { useDeviceType, useBlockHoverState } from '~stackable/hooks' import { useEffect } from '@wordpress/element' import { useSelect } from '@wordpress/data' import { registerPlugin } from '@wordpress/plugins' +import { addFilter } from '@wordpress/hooks' const EditorPreviewClass = () => { const deviceType = useDeviceType() @@ -45,6 +46,10 @@ const EditorPreviewClass = () => { if ( document.querySelector( 'body' ).className.match( themeRegex ) && ! editorEl.className.match( themeRegex ) ) { const theme = document.querySelector( 'body' ).className.match( themeRegex )[ 0 ] editorEl.classList.add( theme ) + addFilter( 'stackable.global-styles.classnames', 'stackable/theme-classname', styleIds => { + styleIds.push( theme ) + return styleIds + } ) } // At first load of the editor, the `stk-preview-device-*` and `stk--is-*-theme` are removed, so we have to re-add it. @@ -60,6 +65,10 @@ const EditorPreviewClass = () => { if ( document.querySelector( 'body' ).className.match( themeRegex ) && ! editorEl.className.match( themeRegex ) ) { const theme = document.querySelector( 'body' ).className.match( themeRegex )[ 0 ] editorEl.classList.add( theme ) + addFilter( 'stackable.global-styles.classnames', 'stackable/theme-classname', styleIds => { + styleIds.push( theme ) + return styleIds + } ) } } ) diff --git a/src/plugins/global-settings/color-schemes/color-scheme-picker.js b/src/plugins/global-settings/color-schemes/color-scheme-picker.js index 13bfc86709..9a1a8e4b3f 100644 --- a/src/plugins/global-settings/color-schemes/color-scheme-picker.js +++ b/src/plugins/global-settings/color-schemes/color-scheme-picker.js @@ -106,6 +106,12 @@ const ColorSchemePicker = props => { const currentState = `desktop${ hoverState[ currentHoverState ] }` + const changeDefaultName = ! itemInEdit?.key.startsWith( 'scheme-default' ) ? false + : ( itemInEdit?.key === 'scheme-default-1' + ? itemInEdit?.name === __( 'Default Scheme', i18n ) + : itemInEdit?.name === __( 'Background Scheme', i18n ) + ) + const showResetButton = item => { return ! isEqual( item.colorScheme, DEFAULT_COLOR_SCHEME_COLORS ) } @@ -197,6 +203,11 @@ const ColorSchemePicker = props => { } const currentItem = cloneDeep( itemInEdit ) + + if ( ! schemeHasValue( currentItem.colorScheme ) && changeDefaultName ) { + currentItem.name = currentItem?.key === 'scheme-default-1' ? 'Color Scheme 1' : 'Color Scheme 2' + } + currentItem.colorScheme[ property ][ currentState ] = color if ( property === 'backgroundColor' && isGradient( color ) ) { @@ -227,6 +238,11 @@ const ColorSchemePicker = props => { return } const currentItem = cloneDeep( itemInEdit ) + + if ( ! schemeHasValue( currentItem.colorScheme ) && changeDefaultName ) { + currentItem.name = currentItem?.key === 'scheme-default-1' ? __( 'Color Scheme 1', i18n ) : __( 'Color Scheme 2', i18n ) + } + if ( currentHoverState === 'normal' ) { currentItem.colorScheme = cloneDeep( DEFAULT_COLOR_SCHEME_COLORS ) } @@ -252,6 +268,14 @@ const ColorSchemePicker = props => { } const currentItem = cloneDeep( item ) + if ( currentItem.key === 'scheme-default-1' && currentItem.name === __( 'Color Scheme 1', i18n ) ) { + currentItem.name = __( 'Default Scheme', i18n ) + } + + if ( currentItem.key === 'scheme-default-2' && currentItem.name === __( 'Color Scheme 2', i18n ) ) { + currentItem.name = __( 'Background Scheme', i18n ) + } + currentItem.colorScheme = cloneDeep( DEFAULT_COLOR_SCHEME_COLORS ) if ( itemInEdit ) { diff --git a/src/plugins/global-settings/color-schemes/editor-loader.js b/src/plugins/global-settings/color-schemes/editor-loader.js index 5591d0aa8b..92876e291e 100644 --- a/src/plugins/global-settings/color-schemes/editor-loader.js +++ b/src/plugins/global-settings/color-schemes/editor-loader.js @@ -19,7 +19,7 @@ import { useBlockColorSchemes, useBlockHoverState } from '~stackable/hooks' */ import { useEffect, useState } from '@wordpress/element' import { useSelect } from '@wordpress/data' -import { applyFilters } from '@wordpress/hooks' +import { applyFilters, addFilter } from '@wordpress/hooks' const renderGlobalStyles = ( setStyles, @@ -150,6 +150,10 @@ export const GlobalColorSchemeStyles = () => { if ( editorEl ) { if ( styles !== '' && editorEl.classList.contains( 'stk-has-color-schemes' ) === false ) { editorEl.classList.add( 'stk-has-color-schemes' ) + addFilter( 'stackable.global-styles.classnames', `stackable/global-settings.color-schemes`, classnames => { + classnames.push( 'stk-has-color-schemes' ) + return classnames + } ) } if ( styles === '' ) { editorEl.classList.remove( 'stk-has-color-schemes' ) @@ -159,6 +163,10 @@ export const GlobalColorSchemeStyles = () => { const mo = onClassChange( editorEl, () => { if ( styles !== '' && editorEl?.classList.contains( 'stk-has-color-schemes' ) === false ) { editorEl?.classList.add( 'stk-has-color-schemes' ) + addFilter( 'stackable.global-styles.classnames', `stackable/global-settings.color-schemes`, classnames => { + classnames.push( 'stk-has-color-schemes' ) + return classnames + } ) } if ( styles === '' ) { editorEl?.classList.remove( 'stk-has-color-schemes' ) diff --git a/src/plugins/global-settings/color-schemes/index.js b/src/plugins/global-settings/color-schemes/index.js index 74ebae6ed7..922c4b1db9 100644 --- a/src/plugins/global-settings/color-schemes/index.js +++ b/src/plugins/global-settings/color-schemes/index.js @@ -8,17 +8,17 @@ import ColorSchemePicker from './color-scheme-picker' /** * External dependencies */ +import { i18n } from 'stackable' import { - i18n, showProNotice, isPro, -} from 'stackable' -import { - PanelAdvancedSettings, ProControlButton, HelpTooltip, + PanelAdvancedSettings, HelpTooltip, + AdvancedSelectControl, SectionSettings, } from '~stackable/components' +import { useBlockColorSchemes } from '~stackable/hooks' /** * WordPress dependencies */ -import { addFilter, applyFilters } from '@wordpress/hooks' +import { addFilter } from '@wordpress/hooks' import { Fragment, useState } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { ToggleControl } from '@wordpress/components' @@ -27,11 +27,40 @@ import { models } from '@wordpress/api' export { GlobalColorSchemeStyles } from './editor-loader' +let saveTimeout = null + +const COLOR_SCHEME_OPTION_NAMES = { + baseColorScheme: 'stackable_global_base_color_scheme', + backgroundModeColorScheme: 'stackable_global_background_mode_color_scheme', + containerModeColorScheme: 'stackable_global_container_mode_color_scheme', +} + addFilter( 'stackable.global-settings.inspector', 'stackable/global-color-schemes', output => { const [ itemInEdit, setItemInEdit ] = useState( null ) const [ isOpen, setIsOpen ] = useState( false ) const [ displayHoverNotice, setDisplayHoverNotice ] = useState( false ) + const { + COLOR_SCHEME_OPTIONS, + baseColorScheme, + backgroundModeColorScheme, + containerModeColorScheme, + } = useBlockColorSchemes() + + const onChangeDefaultColorScheme = ( mode, colorSchemeKey ) => { + clearTimeout( saveTimeout ) + saveTimeout = setTimeout( () => { + const settings = new models.Settings( { + [ COLOR_SCHEME_OPTION_NAMES[ mode ] ]: colorSchemeKey, // eslint-disable-line camelcase + stackable_global_color_scheme_generated_css: '', // eslint-disable-line camelcase + } ) + settings.save() + }, 300 ) + + // Update our store. + dispatch( 'stackable/global-color-schemes' ).updateSettings( { [ mode ]: colorSchemeKey } ) + } + return ( { output } @@ -67,8 +96,34 @@ addFilter( 'stackable.global-settings.inspector', 'stackable/global-color-scheme setItemInEdit={ setItemInEdit } setDisplayHoverNotice={ setDisplayHoverNotice } /> - { isPro && applyFilters( 'stackable.global-settings.global-color-schemes.inspector', Fragment, itemInEdit ) } - { ! itemInEdit && showProNotice && } + { ! itemInEdit && <> + + onChangeDefaultColorScheme( 'baseColorScheme', colorScheme ) } + help={ __( 'Default color scheme to use for all blocks when no special options are enabled.', i18n ) } + /> + onChangeDefaultColorScheme( 'backgroundModeColorScheme', colorScheme ) } + help={ __( 'Colors applied when the background option is enabled for a block.', i18n ) } + /> + onChangeDefaultColorScheme( 'containerModeColorScheme', colorScheme ) } + help={ __( 'Colors applied when the container option is enabled for a block.', i18n ) } + /> + + } ) diff --git a/src/plugins/global-settings/color-schemes/index.php b/src/plugins/global-settings/color-schemes/index.php index f8fc782b07..8af0735ee2 100644 --- a/src/plugins/global-settings/color-schemes/index.php +++ b/src/plugins/global-settings/color-schemes/index.php @@ -463,7 +463,7 @@ public function getDefaultContainerColors( $styles, $scheme ) { '--stk-text-color' => 'var(--stk-container-color, initial)', '--stk-link-color' => 'var(--stk-default-link-color, var(--stk-text-color, initial))', '--stk-accent-color' => '#ddd', - '--stk-subtitle-color' => '#39414d', + '--stk-subtitle-color' => 'var(--stk-default-subtitle-color, #39414d)', '--stk-default-icon-color' => 'var(--stk-icon-color)', '--stk-button-background-color' => 'var(--stk-default-button-background-color, #008de4)', '--stk-button-text-color' => 'var(--stk-default-button-text-color, #fff)', diff --git a/src/plugins/global-settings/color-schemes/utils.js b/src/plugins/global-settings/color-schemes/utils.js index 759a9fa8bd..6c50679f75 100644 --- a/src/plugins/global-settings/color-schemes/utils.js +++ b/src/plugins/global-settings/color-schemes/utils.js @@ -170,7 +170,7 @@ export const getDefaultColors = () => { decls += '--stk-text-color: var(--stk-container-color, initial);' decls += '--stk-link-color: var(--stk-default-link-color, var(--stk-text-color, initial));' decls += '--stk-accent-color: #ddd;' - decls += '--stk-subtitle-color: #39414d;' + decls += '--stk-subtitle-color: var(--stk-default-subtitle-color, #39414d);' decls += '--stk-default-icon-color: var(--stk-icon-color);' decls += '--stk-button-background-color: var(--stk-default-button-background-color, #008de4);' decls += '--stk-button-text-color: var(--stk-default-button-text-color, #fff);' diff --git a/src/plugins/global-settings/editor-loader.js b/src/plugins/global-settings/editor-loader.js index 73a506188f..878d5ca058 100644 --- a/src/plugins/global-settings/editor-loader.js +++ b/src/plugins/global-settings/editor-loader.js @@ -26,6 +26,7 @@ import { useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { useSelect } from '@wordpress/data' import domReady from '@wordpress/dom-ready' +import { addFilter } from '@wordpress/hooks' const GlobalSettingsLoader = () => { const deviceType = useDeviceType() @@ -76,6 +77,20 @@ globalSpacingAndBorderWrapper?.setAttribute( 'id', 'stk-global-spacing-and-borde globalButtonsAndIconsWrapper?.setAttribute( 'id', 'stk-global-buttons-and-icons-styles' ) globalColorSchemesWrapper?.setAttribute( 'id', 'stk-global-color-schemes-styles' ) globalPresetControlsWrapper?.setAttribute( 'id', 'stk-global-preset-controls-styles' ) + +addFilter( 'stackable.global-styles.ids', 'stackable/global-settings', styleIds => { + styleIds.push( + 'stk-global-typography-styles', + 'stk-global-color-styles', + 'stk-global-spacing-and-borders-styles', + 'stk-global-buttons-and-icons-styles', + 'stk-global-color-schemes-styles', + 'stk-global-preset-controls-styles' + ) + + return styleIds +} ) + domReady( () => { document?.head?.appendChild( globalTypographyWrapper ) document?.head?.appendChild( globalColorWrapper ) diff --git a/src/plugins/global-settings/typography/editor-loader.js b/src/plugins/global-settings/typography/editor-loader.js index 29e56a05e0..934a2a1c29 100644 --- a/src/plugins/global-settings/typography/editor-loader.js +++ b/src/plugins/global-settings/typography/editor-loader.js @@ -201,7 +201,7 @@ export const formClassOrTagSelectors = ( selector, applyTo ) => { const isClassSelector = selector.startsWith( '.' ) // Include Stackable blocks. - selectors.push( `[data-type^="stackable/"] ${ selector }` ) + selectors.push( `:is([data-type^="stackable/"], .stk-block) ${ selector }` ) // Include native blocks. if ( isClassSelector || applyTo !== 'blocks-stackable' ) { diff --git a/src/plugins/global-settings/utils/use-block-layout-editor-loader.js b/src/plugins/global-settings/utils/use-block-layout-editor-loader.js index 6320229663..ff07be79ab 100644 --- a/src/plugins/global-settings/utils/use-block-layout-editor-loader.js +++ b/src/plugins/global-settings/utils/use-block-layout-editor-loader.js @@ -8,6 +8,7 @@ import { getDefault, onClassChange } from './block-layout-utils' */ import { useSelect } from '@wordpress/data' import { useEffect, useState } from '@wordpress/element' +import { addFilter } from '@wordpress/hooks' /** * External dependencies @@ -167,6 +168,10 @@ export const useBlockLayoutEditorLoader = ( storeName, classSuffix ) => { const className = `stk-has-design-system-${ classSuffix }` if ( styles !== '' && editorEl.classList.contains( className ) === false ) { editorEl.classList.add( className ) + addFilter( 'stackable.global-styles.classnames', `stackable/global-settings.${ classSuffix }`, classnames => { + classnames.push( className ) + return classnames + } ) } if ( styles === '' ) { editorEl.classList.remove( className ) @@ -175,6 +180,10 @@ export const useBlockLayoutEditorLoader = ( storeName, classSuffix ) => { const mo = onClassChange( editorEl, () => { if ( styles !== '' && editorEl?.classList.contains( className ) === false ) { editorEl?.classList.add( className ) + addFilter( 'stackable.global-styles.classnames', `stackable/global-settings.${ classSuffix }`, classnames => { + classnames.push( className ) + return classnames + } ) } if ( styles === '' ) { editorEl?.classList.remove( className ) diff --git a/src/plugins/theme-block-style-inheritance/index.js b/src/plugins/theme-block-style-inheritance/index.js index 9f2eae02e2..53790a4e67 100644 --- a/src/plugins/theme-block-style-inheritance/index.js +++ b/src/plugins/theme-block-style-inheritance/index.js @@ -9,6 +9,7 @@ import { fetchSettings } from '~stackable/util' import { useEffect, useState } from '@wordpress/element' import { useSelect } from '@wordpress/data' import { registerPlugin } from '@wordpress/plugins' +import { addFilter } from '@wordpress/hooks' // Adds a body class for block style inheritance const ThemeBlockStyleInheritanceClass = () => { @@ -19,7 +20,15 @@ const ThemeBlockStyleInheritanceClass = () => { useEffect( () => { fetchSettings().then( response => { - setDisableBlockStyleInheritance( response.stackable_disable_block_style_inheritance ) + const isDisabled = response.stackable_disable_block_style_inheritance + setDisableBlockStyleInheritance( isDisabled ) + + if ( ! isDisabled ) { + addFilter( 'stackable.global-styles.classnames', `stackable/global-settings.block-style-inheritance`, classnames => { + classnames.push( 'stk-has-block-style-inheritance' ) + return classnames + } ) + } } ) }, [] ) diff --git a/src/plugins/theme-block-style-inheritance/index.php b/src/plugins/theme-block-style-inheritance/index.php index 747643d2b4..6d7168e750 100644 --- a/src/plugins/theme-block-style-inheritance/index.php +++ b/src/plugins/theme-block-style-inheritance/index.php @@ -199,6 +199,8 @@ function add_block_style_inheritance( $current_css ) { $style_declarations['root']['declarations'][ '--stk-container-color' ] = $root_properties[ 'text-color' ]; $style_declarations['root']['declarations'][ '--stk-text-color' ] = $root_properties[ 'text-color' ]; $style_declarations['root']['declarations'][ '--stk-default-text-color' ] = $root_properties[ 'text-color' ]; + $style_declarations['root']['declarations'][ '--stk-subtitle-color' ] = $root_properties[ 'text-color' ]; + $style_declarations['root']['declarations'][ '--stk-default-subtitle-color' ] = $root_properties[ 'text-color' ]; } if ( $root_properties[ 'heading-color' ] ) { diff --git a/src/styles/block-design-system-blocks.scss b/src/styles/block-design-system-blocks.scss index ff1a13f563..84f4bc0bd2 100644 --- a/src/styles/block-design-system-blocks.scss +++ b/src/styles/block-design-system-blocks.scss @@ -430,7 +430,7 @@ body:not(.wp-admin) .stk-block-columns:has(> .stk-block-content > .stk-block-col .stk-block-posts { &__meta { &:where(:hover) { - color: var(--stk-subtitle-color-hover, cssvar(subtitle-color)); + color: var(--stk-subtitle-color-hover, cssvar(subtitle-color)); } } } diff --git a/src/util/admin.js b/src/util/admin.js index 846fd5ef8f..f10afd2a9a 100644 --- a/src/util/admin.js +++ b/src/util/admin.js @@ -13,6 +13,7 @@ import apiFetch from '@wordpress/api-fetch' // them by type. export const importBlocks = r => { const blocks = {} + const blockDependencies = {} r.keys().forEach( key => { const meta = r( key ) const type = meta[ 'stk-type' ] @@ -36,11 +37,16 @@ export const importBlocks = r => { } ) } } ) + + if ( meta[ 'stk-block-dependency' ] ) { + blockDependencies[ meta.name ] = meta[ 'stk-block-dependency' ] + } } ) + Object.keys( blocks ).forEach( type => { blocks[ type ] = sortBy( blocks[ type ], 'name' ) } ) - return blocks + return [ blocks, blockDependencies ] } let fetchingPromise = null diff --git a/src/welcome/admin.js b/src/welcome/admin.js index fc25f7522a..938323510f 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -42,7 +42,7 @@ import { BLOCK_STATE } from '~stackable/util/blocks' import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin' import blockData from '~stackable/deprecated/v2/welcome/blocks' -const FREE_BLOCKS = importBlocks( require.context( '../block', true, /block\.json$/ ) ) +const [ FREE_BLOCKS, BLOCK_DEPENDENCIES ] = importBlocks( require.context( '../block', true, /block\.json$/ ) ) export const getAllBlocks = () => applyFilters( 'stackable.settings.blocks', FREE_BLOCKS ) @@ -272,6 +272,17 @@ const getParentBlocks = blockName => { return parents } +export const getBlockTitle = name => { + for ( const category in DERIVED_BLOCKS ) { + for ( const block of DERIVED_BLOCKS[ category ] ) { + if ( block.name === name ) { + return block.title + } + } + } + return name +} + const BlockList = () => { const DERIVED_BLOCKS = getAllBlocks() return ( @@ -333,25 +344,14 @@ const RestSettingsNotice = () => { // Confirmation dialog when disabling a block that is dependent on another block. const ToggleBlockDialog = ( { - blockName, + blockName = '', blockList, isDisabled, onConfirm, onCancel, + hideHeader = false, + customText = '', } ) => { - const DERIVED_BLOCKS = getAllBlocks() - - const getBlockTitle = name => { - for ( const category in DERIVED_BLOCKS ) { - for ( const block of DERIVED_BLOCKS[ category ] ) { - if ( block.name === name ) { - return block.title - } - } - } - return name - } - const blockTitle = getBlockTitle( blockName ) return ( @@ -362,10 +362,13 @@ const ToggleBlockDialog = ( { ? sprintf( __( 'Disable %s block?', i18n ), blockTitle ) : sprintf( __( 'Enable %s block?', i18n ), blockTitle ) } onRequestClose={ onCancel } + __experimentalHideHeader={ hideHeader } > - { isDisabled - ?

    { __( 'Disabling this block will also disable these blocks that require this block to function:', i18n ) }

    // eslint-disable-line @wordpress/i18n-no-variables - :

    { __( 'Enabling this block will also enable these blocks that are needed for this block to function:', i18n ) }

    // eslint-disable-line @wordpress/i18n-no-variables + { hideHeader ? customText + : ( isDisabled + ?

    { __( 'Disabling this block will also disable these blocks that require this block to function:', i18n ) }

    // eslint-disable-line @wordpress/i18n-no-variables + :

    { __( 'Enabling this block will also enable these blocks that are needed for this block to function:', i18n ) }

    // eslint-disable-line @wordpress/i18n-no-variables + ) }
      { blockList.map( ( block, i ) => ( @@ -445,6 +448,7 @@ const Sidenav = ( { onKeyDown={ () => handleTabChange( id ) } role="tab" tabIndex={ 0 } + id={ `stk-tab__${ id }` } > { label } @@ -502,6 +506,7 @@ const Settings = () => { const [ currentTab, setCurrentTab ] = useState( 'editor-settings' ) const [ currentSearch, setCurrentSearch ] = useState( '' ) const [ isSaving, setIsSaving ] = useState( false ) + const [ isFetching, setIsFetching ] = useState( false ) const [ isRecentlySaved, setIsRecentlySaved ] = useState( false ) const [ hasV2Tab, setHasV2Tab ] = useState( false ) @@ -536,10 +541,12 @@ const Settings = () => { }, [ unsavedChanges, settings ] ) useEffect( () => { + setIsFetching( true ) fetchSettings().then( response => { setSettings( response ) // Should only be set initially since we have to reload after setting for it to work with the backend setHasV2Tab( hasV2Compatibility( response ) ) + setIsFetching( false ) } ) }, [] ) @@ -591,6 +598,7 @@ const Settings = () => { handleSettingsChange, filteredSearchTree, currentTab, + isFetching, } return <> @@ -879,6 +887,7 @@ const Blocks = props => { settings, handleSettingsChange, filteredSearchTree, + isFetching, } = props const BLOCK_STATE_MAP = Object.freeze( { @@ -896,6 +905,33 @@ const Blocks = props => { const [ currentToggleBlock, setCurrentToggleBlock ] = useState( '' ) const [ currentToggleBlockList, setCurrentToggleBlockList ] = useState( [] ) + const [ blocksToEnable, setBlocksToEnable ] = useState( [] ) + + useEffect( () => { + // eslint-disable-next-line @wordpress/no-global-event-listener + window.addEventListener( 'message', ev => { + if ( typeof ev.data !== 'object' || ev.data.source !== 'STK_DESIGN_LIBRARY' || ev.origin !== window.location.origin ) { + return + } + + if ( ev.data.type === 'STK_ENABLE_BLOCKS' && ev.data.blocks instanceof Set && ev.data.blocks.size ) { + const blocks = [ ...ev.data.blocks ].map( block => { + if ( block in BLOCK_DEPENDENCIES ) { + return BLOCK_DEPENDENCIES[ block ] + } + return block + } ) + setBlocksToEnable( blocks ) + } + } ) + }, [] ) + + useEffect( () => { + if ( ! isFetching && blocksToEnable.length ) { + setIsEnabledDialogOpen( true ) + } + }, [ isFetching, blocksToEnable ] ) + // Map string states to integer block states const mapStringStates = states => { return states?.map( @@ -989,11 +1025,11 @@ const Blocks = props => { handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase } - const handleEnableDialogConfirm = () => { + const handleEnableDialogConfirm = ( blockList = currentToggleBlockList ) => { setIsEnabledDialogOpen( false ) const newDisabledBlocks = { ...disabledBlocks } delete newDisabledBlocks[ currentToggleBlock ] - currentToggleBlockList.forEach( block => { + blockList.forEach( block => { delete newDisabledBlocks[ block ] } ) handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase @@ -1001,26 +1037,42 @@ const Blocks = props => { return ( <> - { isDisabledDialogOpen && currentToggleBlockList && ( + { isDisabledDialogOpen && currentToggleBlockList.length !== 0 && ( handleDisableDialogConfirm() } onCancel={ () => { setIsDisabledDialogOpen( false ) } } /> ) } - { isEnabledDialogOpen && currentToggleBlockList && ( + { isEnabledDialogOpen && currentToggleBlockList.length !== 0 && ( handleEnableDialogConfirm() } + onCancel={ () => { + setIsEnabledDialogOpen( false ) + } } + /> + ) } + { isEnabledDialogOpen && blocksToEnable.length !== 0 && ( + { + handleEnableDialogConfirm( blocksToEnable ) + setBlocksToEnable( [] ) + } } onCancel={ () => { setIsEnabledDialogOpen( false ) + setBlocksToEnable( [] ) } } /> ) }