diff --git a/src/block/card/variations.js b/src/block/card/variations.js index a93871140..dc83b855a 100644 --- a/src/block/card/variations.js +++ b/src/block/card/variations.js @@ -11,6 +11,7 @@ import ImageFaded from './images/faded.svg' * External dependencies */ import { i18n, isPro } from 'stackable' +import { substituteIfDisabled } from '~stackable/util' /** * WordPress dependencies @@ -105,7 +106,7 @@ const variations = applyFilters( bottom: 0, left: 0, }, - }, [ + }, [ substituteIfDisabled( [ 'stackable/button-group|social-buttons', 'stackable/button-group|icon-button' ], [ 'stackable/button-group', { columnSpacing: { top: 0, @@ -132,6 +133,15 @@ const variations = applyFilters( className: 'is-style-plain', } ], ] ], + [ 'core/social-links', + { + align: 'right', + }, + [ + [ 'core/social-link', { service: 'facebook' } ], + [ 'core/social-link', { service: 'twitter' } ], + ], + ] ), ] ], ] ], ], diff --git a/src/block/icon-box/edit.js b/src/block/icon-box/edit.js index bbc3f1613..62ef457b7 100644 --- a/src/block/icon-box/edit.js +++ b/src/block/icon-box/edit.js @@ -33,6 +33,7 @@ import { useBlockContext } from '~stackable/hooks' import { withBlockAttributeContext, withBlockWrapperIsHovered, withQueryLoopContext, } from '~stackable/higher-order' +import { substituteCoreIfDisabled } from '~stackable/util' /** * WordPress dependencies @@ -49,9 +50,9 @@ export const TEMPLATE = [ text: __( 'Icon Box', i18n ), hasP: true, textTag: 'h4', } ], ] ], - [ 'stackable/text', { + substituteCoreIfDisabled( 'stackable/text', { text: 'Description for this block. Use this space for describing your block.', - } ], + } ), ] const Edit = props => { diff --git a/src/block/notification/variations.js b/src/block/notification/variations.js index 84a7cb1c5..f031d5940 100644 --- a/src/block/notification/variations.js +++ b/src/block/notification/variations.js @@ -2,6 +2,7 @@ * External dependencies */ import { i18n, isPro } from 'stackable' +import { removeChildIfDisabled } from '~stackable/util' /** * WordPress dependencies @@ -37,7 +38,7 @@ const variations = applyFilters( pickerTitle: __( 'Default', i18n ), pickerIcon: ImageDefault, isActive: [ 'className' ], - innerBlocks: [ + innerBlocks: removeChildIfDisabled( 'stackable/icon', [ [ 'stackable/icon', { icon: '', iconColor1: '#FFFFFF', @@ -62,7 +63,7 @@ const variations = applyFilters( }, } ], ] ], - ], + ] ), scope: [ 'block' ], }, { @@ -83,7 +84,7 @@ const variations = applyFilters( pickerTitle: __( 'Plain', i18n ), pickerIcon: ImagePlain, isActive: [ 'className' ], - innerBlocks: [ + innerBlocks: removeChildIfDisabled( 'stackable/icon', [ [ 'stackable/icon', { icon: '', iconColor1: 'var(--stk-container-background-color, #40ba7b)', @@ -111,7 +112,7 @@ const variations = applyFilters( }, } ], ] ], - ], + ] ), scope: [ 'block' ], }, { diff --git a/src/block/team-member/variations.js b/src/block/team-member/variations.js index 9f0ec028e..cea8faf19 100644 --- a/src/block/team-member/variations.js +++ b/src/block/team-member/variations.js @@ -2,6 +2,7 @@ * Internal dependencies */ import { i18n, isPro } from 'stackable' +import { substituteIfDisabled } from '~stackable/util' /** * WordPress dependencies @@ -53,6 +54,14 @@ import ImageCover from './images/cover.svg' ], ] +const socialLinksInnerBlocks = [ + [ 'core/social-link', { service: 'facebook' } ], + [ 'core/social-link', { service: 'twitter' } ], + [ 'core/social-link', { service: 'instagram' } ], + [ 'core/social-link', { service: 'youtube' } ], + [ 'core/social-link', { service: 'linkedin' } ], +] + const variations = applyFilters( 'stackable.team-member.variations', [ @@ -77,7 +86,10 @@ const variations = applyFilters( text: __( 'Position', i18n ), } ], [ 'stackable/text', { text: _x( 'Description for this block. Use this space for describing your block. Any text will do. Description for this block. You can use this space for describing your block.', 'Content placeholder', i18n ) } ], - [ 'stackable/button-group', {}, buttonGroupInnerBlocks ], + substituteIfDisabled( [ 'stackable/button-group|social-buttons', 'stackable/button-group|icon-button' ], + [ 'stackable/button-group', {}, buttonGroupInnerBlocks ], + [ 'core/social-links', { align: 'center' }, socialLinksInnerBlocks ], + ), ], scope: [ 'block' ], }, @@ -98,7 +110,10 @@ const variations = applyFilters( [ 'stackable/subtitle', { text: __( 'Position', i18n ), } ], - [ 'stackable/button-group', {}, buttonGroupInnerBlocks ], + substituteIfDisabled( [ 'stackable/button-group|social-buttons', 'stackable/button-group|icon-button' ], + [ 'stackable/button-group', {}, buttonGroupInnerBlocks ], + [ 'core/social-links', { align: 'center' }, socialLinksInnerBlocks ], + ), ], scope: [ 'block' ], }, diff --git a/src/block/timeline/edit.js b/src/block/timeline/edit.js index b5edcf3f3..e895076c6 100644 --- a/src/block/timeline/edit.js +++ b/src/block/timeline/edit.js @@ -42,6 +42,7 @@ import { withQueryLoopContext, } from '~stackable/higher-order' import { range } from 'lodash' +import { substituteCoreIfDisabled } from '~stackable/util' /** * WordPress dependencies @@ -68,9 +69,9 @@ const TEMPLATE = [ left: 0, }, }, [ - [ 'stackable/text', { + substituteCoreIfDisabled( 'stackable/text', { text: _x( 'Description for this block. Use this space for describing your block. Any text will do.', 'Content placeholder', i18n ), - } ], + } ), ] ], ] diff --git a/src/blocks.js b/src/blocks.js index 6bdc8e1ee..2671227d7 100644 --- a/src/blocks.js +++ b/src/blocks.js @@ -13,7 +13,10 @@ import './disabled-blocks' * External dependencies */ import { i18n } from 'stackable' -import { addStackableBlockCategory, registerBlockType } from '~stackable/util' +import { + addStackableBlockCategory, + registerBlockType, +} from '~stackable/util' import { withVisualGuideContext } from '~stackable/higher-order' /** @@ -45,7 +48,7 @@ const importAllAndRegister = r => { settings.keywords = settings.keywords.map( keyword => __( keyword, i18n ) ) // eslint-disable-line @wordpress/i18n-no-variables } - // Register the block. + // Register the block if it's not already registered and not disabled. if ( ! getBlockType( name ) ) { registerBlockType( name, settings ) } diff --git a/src/components/admin-base-setting/index.js b/src/components/admin-base-setting/index.js index 21d78b9e6..bb143a41c 100644 --- a/src/components/admin-base-setting/index.js +++ b/src/components/admin-base-setting/index.js @@ -5,11 +5,14 @@ let i = 1 const AdminBaseSetting = props => { const [ uid ] = useState( `ugb-admin-setting-${ i++ }` ) + const isSearched = props.searchTerm && props.label.toLowerCase().includes( props.searchTerm ) const mainClasses = classnames( [ 'ugb-admin-setting', props.className, ], { [ `ugb-admin-setting--${ props.size }` ]: props.size, + 'ugb-admin-setting--highlight': isSearched, + 'ugb-admin-setting--not-highlight': props.searchTerm && ! isSearched, } ) return ( diff --git a/src/components/admin-toolbar-setting/editor.scss b/src/components/admin-toolbar-setting/editor.scss new file mode 100644 index 000000000..2ec733bf0 --- /dev/null +++ b/src/components/admin-toolbar-setting/editor.scss @@ -0,0 +1,14 @@ +.ugb-admin-toolbar-setting__wrapper { + display: flex; + a { + margin-right: 10px; + width: 80px; + } + .ugb-admin-toolbar-setting { + display: flex; + .ugb-button-component { + padding: 14px; + width: 100%; + } + } +} diff --git a/src/components/admin-toolbar-setting/index.js b/src/components/admin-toolbar-setting/index.js new file mode 100644 index 000000000..d50575eaf --- /dev/null +++ b/src/components/admin-toolbar-setting/index.js @@ -0,0 +1,74 @@ +import AdminBaseSetting from '../admin-base-setting' +import Button from '../button' +import { ButtonGroup } from '@wordpress/components' +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' + +const AdminToolbarSetting = props => { + return ( + +
+ ev.stopPropagation() } + > + { __( 'view demo', i18n ) } + + { + const isSelected = props.value ? props.value === option.value : props.placeholder === option.value + const tabindex = isSelected ? '0' : '-1' + + return
+
+ ) +} + +AdminToolbarSetting.defaultProps = { + controls: [], + label: '', + value: '', + onChange: () => {}, +} + +export default AdminToolbarSetting diff --git a/src/disabled-blocks.js b/src/disabled-blocks.js index 039350262..9b32297ac 100644 --- a/src/disabled-blocks.js +++ b/src/disabled-blocks.js @@ -1,40 +1,63 @@ /** - * Filter that modified the metadata of the blocks to disable blocks and + * Filter that modified the metadata of the blocks to hide blocks and * variations depending on the settings of the user. */ import { settings } from 'stackable' import { addFilter } from '@wordpress/hooks' +import { + BLOCK_STATE, + BLOCK_DEPENDENCIES, + substituteCoreIfDisabled, +} from '~stackable/util' +import _ from 'lodash' -// Disable these blocks when the following variables are disabled. We need this -// so that if a variation is disabled, we will no longer be able to add the -// relevant block. -const BLOCK_DEPENDENCIES = { - 'stackable/icon-button': 'stackable/button-group|icon-button', - 'stackable/button': 'stackable/button-group|button', +// Contains the hookname of block variations and a list of whitelisted block names to substitute. +const VARIATION_FILTERS_WHITELIST = { + 'stackable.accordion.variations': [ 'stackable/text' ], + 'stackable.card.variations': [ 'stackable/heading', 'stackable/text', 'stackable/subtitle', 'stackable/button-group', 'stackable/button' ], + 'stackable.image-box.variations': [ 'stackable/heading', 'stackable/text' ], + 'stackable.notification.variations': [ 'stackable/heading', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.blockquote.variations': [ 'stackable/text' ], + 'stackable.call-to-action.variations': [ 'stackable/heading', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.feature.variations': [ 'stackable/heading', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.feature-grid.variations': [ 'stackable/image', 'stackable/heading', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.hero.variations': [ 'stackable/heading', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.pricing-box.variations': [ 'stackable/heading', 'stackable/subtitle', 'stackable/button-group', 'stackable/button' ], + 'stackable.team-member.variations': [ 'stackable/image', 'stackable/heading', 'stackable/subtitle', 'stackable/text', 'stackable/button-group', 'stackable/button' ], + 'stackable.testimonial.variations': [ 'stackable/image', 'stackable/heading', 'stackable/subtitle', 'stackable/text' ], } const getDefaultVariation = variations => { return variations?.find( ( { isDefault } ) => isDefault )?.name } const getVariationsToRemove = ( disabledBlocks, blockName ) => { - return disabledBlocks.filter( disabledBlock => disabledBlock.startsWith( `${ blockName }|` ) ) - .map( disabledBlock => disabledBlock.split( '|' )[ 1 ] ) + const variations = [] + for ( const block in disabledBlocks ) { + if ( block.startsWith( `${ blockName }|` ) ) { + variations.push( block.split( '|' )[ 1 ] ) + } + } + return variations } const applySettingsToMeta = metadata => { - let inserter = ! settings.stackable_disabled_blocks.includes( metadata.name ) + const disabledBlocks = settings.stackable_disabled_blocks || {} // eslint-disable-line camelcase + let inserter = true + + // If the block is hidden, set the inserter to false. + if ( metadata.name in disabledBlocks ) { + inserter = ! disabledBlocks[ metadata.name ] === BLOCK_STATE.HIDDEN + } // Check if this block is dependent on another variation being enabled. - if ( BLOCK_DEPENDENCIES[ metadata.name ] ) { - if ( settings.stackable_disabled_blocks.includes( BLOCK_DEPENDENCIES[ metadata.name ] ) ) { - inserter = false - } + if ( BLOCK_DEPENDENCIES[ metadata.name ] && BLOCK_DEPENDENCIES[ metadata.name ] in disabledBlocks ) { + inserter = ! disabledBlocks[ BLOCK_DEPENDENCIES[ metadata.name ] ] === BLOCK_STATE.HIDDEN } - const variationsToRemove = getVariationsToRemove( settings.stackable_disabled_blocks, metadata.name ) + const variationsToRemove = getVariationsToRemove( disabledBlocks, metadata.name ) let variations = metadata.variations || [] - // Remove variations if there are ones disabled. + // Remove the variations that are hidden which removes the block from the inserter. if ( variationsToRemove.length ) { const hasDefaultVariation = !! getDefaultVariation( metadata.variations ) variations = variations.filter( variation => ! variationsToRemove.includes( variation.name ) ) @@ -64,3 +87,36 @@ const applySettingsToMeta = metadata => { } addFilter( 'stackable.block.metadata', 'stackable/disabled-blocks', applySettingsToMeta ) + +// Traverse the innerblocks of a given block definition and substitute core blocks if disabled and whitelisted. +const traverseBlocksAndSubstitute = ( blocks, whitelist ) => { + return blocks.map( block => { + let [ blockName, blockAttributes, innerBlocks ] = block + + // If there are innerBlocks, recursively traverse them. + if ( innerBlocks && innerBlocks.length > 0 ) { + innerBlocks = traverseBlocksAndSubstitute( innerBlocks, whitelist ) + } + + if ( whitelist.includes( blockName ) ) { + return substituteCoreIfDisabled( blockName, blockAttributes, innerBlocks ) + } + + if ( innerBlocks ) { + return [ blockName, blockAttributes, innerBlocks ] + } + return [ blockName, blockAttributes ] + } ) +} + +Object.entries( VARIATION_FILTERS_WHITELIST ).forEach( ( [ hookName, whitelist ] ) => { + // Make sure to run after pro filters + addFilter( hookName, 'stackable/disabled-blocks', blockVariations => { + return blockVariations.map( variation => { + const newVariation = _.cloneDeep( variation ) + newVariation.innerBlocks = traverseBlocksAndSubstitute( newVariation.innerBlocks, whitelist ) + return newVariation + } ) + }, 11 ) +} ) + diff --git a/src/editor-settings.php b/src/editor-settings.php index 072499d06..fd89194af 100644 --- a/src/editor-settings.php +++ b/src/editor-settings.php @@ -33,16 +33,19 @@ public function register_settings() { register_setting( 'stackable_editor_settings', 'stackable_disabled_blocks', + // Use an object to store the block names as keys and the value that represents if disabled or hidden. + // Enabled blocks are not stored in the object to save memory. array( - 'type' => 'array', + 'type' => 'object', 'description' => __( 'Blocks that should be hidden in the block editor', STACKABLE_I18N ), 'sanitize_callback' => array( $this, 'sanitize_array_setting' ), 'show_in_rest' => array( 'schema' => array( - 'items' => array( - 'type' => 'string', - ) - ) + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'number', + ), + ), ), 'default' => array(), ) @@ -120,6 +123,18 @@ public function register_settings() { ) ); + register_setting( + 'stackable_editor_settings', + 'stackable_enable_global_settings', + array( + 'type' => 'boolean', + 'description' => __( 'Allow the configuration of global settings such as color palette, typography, and block defaults', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + register_setting( 'stackable_editor_settings', 'stackable_enable_block_linking', @@ -154,6 +169,66 @@ public function register_settings() { 'default' => true, ) ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_text_highlight', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for highlighting text', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_dynamic_content', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for inserting dynamic content', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_copy_paste_styles', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for copying and pasting block styles', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_reset_layout', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for resetting the layout of a block', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_save_as_default_block', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for saving the current block variation as the default block', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); register_setting( 'stackable_editor_settings', @@ -184,9 +259,16 @@ public function add_settings( $settings ) { $settings['stackable_enable_design_library'] = get_option( 'stackable_enable_design_library' ); $settings['stackable_optimize_inline_css'] = get_option( 'stackable_optimize_inline_css' ); $settings['stackable_auto_collapse_panels'] = get_option( 'stackable_auto_collapse_panels' ); + $settings['stackable_enable_global_settings'] = get_option( 'stackable_enable_global_settings' ); $settings['stackable_enable_block_linking'] = get_option( 'stackable_enable_block_linking' ); $settings['stackable_enable_carousel_lazy_loading'] = get_option( 'stackable_enable_carousel_lazy_loading' ); + $settings['stackable_enable_text_highlight'] = get_option( 'stackable_enable_text_highlight' ); + $settings['stackable_enable_dynamic_content'] = get_option( 'stackable_enable_dynamic_content' ); + $settings['stackable_enable_copy_paste_styles'] = get_option( 'stackable_enable_copy_paste_styles' ); + $settings['stackable_enable_reset_layout'] = get_option( 'stackable_enable_reset_layout' ); + $settings['stackable_enable_save_as_default_block'] = get_option( 'stackable_enable_save_as_default_block' ); $settings['stackable_enable_text_default_block'] = get_option( 'stackable_enable_text_default_block' ); + return $settings; } diff --git a/src/format-types/highlight/index.js b/src/format-types/highlight/index.js index 3106e6988..bd13cbc15 100644 --- a/src/format-types/highlight/index.js +++ b/src/format-types/highlight/index.js @@ -5,7 +5,7 @@ import { ColorPaletteControl, AdvancedToolbarControl, Popover, } from '~stackable/components' import { whiteIfDarkBlackIfLight } from '~stackable/util' -import { i18n } from 'stackable' +import { i18n, settings } from 'stackable' /** * WordPress dependencies @@ -256,17 +256,19 @@ const HighlightButton = props => { ) } -registerFormatType( - 'stk/highlight', { - title: __( 'Highlight Text', i18n ), - tagName: 'span', - className: 'stk-highlight', - edit: HighlightButton, - attributes: { - style: 'style', - }, - } -) +if ( settings.stackable_enable_toolbar_text_highlight ) { + registerFormatType( + 'stk/highlight', { + title: __( 'Highlight Text', i18n ), + tagName: 'span', + className: 'stk-highlight', + edit: HighlightButton, + attributes: { + style: 'style', + }, + } + ) +} // Backward compatibility, ugb/highlight, but this is not visible. registerFormatType( diff --git a/src/plugins/global-settings/index.js b/src/plugins/global-settings/index.js index 733d335f7..015ee9e6f 100644 --- a/src/plugins/global-settings/index.js +++ b/src/plugins/global-settings/index.js @@ -8,7 +8,11 @@ import './block-defaults' * External dependencies */ import { SVGStackableIcon } from '~stackable/icons' -import { i18n, isContentOnlyMode } from 'stackable' +import { + i18n, + isContentOnlyMode, + settings, +} from 'stackable' /** WordPress dependencies */ @@ -59,7 +63,7 @@ const GlobalSettings = () => { ) } -if ( ! isContentOnlyMode ) { +if ( ! isContentOnlyMode && settings.stackable_enable_global_settings ) { registerPlugin( 'stackable-global-settings', { render: GlobalSettings, } ) diff --git a/src/plugins/layout-picker-reset/index.js b/src/plugins/layout-picker-reset/index.js index eae9c27e2..236a54baf 100644 --- a/src/plugins/layout-picker-reset/index.js +++ b/src/plugins/layout-picker-reset/index.js @@ -1,7 +1,9 @@ /** * External dependencies */ -import { i18n, isContentOnlyMode } from 'stackable' +import { + i18n, isContentOnlyMode, settings, +} from 'stackable' // import { Button } from '~stackable/components' /** @@ -55,7 +57,7 @@ if ( ! isContentOnlyMode ) { return ( <> - { hasVariations && hasLayoutReset && ( + { settings.stackable_enable_reset_layout && hasVariations && hasLayoutReset && ( ) } diff --git a/src/plugins/save-block/index.js b/src/plugins/save-block/index.js index a73cc3b75..bb8c594de 100644 --- a/src/plugins/save-block/index.js +++ b/src/plugins/save-block/index.js @@ -6,6 +6,7 @@ import './variation-picker' import './custom-block-styles-editor' import SaveMenu from './save-menu' import { useSavedDefaultBlockStyle } from '~stackable/hooks' +import { settings } from 'stackable' /** * WordPress dependencies @@ -25,7 +26,9 @@ const _SaveMenu = withSelect( select => { } } )( SaveMenu ) -registerPlugin( 'stackable-save-block-menu', { render: _SaveMenu } ) +if ( settings.stackable_enable_save_as_default_block ) { + registerPlugin( 'stackable-save-block-menu', { render: _SaveMenu } ) +} /** * Add the block style loader to each Stackable block. diff --git a/src/util/blocks.js b/src/util/blocks.js index ccdcda278..8dcbb4e76 100644 --- a/src/util/blocks.js +++ b/src/util/blocks.js @@ -13,7 +13,11 @@ import { orderBy, last, } from 'lodash' -import { blockCategoryIndex, i18n } from 'stackable' +import { + blockCategoryIndex, + i18n, + settings as stackableSettings, +} from 'stackable' /** * WordPress dependencies @@ -33,6 +37,23 @@ import { useMemo } from '@wordpress/element' import { BlockIcon } from '@wordpress/block-editor' import { __ } from '@wordpress/i18n' +/** + * Enum for disabling and hiding blocks. + */ +export const BLOCK_STATE = Object.freeze( { + ENABLED: 1, + HIDDEN: 2, + DISABLED: 3, +} ) + +/** + * Block dependencies. If a block is hidden/disabled, the block it depends on will also be one. + */ +export const BLOCK_DEPENDENCIES = { + 'stackable/icon-button': 'stackable/button-group|icon-button', + 'stackable/button': 'stackable/button-group|button', +} + /** * Converts the registered block name into a block name string that can be used in hook names or ids. * @@ -476,6 +497,14 @@ export const addStackableBlockCategory = () => { * @param {Object} _settings The block properties to register */ export const registerBlockType = ( name, _settings ) => { + // Do not register the block if the block is disabled. + if ( ( BLOCK_DEPENDENCIES[ name ] in stackableSettings.stackable_disabled_blocks && + stackableSettings.stackable_disabled_blocks[ BLOCK_DEPENDENCIES[ name ] ] === BLOCK_STATE.DISABLED ) || + stackableSettings.stackable_disabled_blocks[ name ] === BLOCK_STATE.DISABLED + ) { + return + } + let settings = applyFilters( `stackable.block.metadata`, _settings || {} ) // If there is no variation title, then some labels in the editor will show @@ -504,3 +533,116 @@ export const registerBlockType = ( name, _settings ) => { settings = applyFilters( `stackable.${ name.replace( 'stackable/', '' ) }.settings`, settings ) _registerBlockType( name, settings ) } + +/** + * Substitutes a stackable block with an equivalent core block if the block is disabled. + * + * @param {string} blockName The block name + * @param {Object} blockAttributes The block attributes + * @param {Array} children The children blocks + * + * @return {Array} The resulting block definition + */ +export const substituteCoreIfDisabled = ( blockName, blockAttributes, children ) => { + const disabled_blocks = stackableSettings.stackable_disabled_blocks || {} // eslint-disable-line camelcase + + if ( blockName === 'stackable/text' ) { + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/paragraph', { + content: blockAttributes.text, + } ] + } + return [ 'stackable/text', blockAttributes ] + } + + if ( blockName === 'stackable/heading' ) { + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/heading', { + content: blockAttributes.text, + level: blockAttributes.textTag ? Number( blockAttributes.textTag.replace( 'h', '' ) ) : 2, + } ] + } + return [ 'stackable/heading', { ...blockAttributes } ] + } + + if ( blockName === 'stackable/subtitle' ) { + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/paragraph', { + content: blockAttributes.text, + } ] + } + return [ 'stackable/subtitle', blockAttributes ] + } + + if ( blockName === 'stackable/button-group' ) { + if ( 'stackable/button-group|button' in disabled_blocks && disabled_blocks[ 'stackable/button-group|button' ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/buttons', {}, children ] + } + return [ 'stackable/button-group', blockAttributes, children ] + } + + if ( blockName === 'stackable/button' ) { + if ( 'stackable/button-group|button' in disabled_blocks && disabled_blocks[ 'stackable/button-group|button' ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/button', { + text: blockAttributes.text, + } ] + } + return [ 'stackable/button', blockAttributes ] + } + + if ( blockName === 'stackable/image' ) { + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + if ( blockAttributes ) { + return [ 'core/image', { + height: blockAttributes.imageHeight, + } ] + } + return [ 'core/image' ] + } + return [ 'stackable/image', blockAttributes ] + } + + if ( children ) { + return [ blockName, blockAttributes, children ] + } + return [ blockName, blockAttributes ] +} + +/** + * Substitutes a block definition with another block definition if any of the block names given are disabled. + * + * @param {Array} blockNames The block names to check if disabled + * @param {Array} originalBlockDefinition The original block definition + * @param {Array} substituteBlockDefinition The block definition to substitute with + * + * @return {Array} The resulting block definition + */ +export const substituteIfDisabled = ( blockNames, originalBlockDefinition, substituteBlockDefinition ) => { + const disabled_blocks = stackableSettings.stackable_disabled_blocks || {} // eslint-disable-line camelcase + + for ( const blockName of blockNames ) { + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return substituteBlockDefinition + } + } + + return originalBlockDefinition +} + +/** + * Remove a given child block from a block tree definition if disabled + * + * @param {Array} blockName The block name of the child to remove + * @param {Array} blockTree The array that contains the child block + * + * @return {Array} The resulting block tree definition + */ +export const removeChildIfDisabled = ( blockName, blockTree ) => { + const disabled_blocks = stackableSettings.stackable_disabled_blocks || {} // eslint-disable-line camelcase + + if ( blockName in disabled_blocks && disabled_blocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return blockTree.filter( innerBlock => innerBlock[ 0 ] !== blockName ) + } + + return blockTree +} diff --git a/src/welcome/admin.js b/src/welcome/admin.js index 42d6987e2..42a154fe3 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -11,7 +11,7 @@ import SVGSectionIcon from './images/settings-icon-section.svg' */ import { __ } from '@wordpress/i18n' import { - useEffect, useState, Fragment, useCallback, + useEffect, useState, useCallback, useRef, useMemo, lazy, Suspense, } from '@wordpress/element' import domReady from '@wordpress/dom-ready' import { Spinner, CheckboxControl } from '@wordpress/components' @@ -24,6 +24,7 @@ import { applyFilters } from '@wordpress/hooks' import { i18n, showProNoticesOption, + isPro, } from 'stackable' import classnames from 'classnames' import { importBlocks } from '~stackable/util/admin' @@ -31,9 +32,12 @@ import { createRoot } from '~stackable/util/element' import AdminSelectSetting from '~stackable/components/admin-select-setting' import AdminToggleSetting from '~stackable/components/admin-toggle-setting' import AdminTextSetting from '~stackable/components/admin-text-setting' +import AdminToolbarSetting from '~stackable/components/admin-toolbar-setting' import { GettingStarted } from './getting-started' +import { BLOCK_STATE } from '~stackable/util/blocks' const FREE_BLOCKS = importBlocks( require.context( '../block', true, /block\.json$/ ) ) + export const getAllBlocks = () => applyFilters( 'stackable.settings.blocks', FREE_BLOCKS ) export const BLOCK_CATEROGIES = [ @@ -57,6 +61,70 @@ export const BLOCK_CATEROGIES = [ }, ] +const BLOCK_DEPENDENCIES = { + 'stackable/accordion': [ + 'stackable/icon-label', + 'stackable/heading', + 'stackable/icon', + ], + 'stackable/button-group|social-buttons': [ + 'stackable/button-group|icon-button', + ], + 'stackable/expand': [ + 'stackable/text', + 'stackable/button-group|button', + ], + 'stackable/icon-label': [ + 'stackable/icon', + 'stackable/heading', + ], + 'stackable/image-box': [ + 'stackable/image', + 'stackable/subtitle', + 'stackable/icon', + ], + 'stackable/price': [ + 'stackable/text', + ], + 'stackable/video-popup': [ + 'stackable/icon', + 'stackable/image', + ], + 'stackable/blockquote': [ + 'stackable/icon', + ], + 'stackable/feature': [ + 'stackable/image', + ], + 'stackable/icon-box': [ + 'stackable/icon-label', + 'stackable/icon', + 'stackable/heading', + ], + 'stackable/pricing-box': [ + 'stackable/price', + 'stackable/text', + 'stackable/icon-list', + ], + 'stackable/testimonial': [ + 'stackable/image-box', + ], +} + +const getChildrenBlocks = blockname => { + return BLOCK_DEPENDENCIES[ blockname ] || [] +} + +const getParentBlocks = blockName => { + const parents = [] + for ( const parent in BLOCK_DEPENDENCIES ) { + if ( BLOCK_DEPENDENCIES[ parent ].includes( blockName ) ) { + parents.push( parent ) + } + } + return parents +} + const BlockList = () => { const DERIVED_BLOCKS = getAllBlocks() return ( @@ -92,549 +160,944 @@ const BlockList = () => { ) } -const BlockToggler = () => { - const DERIVED_BLOCKS = getAllBlocks() - const [ isSaving, setIsSaving ] = useState( false ) - const [ disabledBlocks, setDisabledBlocks ] = useState( [] ) +// Create an admin notice if there's an error fetching the settings. +const RestSettingsNotice = () => { + const [ error, setError ] = useState( null ) useEffect( () => { loadPromise.then( () => { const settings = new models.Settings() - settings.fetch().then( response => { - setDisabledBlocks( response.stackable_disabled_blocks ) + settings.fetch().catch( error => { + setError( error ) } ) } ) }, [] ) - const save = ( disabledBlocks, type ) => { - setIsSaving( type ) - const model = new models.Settings( { stackable_disabled_blocks: disabledBlocks } ) // eslint-disable-line camelcase - model.save().then( () => setIsSaving( false ) ) - } - - const enableAllBlocks = type => () => { - let newDisabledBlocks = [ ...disabledBlocks ] - DERIVED_BLOCKS[ type ].forEach( block => { - newDisabledBlocks = newDisabledBlocks.filter( blockName => blockName !== block.name ) - } ) - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) + if ( ! error ) { + return null } - const disableAllBlocks = type => () => { - const newDisabledBlocks = [ ...disabledBlocks ] - DERIVED_BLOCKS[ type ].forEach( block => { - if ( ! newDisabledBlocks.includes( block.name ) ) { - newDisabledBlocks.push( block.name ) + return ( +
+

