first
This commit is contained in:
@ -0,0 +1,342 @@
|
||||
import './style.scss';
|
||||
|
||||
import { Button, ToggleControl } from '@wordpress/components';
|
||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import ControlsRender from '../controls-render';
|
||||
import StepsWizard from './steps-wizard';
|
||||
|
||||
const { plugin_name: pluginName } = window.VPGutenbergVariables;
|
||||
|
||||
const NOTICE_LIMIT = parseInt(
|
||||
window.VPGutenbergVariables.items_count_notice_limit,
|
||||
10
|
||||
);
|
||||
|
||||
function renderControls(props, category) {
|
||||
return <ControlsRender {...props} category={category} isSetupWizard />;
|
||||
}
|
||||
|
||||
function hasLayoutElement(element, attributes) {
|
||||
const { layout_elements: layoutElements } = attributes;
|
||||
const checkIn = element === 'filter' ? 'top' : 'bottom';
|
||||
|
||||
return (
|
||||
typeof layoutElements[checkIn] !== 'undefined' &&
|
||||
layoutElements[checkIn]?.elements.includes(element)
|
||||
);
|
||||
}
|
||||
|
||||
function toggleLayoutElement(element, attributes) {
|
||||
const { layout_elements: layoutElements } = attributes;
|
||||
const checkIn = element === 'filter' ? 'top' : 'bottom';
|
||||
|
||||
if (
|
||||
typeof layoutElements[checkIn] === 'undefined' ||
|
||||
!layoutElements[checkIn]?.elements
|
||||
) {
|
||||
return layoutElements;
|
||||
}
|
||||
|
||||
const result = JSON.parse(JSON.stringify(layoutElements));
|
||||
|
||||
if (hasLayoutElement(element, attributes)) {
|
||||
result[checkIn].elements = [];
|
||||
} else {
|
||||
result[checkIn].elements = [element];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function SetupWizard(props) {
|
||||
const { attributes, setAttributes } = props;
|
||||
const {
|
||||
align,
|
||||
content_source: contentSource,
|
||||
items_click_action: clickAction,
|
||||
layout,
|
||||
images,
|
||||
} = attributes;
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [allowNextStep, setAllowNextStep] = useState(false);
|
||||
const maxSteps = 2.5;
|
||||
|
||||
// Add startup attributes.
|
||||
useEffect(() => {
|
||||
if (!align && !contentSource) {
|
||||
setAttributes({ align: 'wide' });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Set some starter attributes for different content sources.
|
||||
// And hide the setup wizard.
|
||||
const setStarterAttributes = useCallback(() => {
|
||||
let newAttributes = {};
|
||||
|
||||
switch (contentSource) {
|
||||
case 'images':
|
||||
// Hide setup wizard once user select images.
|
||||
if (images && images.length) {
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
items_count: -1,
|
||||
items_click_action: 'popup_gallery',
|
||||
};
|
||||
|
||||
// Add infinite scroll to the gallery when user adds a lot of images.
|
||||
if (layout !== 'slider' && images.length > NOTICE_LIMIT) {
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
items_count: NOTICE_LIMIT,
|
||||
layout_elements: {
|
||||
top: {
|
||||
elements: [],
|
||||
align: 'center',
|
||||
},
|
||||
items: {
|
||||
elements: ['items'],
|
||||
},
|
||||
bottom: {
|
||||
elements: ['pagination'],
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
pagination: 'infinite',
|
||||
pagination_hide_on_end: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'post-based':
|
||||
case 'social-stream':
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
layout_elements: {
|
||||
top: {
|
||||
elements: [],
|
||||
align: 'center',
|
||||
},
|
||||
items: {
|
||||
elements: ['items'],
|
||||
},
|
||||
bottom: {
|
||||
elements: ['pagination'],
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
// Prepare better default settings for Popup.
|
||||
// We can't change defaults of registered controls because it may break existing user galleries.
|
||||
// This is why we change it here, in the Setup Wizard.
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
items_click_action_popup_title_source:
|
||||
contentSource === 'post-based' ? 'title' : 'item_title',
|
||||
items_click_action_popup_description_source:
|
||||
contentSource === 'post-based'
|
||||
? 'description'
|
||||
: 'item_description',
|
||||
items_click_action_popup_deep_link_pid: 'filename',
|
||||
};
|
||||
|
||||
setAttributes(newAttributes);
|
||||
setAllowNextStep(true);
|
||||
}, [contentSource, images, layout, setAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentSource) {
|
||||
setStarterAttributes();
|
||||
}
|
||||
// We have to check for contentSource change here because we don't want to run this on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contentSource]);
|
||||
|
||||
return (
|
||||
<div className={`vpf-setup-wizard vpf-setup-wizard-step-${step}`}>
|
||||
<StepsWizard step={step}>
|
||||
{/* Step 0: Content Source */}
|
||||
<StepsWizard.Step>
|
||||
<div className="vpf-setup-wizard-title">{pluginName}</div>
|
||||
<div
|
||||
className="vpf-setup-wizard-description"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: __(
|
||||
'Set the common settings in the setup wizard,<br />more options will be available in the block settings.<br />Select the Content Source first:',
|
||||
'visual-portfolio'
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{renderControls(props, 'content-source')}
|
||||
{renderControls(props, 'content-source-images')}
|
||||
{renderControls(props, 'content-source-social-stream')}
|
||||
</StepsWizard.Step>
|
||||
|
||||
{/* Step 1: Items Style */}
|
||||
<StepsWizard.Step>
|
||||
<div className="vpf-setup-wizard-title">
|
||||
{__('Items Style', 'visual-portfolio')}
|
||||
</div>
|
||||
<div
|
||||
className="vpf-setup-wizard-description"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: __(
|
||||
'Select one of the featured styles to get started.<br />More style settings will be available in the block settings.',
|
||||
'visual-portfolio'
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{renderControls(props, 'items-style')}
|
||||
</StepsWizard.Step>
|
||||
|
||||
{/* Step 2: Layout Elements */}
|
||||
<StepsWizard.Step>
|
||||
<div className="vpf-setup-wizard-title">
|
||||
{__('Additional Settings', 'visual-portfolio')}
|
||||
</div>
|
||||
<div className="vpf-setup-wizard-layout-elements">
|
||||
<div>
|
||||
<ToggleControl
|
||||
label={__('Filter', 'visual-portfolio')}
|
||||
checked={hasLayoutElement('filter', attributes)}
|
||||
onChange={() => {
|
||||
setAttributes({
|
||||
layout_elements: toggleLayoutElement(
|
||||
'filter',
|
||||
attributes
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ToggleControl
|
||||
label={__('Pagination', 'visual-portfolio')}
|
||||
checked={hasLayoutElement(
|
||||
'pagination',
|
||||
attributes
|
||||
)}
|
||||
onChange={() => {
|
||||
setAttributes({
|
||||
layout_elements: toggleLayoutElement(
|
||||
'pagination',
|
||||
attributes
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ToggleControl
|
||||
label={__('Popup Gallery', 'visual-portfolio')}
|
||||
checked={clickAction === 'popup_gallery'}
|
||||
onChange={() => {
|
||||
setAttributes({
|
||||
items_click_action:
|
||||
clickAction === 'popup_gallery'
|
||||
? 'url'
|
||||
: 'popup_gallery',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</StepsWizard.Step>
|
||||
</StepsWizard>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="vpf-setup-wizard-pagination">
|
||||
{contentSource ? (
|
||||
<>
|
||||
<div className="vpf-setup-wizard-pagination-button">
|
||||
<Button
|
||||
isLink
|
||||
onClick={() => {
|
||||
// Skip Setup
|
||||
if (step === 0) {
|
||||
setAttributes({
|
||||
setup_wizard: '',
|
||||
content_source:
|
||||
attributes.contentSource ||
|
||||
'images',
|
||||
});
|
||||
|
||||
// Previous Step
|
||||
} else {
|
||||
setStep(step - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{step === 0
|
||||
? __('Skip Setup', 'visual-portfolio')
|
||||
: __('Previous Step', 'visual-portfolio')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="vpf-setup-wizard-pagination-progress">
|
||||
<div
|
||||
style={{
|
||||
width: `${Math.max(
|
||||
15,
|
||||
Math.min(100, (100 * step) / maxSteps)
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="vpf-setup-wizard-pagination-button vpf-setup-wizard-pagination-button-end">
|
||||
<Button
|
||||
isPrimary
|
||||
disabled={!allowNextStep}
|
||||
onClick={() => {
|
||||
if (step === 2) {
|
||||
setAttributes({ setup_wizard: '' });
|
||||
} else {
|
||||
setStep(step + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{__('Continue', 'visual-portfolio')}
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ marginLeft: '5px' }}
|
||||
>
|
||||
<path
|
||||
d="M3 10H17"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 4L17 10L11 16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import classnames from 'classnames/dedupe';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
|
||||
|
||||
const { ResizeObserver, MutationObserver } = window;
|
||||
|
||||
function stepsWizard(props) {
|
||||
const { step, children } = props;
|
||||
|
||||
const $ref = useRef();
|
||||
const [newStep, setNewStep] = useState(step);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
const maybeUpdateHeight = useCallback(() => {
|
||||
let newHeight = 0;
|
||||
|
||||
$ref.current.childNodes.forEach(($child) => {
|
||||
const styles = window.getComputedStyle($child);
|
||||
const margin =
|
||||
parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
|
||||
|
||||
newHeight += Math.ceil($child.offsetHeight + margin);
|
||||
});
|
||||
|
||||
setHeight(`${newHeight}px`);
|
||||
}, [$ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== newStep) {
|
||||
setNewStep(step);
|
||||
}
|
||||
}, [step, newStep]);
|
||||
|
||||
useEffect(() => {
|
||||
maybeUpdateHeight();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [newStep]);
|
||||
|
||||
useEffect(() => {
|
||||
const $element = $ref.current;
|
||||
|
||||
const calculateHeight = debounce(100, () => {
|
||||
maybeUpdateHeight();
|
||||
});
|
||||
|
||||
// Resize observer is used to properly set height
|
||||
// when selected images, saved post and reloaded page.
|
||||
const resizeObserver = new ResizeObserver(calculateHeight);
|
||||
const mutationObserver = new MutationObserver(calculateHeight);
|
||||
|
||||
resizeObserver.observe($element);
|
||||
mutationObserver.observe($element, {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributeOldValue: true,
|
||||
characterDataOldValue: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [$ref.current]);
|
||||
|
||||
return typeof children[newStep] !== 'undefined' ? (
|
||||
<div
|
||||
ref={$ref}
|
||||
className={classnames(
|
||||
'vpf-component-steps-wizard',
|
||||
step !== newStep
|
||||
? `vpf-component-steps-wizard-animate-${
|
||||
newStep > step ? 'left' : 'right'
|
||||
}`
|
||||
: false
|
||||
)}
|
||||
style={height ? { height } : {}}
|
||||
>
|
||||
{children[newStep]}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
stepsWizard.Step = function (props) {
|
||||
const { children } = props;
|
||||
return children;
|
||||
};
|
||||
|
||||
export default stepsWizard;
|
@ -0,0 +1,286 @@
|
||||
@use "sass:color";
|
||||
|
||||
$brand_color: #2540cc !default;
|
||||
|
||||
.vpf-setup-wizard {
|
||||
--wp-admin-theme-color: #{$brand_color};
|
||||
|
||||
padding: 20px;
|
||||
padding-top: 50px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border: 1px solid #7e7e7e;
|
||||
border-radius: 6px;
|
||||
|
||||
// Hide default Gutenberg outline and use our own.
|
||||
.wp-block-visual-portfolio-block.is-selected:has(> &) {
|
||||
&::after {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.vpf-setup-wizard {
|
||||
border-color: $brand_color;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-title {
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-description {
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-panel {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
// Pagination.
|
||||
.vpf-setup-wizard-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: none;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-pagination-progress {
|
||||
width: 130px;
|
||||
height: 2px;
|
||||
background-color: #eaeaea;
|
||||
|
||||
> div {
|
||||
height: 2px;
|
||||
background-color: $brand_color;
|
||||
transition: 0.3s width ease;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-pagination-button {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
min-width: 100px;
|
||||
|
||||
.components-button {
|
||||
height: 34px;
|
||||
padding: 6px 20px;
|
||||
|
||||
&.is-link {
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
color: #b9b9b9;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $brand_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-button.is-primary {
|
||||
background-color: $brand_color;
|
||||
|
||||
&:disabled {
|
||||
background-color: $brand_color;
|
||||
border-color: $brand_color;
|
||||
|
||||
svg {
|
||||
color: #fff;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color.adjust($brand_color, $lightness: -10%);
|
||||
}
|
||||
|
||||
&:focus:not(:disabled) {
|
||||
box-shadow: inset 0 0 0 1px #fff, 0 0 0 1.5px $brand_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-setup-wizard-pagination-button-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// Icons Selector
|
||||
.vpf-component-icon-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.vpf-component-icon-selector-item {
|
||||
padding: 15px 20px;
|
||||
background: none !important;
|
||||
border-color: #fff;
|
||||
|
||||
svg {
|
||||
width: 44px;
|
||||
max-width: 44px;
|
||||
height: 44px;
|
||||
color: #2b2b2b;
|
||||
transition: 0.2s color;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
text-transform: initial;
|
||||
transition: 0.2s color;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
svg,
|
||||
span {
|
||||
color: #1e1e1e;
|
||||
}
|
||||
}
|
||||
|
||||
&.vpf-component-icon-selector-item-active {
|
||||
border-color: $brand_color !important;
|
||||
box-shadow: 0 0 0 1px $brand_color !important;
|
||||
|
||||
svg,
|
||||
span {
|
||||
color: $brand_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 0: Content Source Selector
|
||||
&-step-0 {
|
||||
// Gallery Control
|
||||
.vpf-component-gallery-control .vpf-component-gallery-control-items {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Items Style Selector
|
||||
&-step-1 .vpf-component-icon-selector {
|
||||
gap: 20px;
|
||||
padding-bottom: 30px;
|
||||
margin-top: 35px;
|
||||
|
||||
// Limit to 4 items.
|
||||
> .vpf-component-icon-selector-item:nth-child(4) ~ * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vpf-component-icon-selector-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background: #eee !important;
|
||||
border: none !important;
|
||||
border-radius: 6px;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: none !important;
|
||||
box-shadow: 0 0 0 1.5px #fff, 0 0 0 3.5px #000 !important;
|
||||
}
|
||||
|
||||
&.vpf-component-icon-selector-item-active {
|
||||
border-color: none !important;
|
||||
box-shadow: 0 0 0 1.5px #fff, 0 0 0 3.5px $brand_color !important;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
opacity: 0;
|
||||
transition: 0.2s ease;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.vpf-component-icon-selector-item-active {
|
||||
> span {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Layout Elements
|
||||
&-step-2 {
|
||||
.vpf-setup-wizard-layout-elements {
|
||||
> div {
|
||||
padding: 25px 0;
|
||||
|
||||
+ div {
|
||||
border-top: 1px solid #ececec;
|
||||
}
|
||||
}
|
||||
|
||||
.components-toggle-control__label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner,
|
||||
.components-base-control,
|
||||
.components-base-control__field {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Steps Wizard.
|
||||
.vpf-component-steps-wizard {
|
||||
opacity: 1;
|
||||
transition: 0.3s opacity, 0.3s transform, 0.3s height;
|
||||
transform: translateX(0);
|
||||
|
||||
&-animate-right {
|
||||
opacity: 0;
|
||||
transition: 0.3s height;
|
||||
transform: translateX(40px);
|
||||
}
|
||||
|
||||
&-animate-left {
|
||||
opacity: 0;
|
||||
transition: 0.3s height;
|
||||
transform: translateX(-40px);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user