{ __( 'Error getting Stackable settings. We got the following error. Please contact your administrator.', i18n ) }

+ { error.responseJSON && +

{ error.responseJSON.data.status } ({ error.responseJSON.code }). { error.responseJSON.message }

} - } ) - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) - } - - const toggleBlock = useCallback( ( name, type ) => { - let newDisabledBlocks = null - if ( disabledBlocks.includes( name ) ) { - newDisabledBlocks = disabledBlocks.filter( block => block !== name ) - } else { - newDisabledBlocks = [ - ...disabledBlocks, - name, - ] - } - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) - }, [ setDisabledBlocks, disabledBlocks ] ) +
+ ) +} +// Confirmation dialog when disabling a block that is dependent on another block. +const ToggleBlockDialog = ( { + blockName, + blockList, + isDisabled, + onConfirm, + onCancel, +} ) => { return ( - <> - { BLOCK_CATEROGIES.map( ( { - id, label, Icon, - } ) => { - const classes = classnames( [ - 's-box-block__title', - `s-box-block__title--${ id }`, - ] ) - return ( -
-

- { Icon && } - { label } -

-
- { isSaving === id && } - - -
-
- { DERIVED_BLOCKS[ id ].map( ( block, i ) => { - const isDisabled = disabledBlocks.includes( block.name ) - - const demoLink = block[ 'stk-demo' ] && ( - ev.stopPropagation() } - > - { __( 'view demo', i18n ) } - - ) - - const title = <> - { __( block.title, i18n ) } { /* eslint-disable-line @wordpress/i18n-no-variables */ } - { demoLink } - - - return ( - { - toggleBlock( block.name, id ) - } } - size="small" - disabled={ __( 'Disabled', i18n ) } - enabled={ __( 'Enabled', i18n ) } - /> - ) - } ) } -
-
- ) - } ) } - +
+
+ { isDisabled + ?

{ __( 'Disabling ' + blockName + ' will also disable the blocks that require it:', i18n ) }

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

{ __( 'Enabling ' + blockName + ' will also enable its required innerblocks:', i18n ) }

// eslint-disable-line @wordpress/i18n-no-variables + } +
    + { blockList.map( ( block, i ) => ( +
  • { block }
  • + ) ) } +
+ { isDisabled + ?

{ __( 'Are you sure you want to disable this block?', i18n ) }

+ :

{ __( 'Are you sure you want to enable this block?', i18n ) }

+ } +
+ + +
+
+
) } -// Implement pick without using lodash, because themes and plugins might remove -// lodash from the admin. -const pick = ( obj, keys ) => { - return keys.reduce( ( acc, key ) => { - if ( obj && obj.hasOwnProperty( key ) ) { - acc[ key ] = obj[ key ] - } - return acc - }, {} ) -} +// Side navigation with the save changes button and search on tabs +const Sidenav = ( { + currentTab, + handleTabChange, + handleSettingsSave, + currentSearch, + isSaving, +} ) => { + const tabList = useMemo( () => [ + { + id: 'editor-settings', + label: __( 'Editor Settings', i18n ), + settings: [ + 'Nested Block Width', + 'Nested Wide Block Width', + 'Stackable Text as Default Block', + 'Design Library', + 'Block Linking (Beta)', + 'Toolbar Text Highlight', + 'Toolbar Dynamic Content', + 'Copy & Paste Styles', + 'Reset Layout', + 'Save as Default Block', + 'Dont show help video tooltips', + 'Auto-Collapse Panels', + ], + }, + { + id: 'responsiveness', + label: __( 'Responsiveness', i18n ), + settings: [ + 'Tablet Breakpoint', + 'Mobile Breakpoint', + ], + }, + { + id: 'blocks', + label: __( 'Blocks', i18n ), + settings: BLOCK_CATEROGIES.map( ( { id } ) => { + const DERIVED_BLOCKS = getAllBlocks() + return DERIVED_BLOCKS[ id ].map( block => { + return block.name.split( '/' )[ 1 ] + } ) + } ).flat(), + }, + { + id: 'optimizations', + label: __( 'Optimization', i18n ), + settings: [ + 'Optimize Inline CSS', + 'Lazy Load Images within Carousels', + ], + }, + { + id: 'global-settings', + label: __( 'Global Settings', i18n ), + settings: [ + 'Force Typography Styles', + ], + }, + { + id: 'role-manager', + label: __( 'Role Manager', i18n ), + settings: [ + 'Role Manager', + 'Administrator', + 'Editor', + 'Author', + 'Contributor', + 'Subscriber', + ], + }, + { + id: 'custom-fields-settings', + label: __( 'Custom Fields', i18n ), + settings: [ + 'Custom Fields', + 'Administrator', + 'Editor', + 'Author', + 'Contributor', + 'Subscriber', + ], + }, + { + id: 'integrations', + label: __( 'Integration', i18n ), + settings: [ + 'Google Maps API Key', + 'FontAwesome Pro Kit', + 'FontAwesome Icon Library Version', + ], + }, + { + id: 'other-settings', + label: __( 'Miscellaneous ', i18n ), + settings: [ + 'Migration', + 'Show Go premium notices', + 'Generate Global Colors for native blocks', + 'Load version 2 blocks in the editor', + 'Load version 2 blocks in the editor only when the page was using version 2 blocks', + 'Load version 2 frontend block stylesheet and scripts for backward compatibility', + ], + }, + ], [] ) -// Create an admin notice if there's an error fetching the settings. -const SettingsNotice = () => { - const [ error, setError ] = useState( null ) + const sidenavRef = useRef( null ) useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().catch( error => { - setError( error ) - } ) - } ) + const handleScroll = () => { + const header = document.querySelector( '.s-header-settings' ) + + if ( header ) { + // If the header is scrolled out of view, make the sidebar fixed + if ( header.getBoundingClientRect().bottom <= 32 ) { + sidenavRef.current.classList.add( 's-sidenav-fixed' ) + } else { + sidenavRef.current.classList.remove( 's-sidenav-fixed' ) + } + } + } + window.addEventListener( 'scroll', handleScroll ) + return () => window.removeEventListener( 'scroll', handleScroll ) }, [] ) - if ( ! error ) { - return null - } + return ( + <> + + + ) +} +const Searchbar = ( { currentSearch, handleSearchChange } ) => { + const handleSearch = e => { + handleSearchChange( e.target.value.toLowerCase() ) + } return ( -
-

{ __( 'Error getting Stackable settings. We got the following error. Please contact your administrator.', i18n ) }

- { error.responseJSON && -

{ error.responseJSON.data.status } ({ error.responseJSON.code }). { error.responseJSON.message }

- } +
+
) } -const EditorSettings = () => { +// Main settings component +const Settings = () => { const [ settings, setSettings ] = useState( {} ) - const [ isBusy, setIsBusy ] = useState( false ) - const [ saveTimeout, setSaveTimeout ] = useState( null ) + const [ unsavedChanges, setUnsavedChanges ] = useState( {} ) + const [ currentTab, setCurrentTab ] = useState( 'editor-settings' ) + const [ currentSearch, setCurrentSearch ] = useState( '' ) + const [ isSaving, setIsSaving ] = useState( false ) + + const handleSettingsChange = useCallback( newSettings => { + setSettings( prev => ( { ...prev, ...newSettings } ) ) + setUnsavedChanges( prev => ( { ...prev, ...newSettings } ) ) + }, [] ) + + const handleSettingsSave = useCallback( () => { + if ( Object.keys( unsavedChanges ).length === 0 ) { + return + } + setIsSaving( true ) + const model = new models.Settings( unsavedChanges ) + model.save().then( () => { + // Add a little more time for the spinner for better feedback + setTimeout( () => { + setIsSaving( false ) + }, 500 ) + } ) + setUnsavedChanges( {} ) + }, [ unsavedChanges, settings ] ) useEffect( () => { loadPromise.then( () => { const settings = new models.Settings() settings.fetch().then( response => { - setSettings( pick( response, [ - 'stackable_google_maps_api_key', - 'stackable_enable_design_library', - 'stackable_optimize_inline_css', - 'stackable_block_default_width', - 'stackable_block_wide_width', - 'stackable_auto_collapse_panels', - 'stackable_enable_block_linking', - 'stackable_enable_carousel_lazy_loading', - 'stackable_enable_text_default_block', - ] ) ) + setSettings( response ) } ) } ) }, [] ) + const props = { + settings, + handleSettingsChange, + currentSearch, + } + return <> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_design_library: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_design_library: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Adds a button on the top of the editor which gives access to a collection of pre-made block designs.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_optimize_inline_css: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_optimize_inline_css: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Optimize inlined CSS styles. If this is enabled, similar selectors will be combined together, helpful if you changed Block Defaults.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_auto_collapse_panels: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_auto_collapse_panels: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Collapse other inspector panels when opening another, keeping only one open at a time.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_block_linking: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_block_linking: value, // eslint-disable-line camelcase - } ) - } } - help={ - <> - { __( 'Gives you the ability to link columns. Any changes you make on one column will automatically get applied on the other columns.', i18n ) } -   - { __( 'Learn more', i18n ) } - - } - /> - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_block_default_width: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { stackable_block_default_width: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - }, 400 ) ) - } } - help={ __( 'The width used when a Columns block has its Content Width set to center. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } - > - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_block_wide_width: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { stackable_block_wide_width: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - }, 400 ) ) - } } - help={ __( 'The width used when a Columns block has its Content Width set to wide. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } - /> - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_google_maps_api_key: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( - setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { - stackable_google_maps_api_key: value, // eslint-disable-line camelcase - } ) - model.save().then( () => setIsBusy( false ) ) - }, 400 ) - ) - } } - help={ - <> - { __( - 'Adding a Google API Key enables additional features of the Stackable Map Block.', - i18n - ) } -   - { __( 'Learn more', i18n ) } - - } + - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_carousel_lazy_loading: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_carousel_lazy_loading: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Disable this if you encounter layout or spacing issues when using images inside carousel-type blocks because of image lazy loading.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_text_default_block: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_text_default_block: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'If enabled, Stackable Text blocks will be added by default instead of the native Paragraph Block.', i18n ) } - /> - { isBusy && -
- -
- } +
+ + { currentTab === 'editor-settings' && } + { currentTab === 'responsiveness' && } + { currentTab === 'blocks' && } + { currentTab === 'optimizations' && } + { currentTab === 'global-settings' && } + { currentTab === 'role-manager' && } + { currentTab === 'custom-fields-settings' && } + { currentTab === 'integrations' && } + { currentTab === 'other-settings' && } +
} -const DynamicBreakpointsSettings = () => { - const [ tabletBreakpoint, setTabletBreakpoint ] = useState( '' ) - const [ mobileBreakpoint, setMobileBreakpoint ] = useState( '' ) - const [ isReady, setIsReady ] = useState( false ) - const [ isBusy, setIsBusy ] = useState( false ) +const EditorSettings = props => { + const { + settings, + handleSettingsChange, + currentSearch, + } = props - useEffect( () => { - setIsBusy( true ) - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - const breakpoints = response.stackable_dynamic_breakpoints - if ( breakpoints ) { - setTabletBreakpoint( breakpoints.tablet || '' ) - setMobileBreakpoint( breakpoints.mobile || '' ) + return ( +
+

{ __( 'Blocks', i18n ) }

+

{ __( 'You can customize the behavior of some blocks here.', i18n ) }

+ { + handleSettingsChange( { stackable_block_default_width: value } ) // eslint-disable-line camelcase + } } + help={ __( 'The width used when a Columns block has its Content Width set to center. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } + /> + { + handleSettingsChange( { stackable_block_wide_width: value } ) // eslint-disable-line camelcase + } } + help={ __( 'The width used when a Columns block has its Content Width set to wide. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_text_default_block: value } ) // eslint-disable-line camelcase + } } + help={ __( 'If enabled, Stackable Text blocks will be added by default instead of the native Paragraph Block.', i18n ) } + /> + +

{ __( 'Editor', i18n ) }

+

{ __( 'You can customize some of the features and behavior of Stackable in the editor here.' ) }

+ { + handleSettingsChange( { stackable_enable_design_library: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a button on the top of the editor which gives access to a collection of pre-made block designs.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_global_settings: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a button on the top of the editor which gives access to Stackable settings.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_block_linking: value } ) // eslint-disable-line camelcase + } } + help={ + <> + { __( 'Gives you the ability to link columns. Any changes you make on one column will automatically get applied on the other columns.', i18n ) } +   + { __( 'Learn more', i18n ) } + } - setIsReady( true ) - setIsBusy( false ) - } ) - } ) - }, [] ) + /> - useEffect( () => { - if ( isReady ) { - const t = setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { - stackable_dynamic_breakpoints: { // eslint-disable-line camelcase - tablet: tabletBreakpoint, - mobile: mobileBreakpoint, - }, - } ) - model.save().then( () => setIsBusy( false ) ) - }, 400 ) - return () => clearTimeout( t ) - } - }, [ tabletBreakpoint, mobileBreakpoint, isReady ] ) +

{ __( 'Toolbar', i18n ) }

+

{ __( 'You can disable some toolbar features here.', i18n ) }

+ { + handleSettingsChange( { stackable_enable_text_highlight: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for highlighting text', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_dynamic_content: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for inserting dynamic content', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_copy_paste_styles: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for copying and pasting block styles', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_reset_layout: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for resetting the layout of a stackble block back to the original', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_save_as_default_block: value } ) // eslint-disable-line + } } + help={ __( 'Adds a toolbar button for saving a block as the default block', i18n ) } + /> + +

{ __( 'Inspector', i18n ) }

+

{ __( 'You can customize some of the features and behavior of Stackable in the inspector here.' ) }

+ { + handleSettingsChange( { stackable_help_tooltip_disabled: value ? '1' : '' } ) // eslint-disable-line camelcase + } } + help={ __( 'Disables the help video tooltips that appear in the inspector.', i18n ) } + /> + { + handleSettingsChange( { stackable_auto_collapse_panels: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Collapse other inspector panels when opening another, keeping only one open at a time.', i18n ) } + /> +
+ ) +} + +const Responsiveness = props => { + const { + settings, + handleSettingsChange, + currentSearch, + } = props - return -
+ return ( +
+

{ __( 'Dynamic Breakpoints', i18n ) }

+

+ { __( 'Blocks can be styles differently for tablet and mobile screens, and some styles adjust to make them fit better in smaller screens. You can change the widths when tablet and mobile views are triggered. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

setTabletBreakpoint( value ) } + value={ settings.stackable_dynamic_breakpoints.tablet || '' } // eslint-disable-line camelcase + onChange={ value => { + handleSettingsChange( { + stackable_dynamic_breakpoints: { // eslint-disable-line camelcase + tablet: value, + mobile: settings.stackable_dynamic_breakpoints.mobile || '', // eslint-disable-line camelcase + }, + } ) + } } placeholder="1024" > px setMobileBreakpoint( value ) } + value={ settings.stackable_dynamic_breakpoints.mobile || '' } // eslint-disable-line camelcase + onChange={ value => { + handleSettingsChange( { + stackable_dynamic_breakpoints: { // eslint-disable-line camelcase + tablet: settings.stackable_dynamic_breakpoints.tablet || '', // eslint-disable-line camelcase + mobile: value, + }, + } ) + } } placeholder="768" > px
- { isBusy && -
- -
- } - + ) } -const GlobalSettings = () => { - const [ forceTypography, setForceTypography ] = useState( false ) +// Toggle the block states between enabled, disabled and hidden. +// Enabled blocks are not stored in the settings object. +const Blocks = props => { + const { + settings, + handleSettingsChange, + currentSearch, + } = props - useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setForceTypography( !! response.stackable_global_force_typography ) + const DERIVED_BLOCKS = getAllBlocks() + const disabledBlocks = settings.stackable_disabled_blocks ?? {} // eslint-disable-line camelcase + + const [ isDisabledDialogOpen, setIsDisabledDialogOpen ] = useState( false ) + const [ isEnabledDialogOpen, setIsEnabledDialogOpen ] = useState( false ) + const [ currentToggleBlock, setCurrentToggleBlock ] = useState( '' ) + const [ currentToggleBlockList, setCurrentToggleBlockList ] = useState( [] ) + + const enableAllBlocks = () => { + handleSettingsChange( { stackable_disabled_blocks: {} } ) // eslint-disable-line camelcase + } + + const disableAllBlocks = () => { + const newDisabledBlocks = {} + BLOCK_CATEROGIES.forEach( ( { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + newDisabledBlocks[ block.name ] = BLOCK_STATE.DISABLED } ) } ) - }, [] ) + handleSettingsChange( { stackable_disabled_blocks: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const hideAllBlocks = () => { + const newDisabledBlocks = {} + BLOCK_CATEROGIES.forEach( ( { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + newDisabledBlocks[ block.name ] = BLOCK_STATE.HIDDEN + } ) + } ) + handleSettingsChange( { stackable_disabled_blocks: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const toggleBlock = ( name, value ) => { + const valueInt = Number( value ) + let newDisabledBlocks = { ...disabledBlocks } + + setCurrentToggleBlock( name ) + + // Check if a parent is being enabled + if ( valueInt === BLOCK_STATE.ENABLED ) { + // Get the parent's children and confirm if they will also be enabled + const childrenBlocks = getChildrenBlocks( name ) + if ( childrenBlocks.length > 0 ) { + setCurrentToggleBlockList( childrenBlocks ) + setIsEnabledDialogOpen( true ) + } else { + delete newDisabledBlocks[ name ] + } + } else if ( valueInt === BLOCK_STATE.DISABLED ) { // Check if a child is being disabled + // Get the child's parents and confirm if they will also be disabled + const parentBlocks = getParentBlocks( name ) + if ( parentBlocks.length > 0 ) { + setCurrentToggleBlockList( parentBlocks ) + setIsDisabledDialogOpen( true ) + } else { + newDisabledBlocks = { ...disabledBlocks, [ name ]: valueInt } + } + } else { + newDisabledBlocks = { ...disabledBlocks, [ name ]: valueInt } + } + handleSettingsChange( { stackable_disabled_blocks: newDisabledBlocks } ) // eslint-disable-line camelcase + } - const updateForceTypography = value => { - const model = new models.Settings( { stackable_global_force_typography: value } ) // eslint-disable-line camelcase - model.save() - setForceTypography( value ) + const handleDisableDialogConfirm = () => { + setIsDisabledDialogOpen( false ) + const newDisabledBlocks = { ...disabledBlocks, [ currentToggleBlock ]: BLOCK_STATE.DISABLED } + currentToggleBlockList.forEach( block => { + newDisabledBlocks[ block ] = BLOCK_STATE.DISABLED + } ) + handleSettingsChange( { stackable_disabled_blocks: newDisabledBlocks } ) // eslint-disable-line camelcase } - return + const handleEnableDialogConfirm = () => { + setIsEnabledDialogOpen( false ) + const newDisabledBlocks = { ...disabledBlocks } + delete newDisabledBlocks[ currentToggleBlock ] + currentToggleBlockList.forEach( block => { + delete newDisabledBlocks[ block ] + } ) + handleSettingsChange( { stackable_disabled_blocks: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + return ( + <> + { isDisabledDialogOpen && ( + { + setIsDisabledDialogOpen( false ) + } } + /> + ) } + + { isEnabledDialogOpen && ( + { + setIsEnabledDialogOpen( false ) + } } + /> + ) } + +
+

{ __( 'Blocks', i18n ) }

+

{ __( 'You can enable, hide and disable Stackable blocks. Hiding the blocks hides them from the editor. Disabling the blocks prevent them from being loaded for faster performance.', i18n ) }

+
+ + + +
+ { BLOCK_CATEROGIES.map( ( { + id, label, Icon, + } ) => { + const classes = classnames( [ + 's-box-block__title', + `s-box-block__title--${ id }`, + ] ) + return ( +
+

+ { Icon && } + { label } +

+
+ { DERIVED_BLOCKS[ id ].map( ( block, i ) => { + const blockState = disabledBlocks[ block.name ] ?? BLOCK_STATE.ENABLED + + return ( + { + toggleBlock( block.name, value ) + } } + isSmall={ true } + /> + ) + } ) } +
+
+ ) + } ) } +
+ + ) +} + +const Optimizations = props => { + const { + settings, + handleSettingsChange, + currentSearch, + } = props + + return ( +
+

{ __( 'Optimizations', i18n ) }

+ { + handleSettingsChange( { stackable_optimize_inline_css: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Optimize inlined CSS styles. If this is enabled, similar selectors will be combined together, helpful if you changed Block Defaults.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_carousel_lazy_loading: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Disable this if you encounter layout or spacing issues when using images inside carousel-type blocks because of image lazy loading.', i18n ) } + /> +
+ ) +} + +const GlobalSettings = props => { + return <> +

{ __( 'Global Settings', i18n ) }

{ + props.handleSettingsChange( { stackable_global_force_typography: value } ) // eslint-disable-line camelcase + } } disabled={ __( 'Not forced', i18n ) } enabled={ __( 'Force styles', i18n ) } /> -
+ } -const IconSettings = () => { - const [ faVersion, setFaVersion ] = useState( '' ) +const EditorModeSettings = lazy( () => import( '../../pro__premium_only/src/welcome/editor-mode' ) ) - useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setFaVersion( response.stackable_icons_fa_free_version || '6.5.1' ) - } ) - } ) - }, [] ) +const RoleManager = props => { + return <> +

{ __( '📰 Role Manager', i18n ) }

+

+ { __( 'Lock the Block Editor\'s inspector for different user roles, and give clients edit access to only images and content. Content Editing Mode affects all blocks. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

+ { isPro + ? }> +
+ +
+
+ :

+ { __( 'This is only available in Stackable Premium. ', i18n ) } + + { __( 'Go Premium', i18n ) } + +

+ } + +} - const updateFaVersion = value => { - const model = new models.Settings( { stackable_icons_fa_free_version: value } ) // eslint-disable-line camelcase - model.save() - setFaVersion( value ) - } +const CustomFieldsEnableSettings = lazy( () => import( '../../pro__premium_only/src/welcome/custom-fields-toggle' ) ) +const CustomFieldsManagerSettings = lazy( () => import( '../../pro__premium_only/src/welcome/custom-fields-roles' ) ) - return -
- +const CustomFields = props => { + return <> +
+

{ __( '📋 Custom Fields', i18n ) }

+ { isPro && + }> +
+ +
+
+ }
-
- + { __( 'Create Custom Fields that you can reference across your entire site. You can assign which roles can manage your Custom Fields. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

+ { isPro + ? }> +
+ +
+
+ :

+ { __( 'This is only available in Stackable Premium. ', i18n ) } + + { __( 'Go Premium', i18n ) } + +

+ } + +} + +const IconSettings = lazy( () => import( '../../pro__premium_only/src/welcome/icons.js' ) ) + +const Integrations = props => { + return ( +
+

{ __( 'Integrations', i18n ) }

+ { + props.handleSettingsChange( { stackable_google_maps_api_key: value } ) // eslint-disable-line camelcase + } } + help={ + <> + { __( + 'Adding a Google API Key enables additional features of the Stackable Map Block.', + i18n + ) } +   + { __( 'Learn more', i18n ) } + + } /> + { isPro + ? }> +
+ +
+
+ : <> +
+ +
+
+

+ { __( 'FontAwesome Pro Kit', i18n ) } + { __( 'This is only available in Stackable Premium. ', i18n ) } + + { __( 'Go Premium', i18n ) } + +

+
+ + } +
+
+ +
+
+ { + props.handleSettingsChange( { stackable_icons_fa_free_version: value } ) // eslint-disable-line camelcase + } } + /> +
+
- + ) } const AdditionalOptions = props => { - const [ helpTooltipsDisabled, setHelpTooltipsDisabled ] = useState( false ) - const [ generateNativeGlobalColors, setGenerateNativeGlobalColors ] = useState( false ) - const [ v2EditorBackwardCompatibility, setV2EditorBackwardCompatibility ] = useState( false ) - const [ v2EditorBackwardCompatibilityUsage, setV2EditorBackwardCompatibilityUsage ] = useState( false ) - const [ v2FrontendBackwardCompatibility, setV2FrontendBackwardCompatibility ] = useState( false ) - const [ showPremiumNotices, setShowPremiumNotices ] = useState( false ) - const [ isBusy, setIsBusy ] = useState( false ) - - useEffect( () => { - setIsBusy( true ) - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setHelpTooltipsDisabled( response.stackable_help_tooltip_disabled === '1' ) - setGenerateNativeGlobalColors( !! response.stackable_global_colors_native_compatibility ) - setV2EditorBackwardCompatibility( response.stackable_v2_editor_compatibility === '1' ) - setV2EditorBackwardCompatibilityUsage( response.stackable_v2_editor_compatibility_usage === '1' ) - setV2FrontendBackwardCompatibility( response.stackable_v2_frontend_compatibility === '1' ) - setShowPremiumNotices( response.stackable_show_pro_notices === '1' ) - setIsBusy( false ) - } ) - } ) - }, [] ) + const { + settings, + handleSettingsChange, + currentSearch, + } = props - const updateSetting = settings => { - setIsBusy( true ) - const model = new models.Settings( settings ) - model.save().then( () => setIsBusy( false ) ) + const searchClassname = label => { + return currentSearch && ( + label.toLowerCase().includes( currentSearch ) + ? 'components-base-control--highlight' + : 'components-base-control--not-highlight' + ) } return ( -
- { props.showProNoticesOption && +
+

{ __( '🔩 Miscellaneous', i18n ) }

+ { showProNoticesOption && { - updateSetting( { stackable_show_pro_notices: checked ? '1' : '' } ) // eslint-disable-line camelcase - setShowPremiumNotices( checked ) + handleSettingsChange( { stackable_show_pro_notices: checked ? '1' : '' } ) // eslint-disable-line camelcase } } /> } - { - updateSetting( { stackable_help_tooltip_disabled: checked ? '1' : '' } ) // eslint-disable-line camelcase - setHelpTooltipsDisabled( checked ) - } } - /> { - updateSetting( { stackable_global_colors_native_compatibility: checked } ) // eslint-disable-line camelcase - setGenerateNativeGlobalColors( checked ) + handleSettingsChange( { stackable_global_colors_native_compatibility: checked } ) // eslint-disable-line camelcase } } />

{ __( '🏠 Migration Settings', i18n ) }

@@ -645,44 +1108,39 @@ const AdditionalOptions = props => {

{ - const settings = { stackable_v2_editor_compatibility: checked ? '1' : '' } // eslint-disable-line camelcase if ( checked ) { - settings.stackable_v2_editor_compatibility_usage = '' // eslint-disable-line camelcase - setV2EditorBackwardCompatibilityUsage( false ) + handleSettingsChange( { stackable_v2_editor_compatibility_usage: '' } ) // eslint-disable-line camelcase } - updateSetting( settings ) - setV2EditorBackwardCompatibility( checked ) + handleSettingsChange( { stackable_v2_editor_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase } } /> { - const settings = { stackable_v2_editor_compatibility_usage: checked ? '1' : '' } // eslint-disable-line camelcase if ( checked ) { - settings.stackable_v2_editor_compatibility = '' // eslint-disable-line camelcase - setV2EditorBackwardCompatibility( false ) + handleSettingsChange( { stackable_v2_editor_compatibility: '' } ) // eslint-disable-line camelcase } - updateSetting( settings ) - setV2EditorBackwardCompatibilityUsage( checked ) + handleSettingsChange( { stackable_v2_editor_compatibility_usage: checked ? '1' : '' } ) // eslint-disable-line camelcase } } /> { - updateSetting( { stackable_v2_frontend_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase - setV2FrontendBackwardCompatibility( checked ) + handleSettingsChange( { stackable_v2_frontend_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase } } /> - { isBusy && -
- -
- }
) } @@ -693,79 +1151,44 @@ AdditionalOptions.defaultProps = { // Load all the options into the UI. domReady( () => { - // This is for the getting started block list. - if ( document.querySelector( '.s-getting-started__block-list' ) ) { - createRoot( - document.querySelector( '.s-getting-started__block-list' ) - ).render( - - ) - } - - // All these below are for the settings page. - if ( document.querySelector( '.s-settings-wrapper' ) ) { - createRoot( - document.querySelector( '.s-settings-wrapper' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-other-options-wrapper' ) ) { - createRoot( - document.querySelector( '.s-other-options-wrapper' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-settings-notice' ) ) { - createRoot( - document.querySelector( '.s-settings-notice' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-editor-settings' ) ) { + if ( document.querySelector( '.s-getting-started__body' ) ) { createRoot( - document.querySelector( '.s-editor-settings' ) + document.querySelector( '.s-getting-started__body' ) ).render( - + ) } - if ( document.querySelector( '.s-dynamic-breakpoints' ) ) { + // This is for the getting started block list. + if ( document.querySelector( '.s-getting-started__block-list' ) ) { createRoot( - document.querySelector( '.s-dynamic-breakpoints' ) + document.querySelector( '.s-getting-started__block-list' ) ).render( - + ) } - if ( document.querySelector( '.s-global-settings' ) ) { + if ( document.querySelector( '.s-sidenav' ) ) { createRoot( - document.querySelector( '.s-global-settings' ) + document.querySelector( '.s-sidenav' ) ).render( - + ) } - if ( document.querySelector( '.s-icon-settings-fa-version' ) ) { + if ( document.querySelector( '.s-rest-settings-notice' ) ) { createRoot( - document.querySelector( '.s-icon-settings-fa-version' ) + document.querySelector( '.s-rest-settings-notice' ) ).render( - + ) } - if ( document.querySelector( '.s-getting-started__body' ) ) { + if ( document.querySelector( '.s-content' ) ) { createRoot( - document.querySelector( '.s-getting-started__body' ) + document.querySelector( '.s-content' ) ).render( - + ) } } ) diff --git a/src/welcome/admin.scss b/src/welcome/admin.scss index 9586aa79e..3995354c9 100644 --- a/src/welcome/admin.scss +++ b/src/welcome/admin.scss @@ -8,6 +8,11 @@ --stk-welcome-light-border: #d0d5dd; } +// Make scroll smooth with inline navigation. +html { + scroll-behavior: smooth; +} + // Clear out the margins of the admin page. body[class*="page_stackable"], @@ -18,6 +23,10 @@ body[class*="page_stk-"] { #wpbody-content > .wrap:not(#fs_connect) { // stylelint-disable-line selector-id-pattern margin: 0; padding: 0 50px; + + &.wrap-settings { + padding: 0; + } } } @@ -31,6 +40,10 @@ body[class*="page_stk-"] { border-bottom: 1px solid var(--stk-welcome-light-border); font-weight: 300; + &.s-header-settings { + margin: 0; + } + .s-header { padding: 0px; position: absolute; @@ -385,6 +398,7 @@ body.toplevel_page_stk-custom-fields { margin-bottom: 30px; transition: all 0.3s ease; position: relative; + min-height: 70vh; &.s-box-spaced { padding-left: 4vw; padding-right: 4vw; @@ -397,6 +411,9 @@ body.toplevel_page_stk-custom-fields { color: #f34957; } } + &.s-box-hidden { + display: none; + } } .s-absolute-spinner { position: absolute; @@ -459,6 +476,9 @@ body.toplevel_page_stk-custom-fields { display: flex; flex-direction: column; } + &.s-body-container-with-sidenav { + margin-left: 250px; + } p, li { line-height: 1.6; @@ -476,10 +496,161 @@ body.toplevel_page_stk-custom-fields { margin-bottom: 0 !important; } } - .s-body-container-grid { - display: grid; - grid-template-columns: 1fr 350px; - grid-gap: 30px; + .s-sidenav { + min-height: 90vh; + width: 250px; + left: 0; + position: absolute; + background-color: #fff; + padding-top: 20px; + display: flex; + flex-direction: column; + justify-content: flex-start; + .s-sidenav-item { + color: #444; + font-size: 14px; + padding: 15px 20px; + text-decoration: none; + display: block; + position: relative; + width: 100%; + text-align: left; + background: none; + border: none; + transition: background-color 0.3s ease; + cursor: pointer; + &:hover { + color: var(--stk-welcome-primary) !important; + } + &.s-active { + font-weight: 600; + color: var(--stk-welcome-primary) !important; + transition: color 0.3s ease; + &::after { + content: ""; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 2px; + background-color: var(--stk-welcome-primary); + transition: width 0.3s ease; + } + } + &.s-sidenav-item-highlight { + font-weight: bold; + } + } + // This is a button, render this in the bottom of the side bar + // The side bar is a flex container, so this will be at the bottom. + .s-save-changes { + background: linear-gradient(135deg, #b300be, #f00069); + transition: all 0.1s ease-in-out; + text-decoration: none; + border: none; + color: #fff; + padding: 12px 20px; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 14px; + cursor: pointer; + box-sizing: border-box !important; + display: inline-block; + margin: 20px 20px 0; + &:hover { + opacity: 0.85; + box-shadow: none !important; + } + } + } + .s-sidenav-fixed { + position: fixed; + height: 100%; + top: 32px; + left: 160px; + .s-save-changes { + margin: 120px 20px 0; + } + } + .s-search-setting { + display: flex; + justify-content: end; + .s-search-setting__input { + margin: 0; + padding: 0.5em 1em; + width: 250px; + } + } + .ugb-admin-setting--highlight { + .ugb-admin-setting__label { + color: #333; + } + } + .components-base-control--highlight { + label { + color: #333; + } + } + .ugb-admin-setting--not-highlight { + display: none; + } + + .components-base-control--not-highlight { + display: none; + } + + .s-toggle-block-dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); // Semi-transparent black + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + .s-toggle-block-dialog-content { + background-color: #fff; + padding: 20px; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-width: 400px; + width: 100%; + ul { + display: inline-block; + text-align: left; + } + } + .s-dialog-button { + margin-top: 20px; + &.s-dialog-button-confirm { + background-color: #28a745; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-right: 10px; + transition: background-color 0.3s; + &:hover { + background-color: #218838; + } + } + &.s-dialog-button-cancel { + background-color: #dc3545; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + &:hover { + background-color: #c82333; + } + } + } } .s-side { h2, @@ -703,11 +874,15 @@ body.toplevel_page_stk-custom-fields { grid-template-columns: 1fr 1fr 1fr; } -// Collapse to a single column for mobile. @media screen and (max-width: 960px) { + // Collapse to a single column for mobile. .s-body-container { grid-template-columns: 1fr !important; } + // Matched the width of wordpress admin menu + .s-sidenav-fixed { + left: 36px !important; + } } // Save spinner for the additional options. diff --git a/src/welcome/index.php b/src/welcome/index.php index b2abfec3c..923f35426 100644 --- a/src/welcome/index.php +++ b/src/welcome/index.php @@ -174,81 +174,31 @@ public static function print_premium_button() { ?> public function stackable_settings_content() { ?> -
-
+
+
print_header() ?> print_premium_button() ?> print_tabs() ?>

-
-
+
+
+
+
+
+
-
-
-
-
-

-

-
-
-
-

-

', '' ) ?>

-
-
-
-

-

', '' ) ?>

-
-
-
-

-
-
- can_use_premium_code() ) : ?> -

- -
-
-

- can_use_premium_code() ) : ?> -

' . __( 'Learn more', STACKABLE_I18N ) . '' ?>

- - can_use_premium_code() ) : ?> -

' . __( 'Learn more', STACKABLE_I18N ) . '' ?>

- -
- can_use_premium_code() ) : ?> -

- -
-
-
-

-
-
-

', '' ) ?>

-
- can_use_premium_code() ) : ?> -

- -
-
-

-

- -
-
+ +
-
+

-
+ + + +