first
This commit is contained in:
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* WordPress.
|
||||
*/
|
||||
@import "../node_modules/@wordpress/base-styles/colors.native";
|
||||
@import "../node_modules/@wordpress/base-styles/variables";
|
||||
@import "../node_modules/@wordpress/base-styles/breakpoints";
|
||||
@import "../node_modules/@wordpress/base-styles/mixins";
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "visual-portfolio/saved",
|
||||
"category": "media",
|
||||
"title": "Visual Portfolio Saved Layout",
|
||||
"description": "Display saved Visual Portfolio layouts.",
|
||||
"keywords": ["saved", "portfolio", "vpf"],
|
||||
"textdomain": "visual-portfolio",
|
||||
"attributes": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"ghostkit": true,
|
||||
"anchor": true,
|
||||
"className": false,
|
||||
"html": false,
|
||||
"align": ["wide", "full"]
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
|
||||
import { Button, PanelBody, Placeholder, Spinner } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { ReactComponent as ElementIcon } from '../../assets/admin/images/icon-gutenberg.svg';
|
||||
import IframePreview from '../components/iframe-preview';
|
||||
import SelectControl from '../components/select-control';
|
||||
|
||||
const { plugin_name: pluginName } = window.VPGutenbergVariables;
|
||||
|
||||
/**
|
||||
* Block Edit Class.
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function BlockEdit(props) {
|
||||
const { clientId, setAttributes, attributes } = props;
|
||||
|
||||
const { id } = attributes;
|
||||
|
||||
const { portfolioLayouts } = useSelect(
|
||||
(select) => ({
|
||||
portfolioLayouts: select('visual-portfolio').getPortfolioLayouts(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
function getSelector() {
|
||||
let portfolioLayoutsSelect = false;
|
||||
let currentItemUrl = false;
|
||||
|
||||
// prepare portfolios list.
|
||||
if (portfolioLayouts) {
|
||||
portfolioLayoutsSelect = {
|
||||
'': __('--- Select Layout ---', 'visual-portfolio'),
|
||||
};
|
||||
Object.keys(portfolioLayouts).forEach((key) => {
|
||||
const val = portfolioLayouts[key];
|
||||
|
||||
portfolioLayoutsSelect[
|
||||
` ${val.id}`
|
||||
] = `${val.title} (#${val.id})`;
|
||||
|
||||
if (id && parseInt(id, 10) === val.id) {
|
||||
currentItemUrl = val.edit_url;
|
||||
}
|
||||
});
|
||||
} else if (id) {
|
||||
portfolioLayoutsSelect = {
|
||||
[` ${id}`]: `#${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!portfolioLayoutsSelect ? <Spinner /> : ''}
|
||||
{portfolioLayoutsSelect &&
|
||||
Object.keys(portfolioLayoutsSelect).length ? (
|
||||
<div className="vpf-component-layout-select">
|
||||
<SelectControl
|
||||
value={id ? ` ${id}` : ''}
|
||||
onChange={(value) =>
|
||||
setAttributes({
|
||||
id: value ? `${parseInt(value, 10)}` : '',
|
||||
})
|
||||
}
|
||||
options={portfolioLayoutsSelect}
|
||||
/>
|
||||
{currentItemUrl ? (
|
||||
<Button
|
||||
href={currentItemUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 13C11.6569 13 13 11.6569 13 10C13 8.34315 11.6569 7 10 7C8.34315 7 7 8.34315 7 10C7 11.6569 8.34315 13 10 13Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16.0545 12.4545C15.9456 12.7013 15.9131 12.9751 15.9613 13.2405C16.0094 13.5059 16.1359 13.7508 16.3245 13.9436L16.3736 13.9927C16.5258 14.1447 16.6465 14.3252 16.7288 14.5238C16.8112 14.7225 16.8536 14.9354 16.8536 15.1505C16.8536 15.3655 16.8112 15.5784 16.7288 15.7771C16.6465 15.9757 16.5258 16.1562 16.3736 16.3082C16.2217 16.4603 16.0412 16.581 15.8425 16.6634C15.6439 16.7457 15.431 16.7881 15.2159 16.7881C15.0009 16.7881 14.7879 16.7457 14.5893 16.6634C14.3906 16.581 14.2102 16.4603 14.0582 16.3082L14.0091 16.2591C13.8163 16.0705 13.5714 15.9439 13.3059 15.8958C13.0405 15.8477 12.7668 15.8802 12.52 15.9891C12.278 16.0928 12.0716 16.265 11.9263 16.4845C11.7809 16.704 11.7029 16.9613 11.7018 17.2245V17.3636C11.7018 17.7976 11.5294 18.2138 11.2225 18.5207C10.9157 18.8276 10.4994 19 10.0655 19C9.63146 19 9.21525 18.8276 8.90837 18.5207C8.60149 18.2138 8.42909 17.7976 8.42909 17.3636V17.29C8.42276 17.0192 8.3351 16.7565 8.17751 16.5362C8.01992 16.3159 7.79969 16.1481 7.54545 16.0545C7.29868 15.9456 7.02493 15.9131 6.75952 15.9613C6.4941 16.0094 6.24919 16.1359 6.05636 16.3245L6.00727 16.3736C5.8553 16.5258 5.67483 16.6465 5.47617 16.7288C5.27752 16.8112 5.06459 16.8536 4.84955 16.8536C4.6345 16.8536 4.42157 16.8112 4.22292 16.7288C4.02426 16.6465 3.84379 16.5258 3.69182 16.3736C3.53967 16.2217 3.41898 16.0412 3.33663 15.8425C3.25428 15.6439 3.21189 15.431 3.21189 15.2159C3.21189 15.0009 3.25428 14.7879 3.33663 14.5893C3.41898 14.3906 3.53967 14.2102 3.69182 14.0582L3.74091 14.0091C3.92953 13.8163 4.05606 13.5714 4.10419 13.3059C4.15231 13.0405 4.11982 12.7668 4.01091 12.52C3.90719 12.278 3.73498 12.0716 3.51547 11.9263C3.29596 11.7809 3.03873 11.7029 2.77545 11.7018H2.63636C2.20237 11.7018 1.78616 11.5294 1.47928 11.2225C1.1724 10.9157 1 10.4994 1 10.0655C1 9.63146 1.1724 9.21525 1.47928 8.90837C1.78616 8.60149 2.20237 8.42909 2.63636 8.42909H2.71C2.98081 8.42276 3.24346 8.3351 3.46379 8.17751C3.68412 8.01992 3.85195 7.79969 3.94545 7.54545C4.05437 7.29868 4.08686 7.02493 4.03873 6.75952C3.99061 6.4941 3.86408 6.24919 3.67545 6.05636L3.62636 6.00727C3.47422 5.8553 3.35352 5.67483 3.27118 5.47617C3.18883 5.27752 3.14644 5.06459 3.14644 4.84955C3.14644 4.6345 3.18883 4.42157 3.27118 4.22292C3.35352 4.02426 3.47422 3.84379 3.62636 3.69182C3.77834 3.53967 3.95881 3.41898 4.15746 3.33663C4.35611 3.25428 4.56905 3.21189 4.78409 3.21189C4.99913 3.21189 5.21207 3.25428 5.41072 3.33663C5.60937 3.41898 5.78984 3.53967 5.94182 3.69182L5.99091 3.74091C6.18374 3.92953 6.42865 4.05606 6.69406 4.10419C6.95948 4.15231 7.23322 4.11982 7.48 4.01091H7.54545C7.78745 3.90719 7.99383 3.73498 8.1392 3.51547C8.28457 3.29596 8.36259 3.03873 8.36364 2.77545V2.63636C8.36364 2.20237 8.53604 1.78616 8.84292 1.47928C9.14979 1.1724 9.56601 1 10 1C10.434 1 10.8502 1.1724 11.1571 1.47928C11.464 1.78616 11.6364 2.20237 11.6364 2.63636V2.71C11.6374 2.97328 11.7154 3.23051 11.8608 3.45002C12.0062 3.66953 12.2126 3.84174 12.4545 3.94545C12.7013 4.05437 12.9751 4.08686 13.2405 4.03873C13.5059 3.99061 13.7508 3.86408 13.9436 3.67545L13.9927 3.62636C14.1447 3.47422 14.3252 3.35352 14.5238 3.27118C14.7225 3.18883 14.9354 3.14644 15.1505 3.14644C15.3655 3.14644 15.5784 3.18883 15.7771 3.27118C15.9757 3.35352 16.1562 3.47422 16.3082 3.62636C16.4603 3.77834 16.581 3.95881 16.6634 4.15746C16.7457 4.35611 16.7881 4.56905 16.7881 4.78409C16.7881 4.99913 16.7457 5.21207 16.6634 5.41072C16.581 5.60937 16.4603 5.78984 16.3082 5.94182L16.2591 5.99091C16.0705 6.18374 15.9439 6.42865 15.8958 6.69406C15.8477 6.95948 15.8802 7.23322 15.9891 7.48V7.54545C16.0928 7.78745 16.265 7.99383 16.4845 8.1392C16.704 8.28457 16.9613 8.36259 17.2245 8.36364H17.3636C17.7976 8.36364 18.2138 8.53604 18.5207 8.84292C18.8276 9.14979 19 9.56601 19 10C19 10.434 18.8276 10.8502 18.5207 11.1571C18.2138 11.464 17.7976 11.6364 17.3636 11.6364H17.29C17.0267 11.6374 16.7695 11.7154 16.55 11.8608C16.3305 12.0062 16.1583 12.2126 16.0545 12.4545V12.4545Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{portfolioLayoutsSelect &&
|
||||
!Object.keys(portfolioLayoutsSelect).length
|
||||
? __('No saved layouts found.')
|
||||
: ''}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
return (
|
||||
<div {...blockProps}>
|
||||
<InspectorControls>
|
||||
<PanelBody>{getSelector()}</PanelBody>
|
||||
</InspectorControls>
|
||||
{id ? (
|
||||
<IframePreview
|
||||
attributes={{
|
||||
content_source: 'saved',
|
||||
id,
|
||||
}}
|
||||
clientId={clientId}
|
||||
/>
|
||||
) : (
|
||||
<Placeholder
|
||||
className="vpf-setup-wizard-saved"
|
||||
icon={<ElementIcon width="20" height="20" />}
|
||||
label={sprintf(
|
||||
__('Saved %s', 'visual-portfolio'),
|
||||
pluginName
|
||||
)}
|
||||
>
|
||||
{getSelector()}
|
||||
</Placeholder>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import './style.scss';
|
||||
|
||||
import { createBlock, registerBlockType } from '@wordpress/blocks';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import save from './save';
|
||||
import transforms from './transforms';
|
||||
|
||||
const { name, title } = metadata;
|
||||
|
||||
const settings = {
|
||||
icon: {
|
||||
foreground: '#2540CC',
|
||||
src: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0"
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="9"
|
||||
y="8"
|
||||
width="5"
|
||||
height="6"
|
||||
>
|
||||
<path
|
||||
d="M11.1409 14L13.0565 8.49994H11.2789L9.55397 14H11.1409Z"
|
||||
fill="url(#paint0_linear)"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path
|
||||
d="M11.1409 14L13.0565 8.49994H11.2789L9.55397 14H11.1409Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M8.90795 14L6.9923 8.49994H8.76989L10.4948 14H8.90795Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M19 16.2222C19 16.6937 18.8104 17.1459 18.4728 17.4793C18.1352 17.8127 17.6774 18 17.2 18H2.8C2.32261 18 1.86477 17.8127 1.52721 17.4793C1.18964 17.1459 1 16.6937 1 16.2222V3.77778C1 3.30628 1.18964 2.8541 1.52721 2.5207C1.86477 2.1873 2.32261 2 2.8 2H7.3L9.1 4.66667H17.2C17.6774 4.66667 18.1352 4.85397 18.4728 5.18737C18.8104 5.52076 19 5.97295 19 6.44444V16.2222Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1="12.191"
|
||||
y1="8.49994"
|
||||
x2="7.44436"
|
||||
y2="15.1301"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
edit,
|
||||
save,
|
||||
transforms,
|
||||
};
|
||||
|
||||
registerBlockType(name, settings);
|
||||
|
||||
// Fallback.
|
||||
registerBlockType('nk/visual-portfolio', {
|
||||
...settings,
|
||||
title,
|
||||
name: 'nk/visual-portfolio',
|
||||
attributes: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
align: {
|
||||
type: 'string',
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
},
|
||||
anchor: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
edit: (props) => {
|
||||
const { replaceBlocks } = useDispatch('core/block-editor');
|
||||
|
||||
replaceBlocks(
|
||||
[props.clientId],
|
||||
createBlock('visual-portfolio/saved', props.attributes || {})
|
||||
);
|
||||
|
||||
return null;
|
||||
},
|
||||
supports: {
|
||||
...metadata.supports,
|
||||
inserter: false,
|
||||
},
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export default function BlockSave() {
|
||||
return null;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Editor Styles
|
||||
*/
|
||||
|
||||
// Placeholder.
|
||||
.components-placeholder.vpf-setup-wizard-saved {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.components-placeholder__fieldset {
|
||||
display: block;
|
||||
flex: 0 0 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// Saved layout selector.
|
||||
.vpf-component-layout-select {
|
||||
display: flex;
|
||||
|
||||
.vpf-component-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.components-button {
|
||||
height: 32px;
|
||||
margin-left: 4px;
|
||||
|
||||
svg {
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
export default {
|
||||
from: [
|
||||
{
|
||||
type: 'shortcode',
|
||||
tag: 'visual_portfolio',
|
||||
attributes: {
|
||||
id: {
|
||||
type: 'string',
|
||||
shortcode: (data) => data.named.id,
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
shortcode: (data) => data.named.class,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "visual-portfolio/block",
|
||||
"category": "media",
|
||||
"title": "Visual Portfolio",
|
||||
"description": "Display galleries, posts and portfolio grids.",
|
||||
"keywords": ["gallery", "images", "posts", "portfolio", "vpf"],
|
||||
"textdomain": "visual-portfolio",
|
||||
"supports": {
|
||||
"ghostkit": true,
|
||||
"anchor": true,
|
||||
"className": false,
|
||||
"html": false,
|
||||
"align": ["wide", "full"]
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
import save from './save';
|
||||
|
||||
const { attributes } = window.VPGutenbergVariables;
|
||||
|
||||
const V2_23_0_ATTRIBUTES = {
|
||||
// Align.
|
||||
items_style_default__align: 'items_style_default__caption_text_align',
|
||||
items_style_fade__align: 'items_style_fade__overlay_text_align',
|
||||
items_style_fly__align: 'items_style_fly__overlay_text_align',
|
||||
items_style_emerge__align: 'items_style_emerge__caption_text_align',
|
||||
items_style_caption_move__align:
|
||||
'items_style_caption_move__caption_text_align',
|
||||
|
||||
// Color.
|
||||
items_style_default__bg_color: 'items_style_default__overlay_bg_color',
|
||||
items_style_default__text_color: 'items_style_default__overlay_text_color',
|
||||
items_style_default__meta_text_color:
|
||||
'items_style_default__caption_text_color',
|
||||
items_style_default__meta_links_color:
|
||||
'items_style_default__caption_links_color',
|
||||
items_style_default__meta_links_hover_color:
|
||||
'items_style_default__caption_links_hover_color',
|
||||
items_style_fade__bg_color: 'items_style_fade__overlay_bg_color',
|
||||
items_style_fade__text_color: 'items_style_fade__overlay_text_color',
|
||||
items_style_fly__bg_color: 'items_style_fly__overlay_bg_color',
|
||||
items_style_fly__text_color: 'items_style_fly__overlay_text_color',
|
||||
items_style_emerge__bg_color: 'items_style_emerge__caption_bg_color',
|
||||
items_style_emerge__text_color: 'items_style_emerge__caption_text_color',
|
||||
items_style_emerge__links_color: 'items_style_emerge__caption_links_color',
|
||||
items_style_emerge__links_hover_color:
|
||||
'items_style_emerge__caption_links_hover_color',
|
||||
items_style_emerge__img_overlay_bg_color:
|
||||
'items_style_emerge__overlay_bg_color',
|
||||
'items_style_caption-move__bg_color':
|
||||
'items_style_caption-move__caption_bg_color',
|
||||
'items_style_caption-move__text_color':
|
||||
'items_style_caption-move__caption_text_color',
|
||||
'items_style_caption-move__img_overlay_bg_color':
|
||||
'items_style_caption-move__overlay_bg_color',
|
||||
'items_style_caption-move__overlay_text_color':
|
||||
'items_style_caption-move__overlay_text_color',
|
||||
|
||||
// Move Under Image.
|
||||
items_style_fade__move_overlay_under_image:
|
||||
'items_style_fade__overlay_under_image',
|
||||
items_style_fly__move_overlay_under_image:
|
||||
'items_style_fly__overlay_under_image',
|
||||
items_style_emerge__move_overlay_under_image:
|
||||
'items_style_emerge__caption_under_image',
|
||||
'items_style_caption-move__move_overlay_under_image':
|
||||
'items_style_caption-move__caption_under_image',
|
||||
};
|
||||
|
||||
const V2_23_0_BORDER_RADIUS = [
|
||||
'items_style_default__images_rounded_corners',
|
||||
'items_style_fade__images_rounded_corners',
|
||||
'items_style_fly__images_rounded_corners',
|
||||
'items_style_emerge__images_rounded_corners',
|
||||
'items_style_caption_move__images_rounded_corners',
|
||||
];
|
||||
|
||||
export default [
|
||||
// v3.0.0
|
||||
// Changed items style builtin_controls structure.
|
||||
{
|
||||
attributes: {
|
||||
...attributes,
|
||||
...(() => {
|
||||
const attrs = {};
|
||||
Object.keys(V2_23_0_ATTRIBUTES).forEach((k) => {
|
||||
attrs[k] = { type: 'string' };
|
||||
});
|
||||
return attrs;
|
||||
})(),
|
||||
...(() => {
|
||||
const attrs = {};
|
||||
V2_23_0_BORDER_RADIUS.forEach((k) => {
|
||||
attrs[k] = { type: 'number', default: 0 };
|
||||
});
|
||||
return attrs;
|
||||
})(),
|
||||
},
|
||||
migrate(oldAttributes) {
|
||||
const newAttributes = { ...oldAttributes };
|
||||
|
||||
Object.keys(V2_23_0_ATTRIBUTES).forEach((k) => {
|
||||
if (k in newAttributes) {
|
||||
if (newAttributes[k]) {
|
||||
newAttributes[V2_23_0_ATTRIBUTES[k]] = newAttributes[k];
|
||||
}
|
||||
|
||||
delete newAttributes[k];
|
||||
}
|
||||
});
|
||||
|
||||
V2_23_0_BORDER_RADIUS.forEach((k) => {
|
||||
if (typeof newAttributes[k] === 'number') {
|
||||
newAttributes[k] = `${newAttributes[k]}px`;
|
||||
}
|
||||
});
|
||||
|
||||
return [newAttributes, []];
|
||||
},
|
||||
isEligible(attrs) {
|
||||
const keys = Object.keys(V2_23_0_ATTRIBUTES);
|
||||
let eligible = false;
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (!eligible && key in attrs) {
|
||||
eligible = true;
|
||||
}
|
||||
});
|
||||
|
||||
V2_23_0_BORDER_RADIUS.forEach((k) => {
|
||||
if (!eligible && typeof attrs[k] === 'number') {
|
||||
eligible = true;
|
||||
}
|
||||
});
|
||||
|
||||
return eligible;
|
||||
},
|
||||
save,
|
||||
},
|
||||
];
|
103
wp-content/plugins/visual-portfolio/gutenberg/block/edit.js
Normal file
103
wp-content/plugins/visual-portfolio/gutenberg/block/edit.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
import ControlsRender from '../components/controls-render';
|
||||
import IframePreview from '../components/iframe-preview';
|
||||
import SetupWizard from '../components/setup-wizard';
|
||||
|
||||
const {
|
||||
plugin_url: pluginUrl,
|
||||
controls_categories: registeredControlsCategories,
|
||||
} = window.VPGutenbergVariables;
|
||||
|
||||
function renderControls(props) {
|
||||
const { attributes } = props;
|
||||
|
||||
let { content_source: contentSource } = attributes;
|
||||
|
||||
// Saved layouts by default displaying Portfolio source.
|
||||
if (contentSource === 'portfolio') {
|
||||
contentSource = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ControlsRender category="content-source" {...props} />
|
||||
|
||||
{/* Display all settings once selected Content Source */}
|
||||
{contentSource ? (
|
||||
<>
|
||||
{Object.keys(registeredControlsCategories).map((name) => {
|
||||
if (name === 'content-source') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ControlsRender
|
||||
key={name}
|
||||
category={name}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Edit Class.
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function BlockEdit(props) {
|
||||
const { attributes, setAttributes } = props;
|
||||
|
||||
const {
|
||||
block_id: blockId,
|
||||
content_source: contentSource,
|
||||
setup_wizard: setupWizard,
|
||||
preview_image_example: previewExample,
|
||||
layout,
|
||||
} = attributes;
|
||||
|
||||
// Display setup wizard on mount.
|
||||
useEffect(() => {
|
||||
if (!setupWizard && (!blockId || !contentSource)) {
|
||||
setAttributes({
|
||||
setup_wizard: 'true',
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Display block preview.
|
||||
if (previewExample === 'true') {
|
||||
return (
|
||||
<div className="vpf-example-preview">
|
||||
<img
|
||||
src={`${pluginUrl}/assets/admin/images/example-${layout}.png`}
|
||||
alt={`Preview of ${layout} layout`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
return (
|
||||
<div {...blockProps}>
|
||||
{setupWizard === 'true' ? (
|
||||
<SetupWizard {...props} />
|
||||
) : (
|
||||
<>
|
||||
<InspectorControls>
|
||||
{renderControls(props)}
|
||||
</InspectorControls>
|
||||
<IframePreview {...props} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
32
wp-content/plugins/visual-portfolio/gutenberg/block/index.js
Normal file
32
wp-content/plugins/visual-portfolio/gutenberg/block/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
import './style.scss';
|
||||
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
import { ReactComponent as ElementIcon } from '../../assets/admin/images/icon-gutenberg.svg';
|
||||
import metadata from './block.json';
|
||||
import deprecated from './deprecated';
|
||||
import edit from './edit';
|
||||
import save from './save';
|
||||
import transforms from './transforms';
|
||||
import variations from './variations';
|
||||
|
||||
const { name } = metadata;
|
||||
|
||||
const settings = {
|
||||
icon: {
|
||||
foreground: '#2540CC',
|
||||
src: <ElementIcon width="20" height="20" />,
|
||||
},
|
||||
example: {
|
||||
attributes: {
|
||||
preview_image_example: 'true',
|
||||
},
|
||||
},
|
||||
variations,
|
||||
edit,
|
||||
save,
|
||||
transforms,
|
||||
deprecated,
|
||||
};
|
||||
|
||||
registerBlockType(name, settings);
|
@ -0,0 +1,3 @@
|
||||
export default function BlockSave() {
|
||||
return null;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// Block preview example.
|
||||
.vpf-example-preview {
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
|
||||
export default {
|
||||
from: [
|
||||
// Transform from default Gallery block.
|
||||
{
|
||||
type: 'block',
|
||||
blocks: ['core/gallery'],
|
||||
isMatch(attributes, blockData) {
|
||||
return (
|
||||
(blockData &&
|
||||
blockData.innerBlocks &&
|
||||
blockData.innerBlocks.length) ||
|
||||
(attributes &&
|
||||
attributes.images &&
|
||||
attributes.images.length)
|
||||
);
|
||||
},
|
||||
transform(attributes, innerBlocks) {
|
||||
const { className } = attributes;
|
||||
|
||||
// New gallery since WordPress 5.9
|
||||
const isNewGallery = innerBlocks && innerBlocks.length;
|
||||
let images = [];
|
||||
|
||||
if (isNewGallery) {
|
||||
images = innerBlocks.map((img) => ({
|
||||
id: parseInt(img.attributes.id, 10),
|
||||
imgUrl: img.attributes.url,
|
||||
imgThumbnailUrl: img.attributes.url,
|
||||
title: img.attributes.caption,
|
||||
url:
|
||||
(img.attributes.linkDestination === 'custom' ||
|
||||
img.attributes.linkDestination ===
|
||||
'attachment') &&
|
||||
img.attributes.href
|
||||
? img.attributes.href
|
||||
: '',
|
||||
}));
|
||||
} else {
|
||||
images = attributes.images.map((img) => ({
|
||||
id: parseInt(img.id, 10),
|
||||
imgUrl: img.fullUrl,
|
||||
imgThumbnailUrl: img.url,
|
||||
title: img.caption,
|
||||
}));
|
||||
}
|
||||
|
||||
return createBlock('visual-portfolio/block', {
|
||||
setup_wizard: 'false',
|
||||
content_source: 'images',
|
||||
items_count: -1,
|
||||
layout: 'masonry',
|
||||
items_style_fly__align: 'bottom-center',
|
||||
masonry_columns: parseInt(attributes.columns, 10) || 3,
|
||||
items_click_action:
|
||||
attributes.linkTo === 'none' && !isNewGallery
|
||||
? 'false'
|
||||
: 'url',
|
||||
images,
|
||||
className,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Transform from default Latest Posts block.
|
||||
{
|
||||
type: 'block',
|
||||
blocks: ['core/latest-posts'],
|
||||
transform(attributes) {
|
||||
const {
|
||||
className,
|
||||
postLayout,
|
||||
columns = 3,
|
||||
postsToShow = 6,
|
||||
displayPostContent,
|
||||
displayPostContentRadio,
|
||||
excerptLength,
|
||||
displayPostDate,
|
||||
orderBy = 'date',
|
||||
order = 'desc',
|
||||
categories,
|
||||
} = attributes;
|
||||
|
||||
return createBlock('visual-portfolio/block', {
|
||||
content_source: 'post-based',
|
||||
posts_source: 'post',
|
||||
posts_order_by: orderBy,
|
||||
posts_order_direction: order,
|
||||
posts_taxonomies: categories ? [categories] : false,
|
||||
items_count: postsToShow,
|
||||
layout: 'grid',
|
||||
grid_columns: postLayout === 'grid' ? columns : 1,
|
||||
items_style: 'default',
|
||||
items_style_default__show_categories: false,
|
||||
items_style_default__show_date: displayPostDate
|
||||
? 'true'
|
||||
: 'false',
|
||||
items_style_default__show_excerpt: displayPostContent,
|
||||
items_style_default__excerpt_words_count:
|
||||
displayPostContentRadio === 'full_post'
|
||||
? 100
|
||||
: excerptLength,
|
||||
items_style_default__align: 'left',
|
||||
items_style_default__show_read_more: displayPostContent
|
||||
? 'true'
|
||||
: 'false',
|
||||
className,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { RawHTML } from '@wordpress/element';
|
||||
|
||||
const { controls: registeredControls } = window.VPGutenbergVariables;
|
||||
|
||||
export default Object.keys(registeredControls.layout.options).map((name) => {
|
||||
const data = registeredControls.layout.options[name];
|
||||
|
||||
return {
|
||||
// If we don't set the isDefault, our main block will be visible in inserter.
|
||||
// Sometimes users requested this as they are confused it first start.
|
||||
// isDefault: registeredControls.layout.default === data.value,
|
||||
name: data.value,
|
||||
attributes: { layout: data.value },
|
||||
title: data.title,
|
||||
icon: data.icon
|
||||
? {
|
||||
foreground: '#2540CC',
|
||||
src: <RawHTML>{data.icon}</RawHTML>,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}) || [];
|
@ -0,0 +1,60 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Button, Tooltip } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function AlignControl(props) {
|
||||
const { options, value, onChange } = props;
|
||||
|
||||
let controlsArray = ['left', 'center', 'right'];
|
||||
if (options === 'box') {
|
||||
controlsArray = [
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
...controlsArray,
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vpf-component-align-control">
|
||||
{controlsArray.map((align) => {
|
||||
const alignTitle = align
|
||||
.split('-')
|
||||
.map((word) => {
|
||||
return word.slice(0, 1).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Tooltip key={`align-${align}`} text={alignTitle}>
|
||||
<Button
|
||||
className={classnames(
|
||||
`vpf-component-align-control-${align}`,
|
||||
value === align
|
||||
? 'vpf-component-align-control-active'
|
||||
: ''
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(align);
|
||||
}}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
.vpf-component-align-control {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
max-width: 160px;
|
||||
border: 1px solid #d7dade;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 10px 16px;
|
||||
|
||||
> span {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
opacity: 0;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: calc(50% - 1.5px);
|
||||
left: calc(50% - 1.5px);
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
content: "";
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
> span {
|
||||
background-color: #ccc;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.vpf-component-align-control-active {
|
||||
> span {
|
||||
background-color: var(--wp-admin-theme-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.vpf-component-align-control-left,
|
||||
&.vpf-component-align-control-top-left,
|
||||
&.vpf-component-align-control-bottom-left {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.vpf-component-align-control-right,
|
||||
&.vpf-component-align-control-top-right,
|
||||
&.vpf-component-align-control-bottom-right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
import './style.scss';
|
||||
|
||||
import { SelectControl, TextControl } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const DEFAULT_RATIOS = {
|
||||
'': __('Auto', 'visual-portfolio'),
|
||||
'16:9': __('Wide 16:9', 'visual-portfolio'),
|
||||
'21:9': __('Ultra Wide 21:9', 'visual-portfolio'),
|
||||
'4:3': __('TV 4:3', 'visual-portfolio'),
|
||||
'3:2': __('Classic Film 3:2', 'visual-portfolio'),
|
||||
custom: __('Custom', 'visual-portfolio'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class AspectRatio extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
isCustom: typeof DEFAULT_RATIOS[this.props.value] === 'undefined',
|
||||
};
|
||||
|
||||
this.updatePart = this.updatePart.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse aspect ratio string.
|
||||
*
|
||||
* @param {string} val - aspect ratio string.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
parseParts(val) {
|
||||
let left = '';
|
||||
let right = '';
|
||||
|
||||
if (val && /:/g.test(val)) {
|
||||
const parts = val.split(':');
|
||||
|
||||
[left, right] = parts;
|
||||
}
|
||||
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update part of aspect ration string
|
||||
*
|
||||
* @param {string} val - new value for ratio part
|
||||
* @param {boolean} left - left part of aspect ratio
|
||||
*/
|
||||
updatePart(val, left = true) {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
const parse = this.parseParts(value);
|
||||
|
||||
if (!val || !parse[0] || !parse[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
parse[0] = val;
|
||||
} else {
|
||||
parse[1] = val;
|
||||
}
|
||||
|
||||
onChange(`${parse[0]}:${parse[1]}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
const { isCustom } = this.state;
|
||||
|
||||
const parts = this.parseParts(value);
|
||||
|
||||
return (
|
||||
<div className="vpf-component-aspect-ratio">
|
||||
<SelectControl
|
||||
value={isCustom ? 'custom' : value}
|
||||
onChange={(val) => {
|
||||
if (val === 'custom') {
|
||||
this.setState({
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
if (!value) {
|
||||
onChange('3:4');
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isCustom: false,
|
||||
});
|
||||
onChange(val);
|
||||
}
|
||||
}}
|
||||
options={Object.keys(DEFAULT_RATIOS).map((ratio) => ({
|
||||
label: DEFAULT_RATIOS[ratio],
|
||||
value: ratio,
|
||||
}))}
|
||||
/>
|
||||
{isCustom ? (
|
||||
<div className="vpf-component-aspect-ratio-custom">
|
||||
<TextControl
|
||||
label={__('Width', 'visual-portfolio')}
|
||||
type="number"
|
||||
value={parts[0]}
|
||||
onChange={(val) => this.updatePart(val, true)}
|
||||
/>
|
||||
<TextControl
|
||||
label={__('Height', 'visual-portfolio')}
|
||||
type="number"
|
||||
value={parts[1]}
|
||||
onChange={(val) => this.updatePart(val, false)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
.vpf-component-aspect-ratio-custom {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10px;
|
||||
|
||||
> .components-base-control {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
import './style.scss';
|
||||
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
const { navigator } = window;
|
||||
|
||||
// generate dom tree.
|
||||
function getNodeTree(node) {
|
||||
if (node && node.hasChildNodes()) {
|
||||
const children = [];
|
||||
|
||||
for (let j = 0; j < node.childNodes.length; j += 1) {
|
||||
children.push(getNodeTree(node.childNodes[j]));
|
||||
}
|
||||
|
||||
return {
|
||||
classList: node.classList,
|
||||
nodeName: node.nodeName,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class ClassesTree extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
nodes: false,
|
||||
};
|
||||
|
||||
this.onFrameLoad = this.onFrameLoad.bind(this);
|
||||
this.maybeFindIframe = this.maybeFindIframe.bind(this);
|
||||
this.updateTreeData = this.updateTreeData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeFindIframe();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.maybeFindIframe();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (!this.iframePreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iframePreview.removeEventListener('load', this.onFrameLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* On frame load event.
|
||||
*/
|
||||
onFrameLoad() {
|
||||
if (!this.iframePreview.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.frameWindow = this.iframePreview.contentWindow;
|
||||
this.frameJQuery = this.iframePreview.contentWindow.jQuery;
|
||||
|
||||
if (this.frameJQuery) {
|
||||
this.$framePortfolio = this.frameJQuery('.vp-portfolio');
|
||||
}
|
||||
|
||||
this.updateTreeData();
|
||||
}
|
||||
|
||||
maybeFindIframe() {
|
||||
if (this.iframePreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientId } = this.props;
|
||||
|
||||
const iframePreview = document.querySelector(
|
||||
`#block-${clientId} iframe`
|
||||
);
|
||||
|
||||
if (iframePreview) {
|
||||
this.iframePreview = iframePreview;
|
||||
this.iframePreview.addEventListener('load', this.onFrameLoad);
|
||||
this.onFrameLoad();
|
||||
}
|
||||
}
|
||||
|
||||
updateTreeData() {
|
||||
if (this.$framePortfolio) {
|
||||
this.setState({
|
||||
nodes: getNodeTree(this.$framePortfolio[0]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.iframePreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vpf-component-classes-tree">
|
||||
<ClassesTree.TreeItem
|
||||
node={this.state.nodes}
|
||||
skipNodeByClass={/vp-portfolio__item-popup/}
|
||||
collapseByClass={
|
||||
/^(vp-portfolio__preloader-wrap|vp-portfolio__filter-wrap|vp-portfolio__sort-wrap|vp-portfolio__items-wrap|vp-portfolio__pagination-wrap)$/
|
||||
}
|
||||
skipClass={/vp-uid-/}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClassesTree.TreeItem = class TreeItem extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
isCollapsed: null,
|
||||
};
|
||||
|
||||
this.isCollapsed = this.isCollapsed.bind(this);
|
||||
}
|
||||
|
||||
isCollapsed() {
|
||||
const { node, collapseByClass } = this.props;
|
||||
|
||||
let { isCollapsed } = this.state;
|
||||
|
||||
// check if collapsed by default.
|
||||
if (
|
||||
isCollapsed === null &&
|
||||
node &&
|
||||
node.classList &&
|
||||
node.classList.length
|
||||
) {
|
||||
node.classList.forEach((className) => {
|
||||
if (collapseByClass && collapseByClass.test(className)) {
|
||||
isCollapsed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return isCollapsed;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node, skipNodeByClass, skipClass } = this.props;
|
||||
|
||||
if (!node || !node.children.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = [];
|
||||
let skip = false;
|
||||
|
||||
// Classes.
|
||||
if (node.classList && node.classList.length) {
|
||||
node.classList.forEach((className) => {
|
||||
if (!skipClass || !skipClass.test(className)) {
|
||||
classes.push(className);
|
||||
}
|
||||
|
||||
// Skip?
|
||||
if (skipNodeByClass && skipNodeByClass.test(className)) {
|
||||
skip = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li
|
||||
className={`vpf-component-classes-tree-node ${
|
||||
this.isCollapsed() ? '' : 'is-collapsed'
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
{node.children.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="vpf-component-classes-tree-node-collapse"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
isCollapsed: !this.isCollapsed(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<
|
||||
{node.nodeName.toLowerCase()}
|
||||
{classes.length ? (
|
||||
<>
|
||||
{' class="'}
|
||||
{classes.map((className) => (
|
||||
<button
|
||||
key={className}
|
||||
type="button"
|
||||
className="vpf-component-classes-tree-node-class"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
className
|
||||
);
|
||||
}}
|
||||
>
|
||||
{className}
|
||||
</button>
|
||||
))}
|
||||
{'" '}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
{node.children.length && this.isCollapsed()
|
||||
? node.children.map((childNode) => {
|
||||
if (childNode) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<li className="vpf-component-classes-tree-child">
|
||||
<ClassesTree.TreeItem
|
||||
{...this.props}
|
||||
node={childNode}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
: ''}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,126 @@
|
||||
@use "sass:color";
|
||||
|
||||
$dom_tree_color_bg_node_class: #dcdfe6 !default;
|
||||
$dom_tree_color_text_node_class: #595d67 !default;
|
||||
|
||||
/**
|
||||
* DOM Tree
|
||||
*/
|
||||
.vpf-component-classes-tree-help code {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
line-height: 1.2;
|
||||
color: $dom_tree_color_text_node_class;
|
||||
cursor: pointer;
|
||||
background-color: $dom_tree_color_bg_node_class;
|
||||
border-radius: 3px;
|
||||
transition: 0.15s background-color, 0.15s color;
|
||||
|
||||
&:hover {
|
||||
color: color.adjust($dom_tree_color_text_node_class, $lightness: -20%);
|
||||
background-color: color.adjust($dom_tree_color_bg_node_class, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-classes-tree {
|
||||
padding: 15px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
font-family: monospace;
|
||||
color: #67666d;
|
||||
background-color: #f9f9fa;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
.spinner {
|
||||
float: none;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
padding-left: 15px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// Node
|
||||
.vpf-component-classes-tree-node > div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// Node Class
|
||||
.vpf-component-classes-tree-node-class {
|
||||
display: inline;
|
||||
padding: 2px 6px;
|
||||
line-height: 1.2;
|
||||
color: $dom_tree_color_text_node_class;
|
||||
cursor: pointer;
|
||||
background-color: $dom_tree_color_bg_node_class;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
transition: 0.15s background-color, 0.15s color;
|
||||
|
||||
&:hover {
|
||||
color: color.adjust($dom_tree_color_text_node_class, $lightness: -20%);
|
||||
background-color: color.adjust($dom_tree_color_bg_node_class, $lightness: -10%);
|
||||
}
|
||||
|
||||
+ .vpf-component-classes-tree-node-class {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse
|
||||
.vpf-component-classes-tree-node-collapse {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-top: 6px solid;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: -10px;
|
||||
bottom: -7px;
|
||||
left: -10px;
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.is-collapsed .vpf-component-classes-tree-node-collapse {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
// Hover
|
||||
.vpf-component-classes-tree-child {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: 0;
|
||||
bottom: -3px;
|
||||
left: 8px;
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: #f0f0f1;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: 0.2s opacity;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-classes-tree-node:hover ~ .vpf-component-classes-tree-child::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
// It is required to load react-ace first.
|
||||
// eslint-disable-next-line simple-import-sort/imports
|
||||
import AceEditor from 'react-ace';
|
||||
|
||||
import 'ace-builds/src-noconflict/mode-css';
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import 'ace-builds/src-noconflict/snippets/css';
|
||||
import 'ace-builds/src-noconflict/snippets/javascript';
|
||||
import 'ace-builds/src-noconflict/snippets/text';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class CodeEditor extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
codePlaceholder: this.props.codePlaceholder,
|
||||
};
|
||||
|
||||
this.maybeRemovePlaceholder = this.maybeRemovePlaceholder.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeRemovePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove placeholder after first change.
|
||||
*/
|
||||
maybeRemovePlaceholder() {
|
||||
const { value } = this.props;
|
||||
|
||||
const { codePlaceholder } = this.state;
|
||||
|
||||
if (value && codePlaceholder) {
|
||||
this.setState({ codePlaceholder: '' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, onChange, mode, maxLines, minLines } = this.props;
|
||||
|
||||
const { codePlaceholder } = this.state;
|
||||
|
||||
return (
|
||||
<AceEditor
|
||||
className="vpf-component-code-editor"
|
||||
theme="textmate"
|
||||
onLoad={(editor) => {
|
||||
editor.renderer.setScrollMargin(16, 16, 16, 16);
|
||||
editor.renderer.setPadding(16);
|
||||
}}
|
||||
fontSize={12}
|
||||
showPrintMargin
|
||||
showGutter
|
||||
highlightActiveLine={false}
|
||||
width="100%"
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
showLineNumbers: true,
|
||||
printMargin: false,
|
||||
tabSize: 2,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
value={value || codePlaceholder}
|
||||
onChange={(val) => {
|
||||
onChange(val === codePlaceholder ? '' : val);
|
||||
|
||||
this.maybeRemovePlaceholder();
|
||||
}}
|
||||
mode={mode}
|
||||
maxLines={maxLines}
|
||||
minLines={minLines}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
@import "../../variables";
|
||||
|
||||
.vpf-component-code-editor {
|
||||
&.ace_editor {
|
||||
width: 100%;
|
||||
line-height: 1.45;
|
||||
background-color: $light-gray-100;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 1px $light-gray-400;
|
||||
|
||||
.ace_gutter {
|
||||
background-color: $light-gray-400;
|
||||
}
|
||||
|
||||
.ace_gutter-cell {
|
||||
background-color: $light-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_tooltip {
|
||||
padding: 6px 10px;
|
||||
background: none;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e4e4;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 5%);
|
||||
}
|
||||
|
||||
.ace_hidden-cursors {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-control-pre-custom-css {
|
||||
padding: 20px;
|
||||
background-color: $light-gray-400;
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Fragment, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function CollapseControl(props) {
|
||||
const { children, options, initialOpen } = props;
|
||||
|
||||
const [collapsed, setCollapsed] = useState(initialOpen);
|
||||
|
||||
return (
|
||||
<div className="vpf-component-collapse-control">
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<Fragment key={option.category}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCollapsed(
|
||||
option.category === collapsed
|
||||
? ''
|
||||
: option.category
|
||||
);
|
||||
}}
|
||||
className={classnames(
|
||||
'vpf-component-collapse-control-toggle',
|
||||
option.category === collapsed
|
||||
? 'vpf-component-collapse-control-active'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<span>{option.title}</span>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
className="components-panel__arrow"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<div
|
||||
className={classnames(
|
||||
'vpf-component-collapse-control-content',
|
||||
option.category === collapsed
|
||||
? 'vpf-component-collapse-control-content-active'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{children(option)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
.vpf-component-collapse-control {
|
||||
margin-top: 0;
|
||||
margin-top: -10px;
|
||||
margin-right: -16px;
|
||||
margin-bottom: -10px;
|
||||
margin-left: -16px;
|
||||
|
||||
// Toggle.
|
||||
.vpf-component-collapse-control-toggle {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
font-weight: 500;
|
||||
|
||||
// Remove default Gutenberg box shadow.
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-collapse-control-active svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
// Content.
|
||||
.vpf-component-collapse-control-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vpf-component-collapse-control-content-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Fix panel styles.
|
||||
.components-panel__body {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.components-panel__body:has(+ .vpf-component-collapse-control-toggle) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients } from '@wordpress/block-editor';
|
||||
import {
|
||||
Button,
|
||||
ColorPalette,
|
||||
Dropdown,
|
||||
GradientPicker,
|
||||
TabPanel,
|
||||
} from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
function useColors() {
|
||||
// New way to get colors and gradients.
|
||||
if (
|
||||
useMultipleOriginColorsAndGradients &&
|
||||
useMultipleOriginColorsAndGradients()
|
||||
) {
|
||||
const colorsData = useMultipleOriginColorsAndGradients();
|
||||
return {
|
||||
colors: colorsData.colors,
|
||||
gradients: colorsData.gradients,
|
||||
};
|
||||
}
|
||||
|
||||
// Old way.
|
||||
const { colors, gradients } = useSelect((select) => {
|
||||
const settings = select('core/block-editor').getSettings();
|
||||
|
||||
const themeColors = [];
|
||||
const themeGradients = [];
|
||||
|
||||
if (settings.colors && settings.colors.length) {
|
||||
themeColors.push({ name: 'Theme', colors: settings.colors });
|
||||
}
|
||||
if (settings.gradients && settings.gradients.length) {
|
||||
themeGradients.push({
|
||||
name: 'Theme',
|
||||
gradients: settings.gradients,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
colors: themeColors,
|
||||
gradients: themeGradients,
|
||||
};
|
||||
});
|
||||
|
||||
return { colors, gradients };
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function ColorPicker(props) {
|
||||
const {
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
alpha = false,
|
||||
gradient = false,
|
||||
afterDropdownContent,
|
||||
} = props;
|
||||
const { colors, gradients } = useColors();
|
||||
|
||||
const isGradient = value && value.match(/gradient/);
|
||||
const colorValue = isGradient ? undefined : value;
|
||||
const gradientValue = isGradient ? value : undefined;
|
||||
|
||||
const tabs = {
|
||||
solid: (
|
||||
<ColorPalette
|
||||
colors={colors}
|
||||
value={colorValue}
|
||||
enableAlpha={alpha}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
__experimentalHasMultipleOrigins
|
||||
__experimentalIsRenderedInSidebar
|
||||
/>
|
||||
),
|
||||
gradient: (
|
||||
<GradientPicker
|
||||
__nextHasNoMargin
|
||||
value={gradientValue}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
gradients={gradients}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="vpf-component-color-picker__dropdown"
|
||||
contentClassName="vpf-component-color-picker__dropdown-content"
|
||||
popoverProps={{
|
||||
placement: 'left-start',
|
||||
offset: 36,
|
||||
shift: true,
|
||||
}}
|
||||
renderToggle={({ isOpen, onToggle }) => (
|
||||
<Button
|
||||
className={classnames(
|
||||
'vpf-component-color-toggle',
|
||||
isOpen ? 'vpf-component-color-toggle-active' : ''
|
||||
)}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span
|
||||
className="vpf-component-color-toggle-indicator"
|
||||
style={{ background: value || '' }}
|
||||
/>
|
||||
<span className="vpf-component-color-toggle-label">
|
||||
{label}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<div className="vpf-component-color-picker">
|
||||
{gradient ? (
|
||||
<TabPanel
|
||||
tabs={[
|
||||
{
|
||||
name: 'solid',
|
||||
title: 'Solid',
|
||||
},
|
||||
{
|
||||
name: 'gradient',
|
||||
title: 'Gradient',
|
||||
},
|
||||
]}
|
||||
initialTabName={isGradient ? 'gradient' : 'solid'}
|
||||
>
|
||||
{(tab) => {
|
||||
return tabs[tab.name];
|
||||
}}
|
||||
</TabPanel>
|
||||
) : (
|
||||
tabs.solid
|
||||
)}
|
||||
{afterDropdownContent || ''}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
@use "sass:math";
|
||||
|
||||
// Group label.
|
||||
.vpf-control-wrap-html:has(+ .vpf-control-wrap-color) {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.vpf-control-wrap-color:has(+ .vpf-control-wrap-color) {
|
||||
margin-bottom: -9px;
|
||||
|
||||
.vpf-component-color-toggle {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-color-toggle {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 10%);
|
||||
border-radius: 0;
|
||||
|
||||
&.vpf-component-color-toggle-active {
|
||||
color: var(--wp-admin-theme-color);
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.vpf-component-color-toggle-indicator {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin-right: 8px;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(-45deg, transparent 48%, #ddd 0, #ddd 52%, transparent 0);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 20%);
|
||||
}
|
||||
|
||||
.vpf-component-color-toggle-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-color-picker__dropdown {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vpf-component-color-picker__dropdown-content {
|
||||
.components-popover__content {
|
||||
width: 260px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.components-tab-panel__tabs {
|
||||
margin-top: -16px;
|
||||
margin-right: -16px;
|
||||
margin-bottom: 16px;
|
||||
margin-left: -16px;
|
||||
}
|
||||
}
|
@ -0,0 +1,886 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import {
|
||||
__experimentalUnitControl,
|
||||
BaseControl,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
CheckboxControl,
|
||||
Notice,
|
||||
PanelBody,
|
||||
RadioControl,
|
||||
RangeControl,
|
||||
TextareaControl,
|
||||
TextControl,
|
||||
ToggleControl,
|
||||
Tooltip,
|
||||
UnitControl as __stableUnitControl,
|
||||
} from '@wordpress/components';
|
||||
import {
|
||||
Component,
|
||||
RawHTML,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import controlConditionCheck from '../../utils/control-condition-check';
|
||||
import controlGetValue from '../../utils/control-get-value';
|
||||
import { maybeDecode, maybeEncode } from '../../utils/encode-decode';
|
||||
import AlignControl from '../align-control';
|
||||
import AspectRatio from '../aspect-ratio';
|
||||
import ClassesTree from '../classes-tree';
|
||||
import CodeEditor from '../code-editor';
|
||||
import CollapseControl from '../collapse-control';
|
||||
import ColorPicker from '../color-picker';
|
||||
import DatePicker from '../date-picker';
|
||||
import ElementsSelector from '../elements-selector';
|
||||
import GalleryControl from '../gallery-control';
|
||||
import IconsSelector from '../icons-selector';
|
||||
import NavigatorControl from '../navigator-control';
|
||||
import ProNote from '../pro-note';
|
||||
import SelectControl from '../select-control';
|
||||
import SortableControl from '../sortable-control';
|
||||
import TabsControl from '../tabs-control';
|
||||
import TilesSelector from '../tiles-selector';
|
||||
import ToggleGroupControl from '../toggle-group-control';
|
||||
import ToggleModal from '../toggle-modal';
|
||||
|
||||
const UnitControl = __stableUnitControl || __experimentalUnitControl;
|
||||
|
||||
const {
|
||||
controls: registeredControls,
|
||||
controls_categories: registeredControlsCategories,
|
||||
plugin_version: pluginVersion,
|
||||
} = window.VPGutenbergVariables;
|
||||
|
||||
const openedCategoriesCache = {};
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
class ControlsRender extends Component {
|
||||
render() {
|
||||
const {
|
||||
category,
|
||||
categoryToggle = true,
|
||||
attributes,
|
||||
setAttributes,
|
||||
controls,
|
||||
clientId,
|
||||
isSetupWizard,
|
||||
showPanel = true,
|
||||
} = this.props;
|
||||
|
||||
if (!attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// content source conditions.
|
||||
if (
|
||||
/^content-source-/g.test(category) &&
|
||||
category !== 'content-source-general' &&
|
||||
`content-source-${attributes.content_source}` !== category
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usedControls = controls || registeredControls;
|
||||
const result = [];
|
||||
|
||||
Object.keys(usedControls).forEach((name) => {
|
||||
const control = usedControls[name];
|
||||
|
||||
if (
|
||||
category &&
|
||||
(!control.category || category !== control.category)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controlData = applyFilters(
|
||||
'vpf.editor.controls-render-data',
|
||||
{
|
||||
attributes,
|
||||
setAttributes,
|
||||
onChange: (val) => {
|
||||
const newAttrs = applyFilters(
|
||||
'vpf.editor.controls-on-change',
|
||||
{ [control.name]: val },
|
||||
control,
|
||||
val,
|
||||
attributes
|
||||
);
|
||||
setAttributes(newAttrs);
|
||||
},
|
||||
...control,
|
||||
}
|
||||
);
|
||||
|
||||
// Conditions check.
|
||||
if (!ControlsRender.AllowRender(controlData, isSetupWizard)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.push(
|
||||
applyFilters(
|
||||
'vpf.editor.controls-render',
|
||||
<ControlsRender.Control
|
||||
key={`control-${control.name}-${control.label}`}
|
||||
{...controlData}
|
||||
clientId={clientId}
|
||||
isSetupWizard={isSetupWizard}
|
||||
renderProps={this.props}
|
||||
/>,
|
||||
controlData,
|
||||
this.props
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
let categoryTitle = categoryToggle ? category : false;
|
||||
let categoryIcon = false;
|
||||
let categoryPro = false;
|
||||
let categoryOpened = !categoryToggle;
|
||||
|
||||
if (
|
||||
categoryToggle &&
|
||||
typeof registeredControlsCategories[category] !== 'undefined'
|
||||
) {
|
||||
categoryTitle = registeredControlsCategories[category].title;
|
||||
categoryIcon = registeredControlsCategories[category].icon || false;
|
||||
categoryPro = !!registeredControlsCategories[category].is_pro;
|
||||
|
||||
if (typeof openedCategoriesCache[category] === 'undefined') {
|
||||
openedCategoriesCache[category] =
|
||||
registeredControlsCategories[category].is_opened || false;
|
||||
}
|
||||
categoryOpened = openedCategoriesCache[category];
|
||||
}
|
||||
|
||||
if (isSetupWizard) {
|
||||
return result.length ? (
|
||||
<div className="vpf-setup-wizard-panel">{result}</div>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
if (!showPanel) {
|
||||
return result.length ? result : '';
|
||||
}
|
||||
|
||||
return result.length ? (
|
||||
<PanelBody
|
||||
title={
|
||||
categoryTitle ? (
|
||||
<>
|
||||
{categoryIcon ? (
|
||||
<span className="vpf-control-category-title-icon">
|
||||
<RawHTML>{categoryIcon}</RawHTML>
|
||||
</span>
|
||||
) : null}
|
||||
<span>{categoryTitle}</span>
|
||||
{categoryPro ? (
|
||||
<span className="vpf-control-category-title-pro">
|
||||
{__('PRO', 'visual-portfolio')}
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
onToggle={() => {
|
||||
openedCategoriesCache[category] = !categoryOpened;
|
||||
}}
|
||||
initialOpen={categoryOpened}
|
||||
scrollAfterOpen
|
||||
>
|
||||
{result}
|
||||
</PanelBody>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Single Control.
|
||||
*
|
||||
* @param {Object} props - control props.
|
||||
*
|
||||
* @return {JSX} control.
|
||||
*/
|
||||
ControlsRender.Control = function (props) {
|
||||
const { attributes, onChange, isSetupWizard } = props;
|
||||
const $ref = useRef();
|
||||
const [positionInGroup, setPositionInGroup] = useState('');
|
||||
|
||||
const controlVal = controlGetValue(props.name, attributes);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.group && $ref.current) {
|
||||
const $element = $ref.current.parentElement.parentElement;
|
||||
let $prevSibling = $element.previousElementSibling;
|
||||
let $nextSibling = $element.nextElementSibling;
|
||||
|
||||
// Skip separator.
|
||||
while (
|
||||
$prevSibling &&
|
||||
$prevSibling.classList.contains('vpf-control-group-separator')
|
||||
) {
|
||||
$prevSibling = $prevSibling.previousElementSibling;
|
||||
}
|
||||
while (
|
||||
$nextSibling &&
|
||||
$nextSibling.classList.contains('vpf-control-group-separator')
|
||||
) {
|
||||
$nextSibling = $nextSibling.nextElementSibling;
|
||||
}
|
||||
|
||||
const isGroupEnabled =
|
||||
($prevSibling &&
|
||||
$prevSibling.classList.contains(
|
||||
`vpf-control-group-${props.group}`
|
||||
)) ||
|
||||
($nextSibling &&
|
||||
$nextSibling.classList.contains(
|
||||
`vpf-control-group-${props.group}`
|
||||
));
|
||||
const isStart =
|
||||
$prevSibling &&
|
||||
!$prevSibling.classList.contains(
|
||||
`vpf-control-group-${props.group}`
|
||||
) &&
|
||||
$prevSibling.classList.contains(`vpf-control-wrap`);
|
||||
const isEnd =
|
||||
$nextSibling &&
|
||||
!$nextSibling.classList.contains(
|
||||
`vpf-control-group-${props.group}`
|
||||
) &&
|
||||
$nextSibling.classList.contains(`vpf-control-wrap`);
|
||||
|
||||
let newPosition = '';
|
||||
|
||||
if (!isGroupEnabled) {
|
||||
// skip
|
||||
} else if (isStart) {
|
||||
newPosition = 'start';
|
||||
} else if (isEnd) {
|
||||
newPosition = 'end';
|
||||
}
|
||||
|
||||
if (positionInGroup !== newPosition) {
|
||||
setPositionInGroup(newPosition);
|
||||
}
|
||||
}
|
||||
}, [$ref, props.group, controlVal, positionInGroup]);
|
||||
|
||||
// Conditions check.
|
||||
if (!ControlsRender.AllowRender(props, isSetupWizard)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let renderControl = '';
|
||||
let renderControlLabel = props.label;
|
||||
let renderControlAfter = '';
|
||||
let renderControlHelp = props.description ? (
|
||||
<RawHTML className="components-base-control__help">
|
||||
{props.description}
|
||||
</RawHTML>
|
||||
) : null;
|
||||
let renderControlClassName = classnames(
|
||||
'vpf-control-wrap',
|
||||
`vpf-control-wrap-${props.type}`
|
||||
);
|
||||
|
||||
if (props.group) {
|
||||
renderControlClassName = classnames(
|
||||
renderControlClassName,
|
||||
'vpf-control-with-group',
|
||||
`vpf-control-group-${props.group}`,
|
||||
positionInGroup
|
||||
? `vpf-control-group-position-${positionInGroup}`
|
||||
: false
|
||||
);
|
||||
}
|
||||
|
||||
const categoryControlOptions = [];
|
||||
|
||||
// Check if category is empty.
|
||||
if (
|
||||
(props.type === 'category_tabs' ||
|
||||
props.type === 'category_toggle_group' ||
|
||||
props.type === 'category_collapse' ||
|
||||
props.type === 'category_navigator') &&
|
||||
props.options &&
|
||||
props.options.length
|
||||
) {
|
||||
props.options.forEach((opt) => {
|
||||
const isEmpty = ControlsRender.isCategoryEmpty({
|
||||
...props.renderProps,
|
||||
category: opt.category,
|
||||
categoryToggle: false,
|
||||
});
|
||||
|
||||
if (!isEmpty) {
|
||||
categoryControlOptions.push(opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Specific controls.
|
||||
switch (props.type) {
|
||||
case 'category_tabs':
|
||||
if (categoryControlOptions.length) {
|
||||
renderControl = (
|
||||
<TabsControl
|
||||
controlName={props.name}
|
||||
options={categoryControlOptions}
|
||||
key={categoryControlOptions}
|
||||
>
|
||||
{(tab) => {
|
||||
return (
|
||||
<ControlsRender
|
||||
{...props.renderProps}
|
||||
category={tab.name}
|
||||
categoryToggle={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</TabsControl>
|
||||
);
|
||||
} else {
|
||||
renderControl = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'category_toggle_group':
|
||||
if (categoryControlOptions.length) {
|
||||
renderControl = (
|
||||
<ToggleGroupControl
|
||||
controlName={props.name}
|
||||
options={categoryControlOptions}
|
||||
key={categoryControlOptions}
|
||||
>
|
||||
{(group) => {
|
||||
return (
|
||||
<ControlsRender
|
||||
{...props.renderProps}
|
||||
category={group.category}
|
||||
categoryToggle={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ToggleGroupControl>
|
||||
);
|
||||
} else {
|
||||
renderControl = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'category_collapse':
|
||||
if (categoryControlOptions.length) {
|
||||
renderControl = (
|
||||
<CollapseControl
|
||||
controlName={props.name}
|
||||
initialOpen={props.initialOpen}
|
||||
options={categoryControlOptions}
|
||||
key={categoryControlOptions}
|
||||
>
|
||||
{(tab) => {
|
||||
return (
|
||||
<ControlsRender
|
||||
{...props.renderProps}
|
||||
category={tab.category}
|
||||
categoryToggle={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</CollapseControl>
|
||||
);
|
||||
} else {
|
||||
renderControl = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'category_navigator':
|
||||
if (categoryControlOptions.length) {
|
||||
renderControl = (
|
||||
<NavigatorControl
|
||||
controlName={props.name}
|
||||
options={categoryControlOptions}
|
||||
key={categoryControlOptions}
|
||||
>
|
||||
{(tab) => {
|
||||
return (
|
||||
<ControlsRender
|
||||
{...props.renderProps}
|
||||
category={tab.category}
|
||||
categoryToggle={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</NavigatorControl>
|
||||
);
|
||||
} else {
|
||||
renderControl = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'html':
|
||||
renderControl = <RawHTML>{props.default}</RawHTML>;
|
||||
break;
|
||||
case 'select':
|
||||
case 'select2':
|
||||
renderControl = (
|
||||
<SelectControl
|
||||
controlName={props.name}
|
||||
callback={props.value_callback}
|
||||
attributes={attributes}
|
||||
value={controlVal}
|
||||
options={props.options || {}}
|
||||
onChange={(val) => onChange(val)}
|
||||
isSearchable={props.searchable}
|
||||
isMultiple={props.multiple}
|
||||
isCreatable={props.creatable || props.tags}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'buttons':
|
||||
renderControl = (
|
||||
<ButtonGroup>
|
||||
{Object.keys(props.options || {}).map((val) => (
|
||||
<Button
|
||||
isSmall
|
||||
isPrimary={controlVal === val}
|
||||
isPressed={controlVal === val}
|
||||
key={val}
|
||||
onClick={() => onChange(val)}
|
||||
>
|
||||
{props.options[val]}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
);
|
||||
break;
|
||||
case 'icons_selector':
|
||||
renderControl = (
|
||||
<IconsSelector
|
||||
controlName={props.name}
|
||||
callback={props.value_callback}
|
||||
attributes={attributes}
|
||||
value={controlVal}
|
||||
options={props.options}
|
||||
onChange={(val) => onChange(val)}
|
||||
collapseRows={props.collapse_rows || false}
|
||||
isSetupWizard={isSetupWizard}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'tiles_selector':
|
||||
renderControl = (
|
||||
<TilesSelector
|
||||
value={controlVal}
|
||||
options={props.options}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'elements_selector':
|
||||
renderControl = (
|
||||
<ElementsSelector
|
||||
value={controlVal}
|
||||
locations={props.locations}
|
||||
options={props.options}
|
||||
onChange={(val) => onChange(val)}
|
||||
props={props}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'align': {
|
||||
renderControl = (
|
||||
<AlignControl
|
||||
value={controlVal}
|
||||
options={props.options || 'horizontal'}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'aspect_ratio': {
|
||||
renderControl = (
|
||||
<AspectRatio
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'gallery':
|
||||
renderControl = (
|
||||
<GalleryControl
|
||||
imageControls={props.image_controls}
|
||||
focalPoint={props.focal_point}
|
||||
attributes={attributes}
|
||||
name={props.name}
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
isSetupWizard={isSetupWizard}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'code_editor':
|
||||
renderControl = (
|
||||
<CodeEditor
|
||||
value={props.encode ? maybeDecode(controlVal) : controlVal}
|
||||
mode={props.mode}
|
||||
maxLines={props.max_lines}
|
||||
minLines={props.min_lines}
|
||||
codePlaceholder={props.code_placeholder}
|
||||
onChange={(val) =>
|
||||
onChange(props.encode ? maybeEncode(val) : val)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.allow_modal) {
|
||||
renderControlAfter = (
|
||||
<ToggleModal
|
||||
modalTitle={__('Custom CSS', 'visual-portfolio')}
|
||||
buttonLabel={__('Open in Modal', 'visual-portfolio')}
|
||||
size="md"
|
||||
>
|
||||
<BaseControl
|
||||
id={`vpf-custom-css-${props.label || props.name}`}
|
||||
label={props.label}
|
||||
help={
|
||||
props.description ? (
|
||||
<RawHTML>{props.description}</RawHTML>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
className={classnames(
|
||||
'vpf-control-wrap',
|
||||
`vpf-control-wrap-${props.type}`
|
||||
)}
|
||||
>
|
||||
<div>{renderControl}</div>
|
||||
</BaseControl>
|
||||
{props.classes_tree ? (
|
||||
<>
|
||||
<p>{__('Classes Tree:', 'visual-portfolio')}</p>
|
||||
<ClassesTree {...props} />
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</ToggleModal>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'range':
|
||||
renderControl = (
|
||||
<RangeControl
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
value={parseFloat(controlVal)}
|
||||
onChange={(val) => onChange(parseFloat(val))}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'toggle':
|
||||
renderControl = (
|
||||
<ToggleControl
|
||||
checked={controlVal}
|
||||
label={props.alongside}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'checkbox':
|
||||
renderControl = (
|
||||
<CheckboxControl
|
||||
checked={controlVal}
|
||||
label={props.alongside}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'radio':
|
||||
renderControl = (
|
||||
<RadioControl
|
||||
label={renderControlLabel}
|
||||
selected={controlVal}
|
||||
options={Object.keys(props.options || {}).map((val) => ({
|
||||
label: props.options[val],
|
||||
value: val,
|
||||
}))}
|
||||
onChange={(option) => onChange(option)}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'color':
|
||||
renderControl = (
|
||||
<ColorPicker
|
||||
label={renderControlLabel}
|
||||
value={controlVal}
|
||||
alpha={props.alpha}
|
||||
gradient={props.gradient}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'date':
|
||||
renderControl = (
|
||||
<DatePicker
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'textarea':
|
||||
renderControl = (
|
||||
<TextareaControl
|
||||
label={renderControlLabel}
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'url':
|
||||
renderControl = (
|
||||
<TextControl
|
||||
label={renderControlLabel}
|
||||
type="url"
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'number':
|
||||
renderControl = (
|
||||
<TextControl
|
||||
label={renderControlLabel}
|
||||
type="number"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
value={parseFloat(controlVal)}
|
||||
onChange={(val) => onChange(parseFloat(val))}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'unit':
|
||||
renderControl = (
|
||||
<UnitControl
|
||||
label={renderControlLabel}
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
labelPosition="edge"
|
||||
__unstableInputWidth="70px"
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
break;
|
||||
case 'hidden':
|
||||
renderControl = (
|
||||
<TextControl
|
||||
type="hidden"
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'notice':
|
||||
renderControl = renderControlHelp ? (
|
||||
<Notice status={props.status} isDismissible={false}>
|
||||
{renderControlHelp}
|
||||
</Notice>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
renderControlHelp = false;
|
||||
break;
|
||||
case 'pro_note':
|
||||
renderControl = (
|
||||
<ProNote title={renderControlLabel}>
|
||||
{renderControlHelp || ''}
|
||||
<ProNote.Button
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://visualportfolio.co/pricing/?utm_source=plugin&utm_medium=block_settings&utm_campaign=${props.name}&utm_content=${pluginVersion}`}
|
||||
>
|
||||
{__('Go Pro', 'visual-portfolio')}
|
||||
</ProNote.Button>
|
||||
</ProNote>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
renderControlHelp = false;
|
||||
break;
|
||||
case 'sortable':
|
||||
renderControl = (
|
||||
<SortableControl
|
||||
label={renderControlLabel}
|
||||
controlName={props.name}
|
||||
attributes={attributes}
|
||||
value={controlVal}
|
||||
options={props.options || {}}
|
||||
defaultVal={props.default || {}}
|
||||
allowDisablingOptions={
|
||||
props.allow_disabling_options || false
|
||||
}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
renderControl = (
|
||||
<TextControl
|
||||
label={renderControlLabel}
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(val)}
|
||||
/>
|
||||
);
|
||||
renderControlLabel = false;
|
||||
}
|
||||
|
||||
// Hint.
|
||||
if (props.hint) {
|
||||
renderControl = (
|
||||
<Tooltip text={props.hint} position={props.hint_place}>
|
||||
<div>{renderControl}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: use this filter for custom controls.
|
||||
const data = applyFilters(
|
||||
'vpf.editor.controls-render-inner-data',
|
||||
{
|
||||
renderControl,
|
||||
renderControlLabel,
|
||||
renderControlHelp,
|
||||
renderControlAfter,
|
||||
renderControlClassName,
|
||||
},
|
||||
{ props, controlVal }
|
||||
);
|
||||
|
||||
// Prevent rendering.
|
||||
if (data.renderControl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{positionInGroup === 'start' ? (
|
||||
<div className="vpf-control-group-separator" />
|
||||
) : null}
|
||||
<BaseControl
|
||||
id={`vpf-control-group-${props.name}`}
|
||||
label={data.renderControlLabel}
|
||||
className={data.renderControlClassName}
|
||||
>
|
||||
<div ref={$ref}>{data.renderControl}</div>
|
||||
{data.renderControlHelp}
|
||||
</BaseControl>
|
||||
{data.renderControlAfter}
|
||||
{positionInGroup === 'end' ? (
|
||||
<div className="vpf-control-group-separator" />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if control is allowed to rendering.
|
||||
*
|
||||
* @param props
|
||||
* @param isSetupWizard
|
||||
*/
|
||||
ControlsRender.AllowRender = function (props, isSetupWizard = false) {
|
||||
if (props.skip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
props.condition &&
|
||||
props.condition.length &&
|
||||
!controlConditionCheck(props.condition, props.attributes)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSetupWizard && !props.setup_wizard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if category does not contains controls.
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
ControlsRender.isCategoryEmpty = function (props) {
|
||||
const { category, attributes, setAttributes, controls, isSetupWizard } =
|
||||
props;
|
||||
|
||||
const usedControls = controls || registeredControls;
|
||||
let isEmpty = true;
|
||||
|
||||
Object.keys(usedControls).forEach((name) => {
|
||||
if (!isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const control = usedControls[name];
|
||||
|
||||
if (category && (!control.category || category !== control.category)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controlData = applyFilters('vpf.editor.controls-render-data', {
|
||||
attributes,
|
||||
setAttributes,
|
||||
onChange: (val) => {
|
||||
const newAttrs = applyFilters(
|
||||
'vpf.editor.controls-on-change',
|
||||
{ [control.name]: val },
|
||||
control,
|
||||
val,
|
||||
attributes
|
||||
);
|
||||
setAttributes(newAttrs);
|
||||
},
|
||||
...control,
|
||||
});
|
||||
|
||||
// Conditions check.
|
||||
if (!ControlsRender.AllowRender(controlData, isSetupWizard)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isEmpty = false;
|
||||
});
|
||||
|
||||
return isEmpty;
|
||||
};
|
||||
|
||||
export default ControlsRender;
|
@ -0,0 +1,78 @@
|
||||
.vpf-control-wrap {
|
||||
> .components-base-control__help,
|
||||
> .components-base-control__field > .components-base-control__help {
|
||||
margin-bottom: 1em;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
color: rgb(117, 117, 117);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.components-base-control__label {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.components-modal__content & {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Text control.
|
||||
.vpf-control-wrap-text input[disabled] {
|
||||
color: rgba(44, 51, 56, 50%);
|
||||
border-color: rgba(220, 220, 222, 75%);
|
||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
// Notice control.
|
||||
.vpf-control-wrap-notice .components-notice {
|
||||
margin: 0;
|
||||
|
||||
&.is-info {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-control-category-title-pro {
|
||||
padding: 0.2em 0.8em;
|
||||
margin-right: 30px;
|
||||
margin-left: auto;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
background-color: #2540cc;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.components-panel__body-toggle > .vpf-control-category-title-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: block;
|
||||
transform: translateY(-50%);
|
||||
|
||||
+ span {
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Group controls.
|
||||
.vpf-control-group-separator {
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
+ .vpf-control-group-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vpf-component-modal &,
|
||||
.vpf-component-elements-selector-modal & {
|
||||
margin-right: -32px;
|
||||
margin-left: -32px;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import { Button, DatePicker, Dropdown } from '@wordpress/components';
|
||||
import {
|
||||
__experimentalGetSettings as getSettings,
|
||||
dateI18n,
|
||||
} from '@wordpress/date';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class VPDatePicker extends Component {
|
||||
render() {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
const settings = getSettings();
|
||||
const resolvedFormat = settings.formats.datetime || 'F j, Y';
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
renderToggle={({ onToggle }) => (
|
||||
<Button isSecondary isSmall onClick={onToggle}>
|
||||
{value
|
||||
? dateI18n(resolvedFormat, value)
|
||||
: __('Select Date', 'visual-portfolio')}
|
||||
</Button>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<div className="components-datetime vpf-component-date-picker">
|
||||
<DatePicker currentDate={value} onChange={onChange} />
|
||||
{value ? (
|
||||
<Button
|
||||
isSecondary
|
||||
isSmall
|
||||
onClick={() => {
|
||||
onChange('');
|
||||
}}
|
||||
>
|
||||
{__('Reset Date', 'visual-portfolio')}
|
||||
</Button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
import './style.scss';
|
@ -0,0 +1,3 @@
|
||||
.vpf-component-dropdown-no-padding .components-popover__content > div {
|
||||
padding: 0;
|
||||
}
|
@ -0,0 +1,446 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Modal,
|
||||
PanelBody,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
} from '@wordpress/components';
|
||||
import { Component, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import ControlsRender from '../controls-render';
|
||||
|
||||
const alignIcons = {
|
||||
left: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M9 9v6h11V9H9zM4 20h1.5V4H4v16z" />
|
||||
</svg>
|
||||
),
|
||||
center: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M20 9h-7.2V4h-1.6v5H4v6h7.2v5h1.6v-5H20z" />
|
||||
</svg>
|
||||
),
|
||||
right: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M4 15h11V9H4v6zM18.5 4v16H20V4h-1.5z" />
|
||||
</svg>
|
||||
),
|
||||
between: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M9 15h6V9H9v6zm-5 5h1.5V4H4v16zM18.5 4v16H20V4h-1.5z" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Options render
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
function ElementsSelectorOptions(props) {
|
||||
const {
|
||||
location,
|
||||
locationData,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
optionName,
|
||||
parentProps,
|
||||
} = props;
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const openModal = () => setOpen(true);
|
||||
const closeModal = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
className="vpf-component-elements-selector-control-location-options-item"
|
||||
onClick={openModal}
|
||||
>
|
||||
{options[optionName] ? options[optionName].title : optionName}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 4L17 10M17 10L11 16M17 10H3"
|
||||
stroke="black"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen ? (
|
||||
<Modal
|
||||
title={`${
|
||||
options[optionName]
|
||||
? options[optionName].title
|
||||
: optionName
|
||||
} ${__('Settings', 'visual-portfolio')}`}
|
||||
onRequestClose={(e) => {
|
||||
if (
|
||||
e?.relatedTarget?.classList?.contains('media-modal')
|
||||
) {
|
||||
// Don't close modal if opened media modal.
|
||||
} else {
|
||||
closeModal(e);
|
||||
}
|
||||
}}
|
||||
className="vpf-component-elements-selector-modal"
|
||||
>
|
||||
{options[optionName] && options[optionName].category ? (
|
||||
<ControlsRender
|
||||
{...parentProps.props}
|
||||
category={options[optionName].category}
|
||||
categoryToggle={false}
|
||||
/>
|
||||
) : null}
|
||||
{optionName !== 'items' ? (
|
||||
<PanelBody>
|
||||
<Button
|
||||
isLink
|
||||
style={{
|
||||
color: 'red',
|
||||
marginTop: '5px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm(
|
||||
__(
|
||||
'Are you sure you want to remove the element?',
|
||||
'visual-portfolio'
|
||||
)
|
||||
)
|
||||
) {
|
||||
onChange({
|
||||
...value,
|
||||
[location]: {
|
||||
...value[location],
|
||||
elements:
|
||||
locationData.elements.filter(
|
||||
(elementName) =>
|
||||
elementName !==
|
||||
optionName
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{__('Remove', 'visual-portfolio')}
|
||||
{` ${
|
||||
options[optionName]
|
||||
? options[optionName].title
|
||||
: optionName
|
||||
}`}
|
||||
</Button>
|
||||
</PanelBody>
|
||||
) : null}
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class ElementsSelector extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.getLocationData = this.getLocationData.bind(this);
|
||||
this.renderLocation = this.renderLocation.bind(this);
|
||||
this.renderAlignSettings = this.renderAlignSettings.bind(this);
|
||||
}
|
||||
|
||||
getLocationData(location) {
|
||||
const { options, locations, value } = this.props;
|
||||
|
||||
const title =
|
||||
(locations[location] && locations[location].title) || false;
|
||||
const elements =
|
||||
value[location] && value[location].elements
|
||||
? value[location].elements
|
||||
: [];
|
||||
const align =
|
||||
value[location] && value[location].align
|
||||
? value[location].align
|
||||
: false;
|
||||
const availableAlign =
|
||||
locations[location] && locations[location].align
|
||||
? locations[location].align
|
||||
: [];
|
||||
const availableElements = {};
|
||||
|
||||
// find all available elements
|
||||
Object.keys(options).forEach((name) => {
|
||||
const data = options[name];
|
||||
|
||||
if (
|
||||
(!data.allowed_locations ||
|
||||
data.allowed_locations.indexOf(location) !== -1) &&
|
||||
elements.indexOf(name) === -1
|
||||
) {
|
||||
availableElements[name] = data;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
title,
|
||||
elements,
|
||||
align,
|
||||
availableAlign,
|
||||
availableElements,
|
||||
};
|
||||
}
|
||||
|
||||
renderAlignSettings(location) {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
const locationData = this.getLocationData(location);
|
||||
const controls = [];
|
||||
|
||||
if (locationData.availableAlign.length) {
|
||||
locationData.availableAlign.forEach((alignName) => {
|
||||
controls.push(
|
||||
<ToolbarButton
|
||||
key={alignName}
|
||||
icon={alignIcons[alignName]}
|
||||
label={`${
|
||||
alignName.charAt(0).toUpperCase() +
|
||||
alignName.slice(1)
|
||||
}`}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...value,
|
||||
[location]: {
|
||||
...value[location],
|
||||
align: alignName,
|
||||
},
|
||||
})
|
||||
}
|
||||
isActive={
|
||||
locationData.align
|
||||
? locationData.align === alignName
|
||||
: false
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!controls.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="vpf-component-elements-selector-align__dropdown"
|
||||
contentClassName="vpf-component-elements-selector-align__dropdown-content"
|
||||
popoverProps={{
|
||||
placement: 'left-start',
|
||||
offset: 36,
|
||||
shift: true,
|
||||
}}
|
||||
renderToggle={({ isOpen, onToggle }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
className="vpf-component-elements-selector-control-location-options-item"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{locationData.align && alignIcons[locationData.align]
|
||||
? alignIcons[locationData.align]
|
||||
: alignIcons.center}
|
||||
</button>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<Toolbar label="Elements Selector">{controls}</Toolbar>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocation(location) {
|
||||
const { value, onChange, options } = this.props;
|
||||
|
||||
const locationData = this.getLocationData(location);
|
||||
const { availableElements } = locationData;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={location}
|
||||
className="vpf-component-elements-selector-control-location"
|
||||
>
|
||||
{locationData.title ? (
|
||||
<div className="vpf-component-elements-selector-control-location-title">
|
||||
{locationData.title}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{locationData.availableAlign.length &&
|
||||
locationData.elements.length ? (
|
||||
<div className="vpf-component-elements-selector-control-location-align">
|
||||
{this.renderAlignSettings(location)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={classnames(
|
||||
'vpf-component-elements-selector-control-location-options',
|
||||
locationData.align
|
||||
? `vpf-component-elements-selector-control-location-options-${locationData.align}`
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{locationData.elements.length
|
||||
? locationData.elements.map((optionName) => (
|
||||
<ElementsSelectorOptions
|
||||
key={optionName}
|
||||
location={location}
|
||||
locationData={locationData}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
optionName={optionName}
|
||||
parentProps={this.props}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{Object.keys(availableElements).length ? (
|
||||
<DropdownMenu
|
||||
className="vpf-component-elements-selector-control-location-options-add-button"
|
||||
popoverProps={{
|
||||
position: 'bottom center',
|
||||
}}
|
||||
icon={
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M18 11.2h-5.2V6h-1.6v5.2H6v1.6h5.2V18h1.6v-5.2H18z" />
|
||||
</svg>
|
||||
}
|
||||
controls={Object.keys(availableElements).map(
|
||||
(optionName) => ({
|
||||
title: (
|
||||
<>
|
||||
{
|
||||
availableElements[optionName]
|
||||
.title
|
||||
}
|
||||
{availableElements[optionName]
|
||||
.is_pro ? (
|
||||
<span className="vpf-component-elements-selector-control-location-options-title-pro">
|
||||
{__(
|
||||
'PRO',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
),
|
||||
onClick() {
|
||||
if (
|
||||
availableElements[optionName].is_pro
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newElements = [
|
||||
...locationData.elements,
|
||||
];
|
||||
|
||||
if (
|
||||
newElements.indexOf(optionName) ===
|
||||
-1
|
||||
) {
|
||||
newElements.push(optionName);
|
||||
|
||||
onChange({
|
||||
...value,
|
||||
[location]: {
|
||||
...value[location],
|
||||
elements: newElements,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locations } = this.props;
|
||||
|
||||
return (
|
||||
<div className="vpf-component-elements-selector-control">
|
||||
{Object.keys(locations).map((name) =>
|
||||
this.renderLocation(name)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
@import "../../variables";
|
||||
|
||||
.vpf-component-elements-selector-control-location {
|
||||
position: relative;
|
||||
// background-color: $light-gray-100;
|
||||
|
||||
& + & {
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-control-location-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-control-location-align {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 0;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
width: 25px;
|
||||
height: auto;
|
||||
padding: 4px 5px;
|
||||
margin-top: 0;
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--wp-admin-theme-color);
|
||||
background: #f9f8f8;
|
||||
border-color: #cbcbcb;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-left: 0;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-control-location-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: -10px;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons.
|
||||
.vpf-component-elements-selector-control-location-options-add-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
.components-button {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: #f9f8f8;
|
||||
border-color: #cbcbcb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-control-location-options-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 9px 12px;
|
||||
margin-top: 10px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
background-color: #f9f8f8;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 10%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--wp-admin-theme-color);
|
||||
border-color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
button.vpf-component-elements-selector-control-location-options-item {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 13px;
|
||||
height: auto;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// Align dropdown.
|
||||
.vpf-component-elements-selector-align__dropdown-content {
|
||||
.components-accessible-toolbar {
|
||||
border: none;
|
||||
|
||||
.components-button.has-icon.has-icon {
|
||||
min-width: 40px;
|
||||
height: 30px;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.components-button::before {
|
||||
right: 4px;
|
||||
left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-control-location-options-title-pro {
|
||||
padding: 0.2em 0.8em;
|
||||
margin-left: 10px;
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(to left, #743ad5, #d53a9d);
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.vpf-component-elements-selector-modal {
|
||||
.components-panel__body {
|
||||
max-width: 350px;
|
||||
padding-right: 24px;
|
||||
padding-left: 24px;
|
||||
margin-right: -24px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
|
||||
.components-modal__header + .components-panel__body {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.components-panel__body:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Add list of all categories to gallery images.
|
||||
*/
|
||||
addFilter(
|
||||
'vpf.editor.controls-render-data',
|
||||
'vpf/editor/controls-render-data/images-categories-suggestions',
|
||||
(data) => {
|
||||
if (data.name === 'images') {
|
||||
const categories = [];
|
||||
|
||||
// find all used categories.
|
||||
if (data.attributes.images && data.attributes.images.length) {
|
||||
data.attributes.images.forEach((image) => {
|
||||
if (image.categories && image.categories.length) {
|
||||
image.categories.forEach((cat) => {
|
||||
if (categories.indexOf(cat) === -1) {
|
||||
categories.push(cat);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
categories.length &&
|
||||
data.image_controls &&
|
||||
data.image_controls.categories &&
|
||||
data.image_controls.categories.options
|
||||
) {
|
||||
data.image_controls.categories.options = categories.map(
|
||||
(val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
@ -0,0 +1,109 @@
|
||||
import { TextareaControl, TextControl } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Change Title and Description controls when used dynamic source option.
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
function RenderTitleAndDescriptionImageControls(props) {
|
||||
const { data, textSource, img, name, index } = props;
|
||||
|
||||
const { imgData } = useSelect(
|
||||
(select) => {
|
||||
const { getMedia } = select('core');
|
||||
|
||||
return {
|
||||
imgData: img.id ? getMedia(img.id) : null,
|
||||
};
|
||||
},
|
||||
[img]
|
||||
);
|
||||
|
||||
let text = '';
|
||||
let description = '';
|
||||
|
||||
switch (textSource) {
|
||||
case 'title':
|
||||
text = imgData?.title?.raw || '';
|
||||
description = __(
|
||||
'Loaded automatically from the image Title',
|
||||
'visual-portfolio'
|
||||
);
|
||||
break;
|
||||
case 'caption':
|
||||
text = imgData?.caption?.raw || '';
|
||||
description = __(
|
||||
'Loaded automatically from the image Caption',
|
||||
'visual-portfolio'
|
||||
);
|
||||
break;
|
||||
case 'alt':
|
||||
text = imgData?.alt_text || '';
|
||||
description = __(
|
||||
'Loaded automatically from the image Alt',
|
||||
'visual-portfolio'
|
||||
);
|
||||
break;
|
||||
case 'description':
|
||||
text = imgData?.description?.raw || '';
|
||||
description = __(
|
||||
'Loaded automatically from the image Description',
|
||||
'visual-portfolio'
|
||||
);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
const ThisControl = name === 'title' ? TextControl : TextareaControl;
|
||||
|
||||
return (
|
||||
<ThisControl
|
||||
className={`vpf-control-wrap vpf-control-wrap-${
|
||||
name === 'title' ? 'text' : 'textarea'
|
||||
}`}
|
||||
key={`${
|
||||
img.id || img.imgThumbnailUrl || img.imgUrl
|
||||
}-${index}-${name}`}
|
||||
label={data.label}
|
||||
value={text}
|
||||
help={description}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Change gallery image Title and Description control .
|
||||
addFilter(
|
||||
'vpf.editor.gallery-controls-render',
|
||||
'vpf/editor/gallery-controls-render/title-and-description-render-by-source',
|
||||
(control, data, props, controlData) => {
|
||||
const { attributes, img } = props;
|
||||
const { name, index } = controlData;
|
||||
|
||||
if (
|
||||
(name === 'title' &&
|
||||
attributes.images_titles_source &&
|
||||
attributes.images_titles_source !== 'custom') ||
|
||||
(name === 'description' &&
|
||||
attributes.images_descriptions_source &&
|
||||
attributes.images_descriptions_source !== 'custom')
|
||||
) {
|
||||
control = (
|
||||
<RenderTitleAndDescriptionImageControls
|
||||
{...{
|
||||
data,
|
||||
textSource: attributes[`images_${name}s_source`],
|
||||
img,
|
||||
name,
|
||||
index,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
);
|
@ -0,0 +1,779 @@
|
||||
import './style.scss';
|
||||
import './extensions/dynamic-categories';
|
||||
import './extensions/image-title-and-desription';
|
||||
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
rectSortingStrategy,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
|
||||
import {
|
||||
Button,
|
||||
FocalPointPicker,
|
||||
Modal,
|
||||
TextControl,
|
||||
} from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import ControlsRender from '../controls-render';
|
||||
|
||||
const { navigator, VPGutenbergVariables } = window;
|
||||
|
||||
const ALLOWED_MEDIA_TYPES = ['image'];
|
||||
|
||||
function getHumanFileSize(size) {
|
||||
const i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return `${(size / 1024 ** i).toFixed(2) * 1} ${
|
||||
['B', 'KB', 'MB', 'GB', 'TB'][i]
|
||||
}`;
|
||||
}
|
||||
|
||||
function prepareImage(img) {
|
||||
const imgData = {
|
||||
id: img.id,
|
||||
imgUrl: img.url,
|
||||
imgThumbnailUrl: img.url,
|
||||
};
|
||||
|
||||
// Prepare thumbnail for all images except GIF, since GIFs animated only in full size.
|
||||
if (!img.mime || img.mime !== 'image/gif') {
|
||||
if (img.sizes && img.sizes.large && img.sizes.large.url) {
|
||||
imgData.imgThumbnailUrl = img.sizes.large.url;
|
||||
} else if (img.sizes && img.sizes.medium && img.sizes.medium.url) {
|
||||
imgData.imgThumbnailUrl = img.sizes.medium.url;
|
||||
} else if (
|
||||
img.sizes &&
|
||||
img.sizes.thumbnail &&
|
||||
img.sizes.thumbnail.url
|
||||
) {
|
||||
imgData.imgThumbnailUrl = img.sizes.thumbnail.url;
|
||||
}
|
||||
}
|
||||
|
||||
if (img.title) {
|
||||
imgData.title = img.title;
|
||||
}
|
||||
if (img.description) {
|
||||
imgData.description = img.description;
|
||||
}
|
||||
|
||||
return imgData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare selected images array using our format.
|
||||
* Use the current images set with already user data on images.
|
||||
*
|
||||
* @param {Array} images - new images set.
|
||||
* @param {Array} currentImages - current images set.
|
||||
* @return {Array}
|
||||
*/
|
||||
function prepareImages(images, currentImages) {
|
||||
const result = [];
|
||||
const currentImagesIds =
|
||||
currentImages && Object.keys(currentImages).length
|
||||
? currentImages.map((img) => img.id)
|
||||
: [];
|
||||
|
||||
if (images && images.length) {
|
||||
images.forEach((img) => {
|
||||
// We have to check for image URL, because when the image is removed from the
|
||||
// system, it should be removed from our block as well after re-save.
|
||||
if (img.url) {
|
||||
let currentImgData = false;
|
||||
|
||||
if (currentImagesIds.length) {
|
||||
const currentId = currentImagesIds.indexOf(img.id);
|
||||
|
||||
if (currentId > -1 && currentImages[currentId]) {
|
||||
currentImgData = currentImages[currentId];
|
||||
}
|
||||
}
|
||||
|
||||
const imgData = currentImgData || prepareImage(img);
|
||||
|
||||
result.push(imgData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const SelectedImageData = function (props) {
|
||||
const {
|
||||
showFocalPoint,
|
||||
focalPoint,
|
||||
imgId,
|
||||
imgUrl,
|
||||
onChangeFocalPoint,
|
||||
onChangeImage,
|
||||
} = props;
|
||||
|
||||
const [showMoreInfo, setShowMoreInfo] = useState(false);
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
const { imageData } = useSelect(
|
||||
(select) => {
|
||||
if (!imgId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { getMedia } = select('core');
|
||||
|
||||
const imgData = getMedia(imgId);
|
||||
|
||||
if (!imgData) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
imageData: imgData,
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[showMoreInfo, imgId]
|
||||
);
|
||||
|
||||
return (
|
||||
<MediaUploadCheck>
|
||||
<div
|
||||
className={`vpf-component-gallery-control-item-modal-image-info editor-post-featured-image ${
|
||||
showMoreInfo
|
||||
? 'vpf-component-gallery-control-item-modal-image-info-sticky-bottom'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{showFocalPoint ? (
|
||||
<FocalPointPicker
|
||||
url={imageData?.source_url || imgUrl}
|
||||
value={focalPoint}
|
||||
dimensions={{
|
||||
width: imageData?.media_details?.width || 80,
|
||||
height: imageData?.media_details?.height || 80,
|
||||
}}
|
||||
onChange={(val) => {
|
||||
onChangeFocalPoint(val);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<MediaUpload
|
||||
onSelect={(image) => {
|
||||
const imgData = prepareImage(image);
|
||||
onChangeImage(imgData);
|
||||
}}
|
||||
allowedTypes={ALLOWED_MEDIA_TYPES}
|
||||
render={({ open }) => (
|
||||
<Button onClick={open} isSecondary>
|
||||
{__('Replace Image', 'visual-portfolio')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChangeImage(false);
|
||||
}}
|
||||
isLink
|
||||
isDestructive
|
||||
>
|
||||
{__('Remove Image from Gallery', 'visual-portfolio')}
|
||||
</Button>
|
||||
<div className="vpf-component-gallery-control-item-modal-image-additional-info">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowMoreInfo(!showMoreInfo);
|
||||
}}
|
||||
isLink
|
||||
>
|
||||
{showMoreInfo
|
||||
? __('Hide Additional Info', 'visual-portfolio')
|
||||
: __('Show Additional Info', 'visual-portfolio')}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 4L14 10L8 16"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform={`rotate(${
|
||||
showMoreInfo ? '-' : ''
|
||||
}90 10 10)`}
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
{showMoreInfo ? (
|
||||
<>
|
||||
<div>
|
||||
<strong>
|
||||
{__('File name:', 'visual-portfolio')}
|
||||
</strong>{' '}
|
||||
{imageData?.source_url.split('/').pop() || '-'}
|
||||
<br />{' '}
|
||||
<strong>
|
||||
{__('File type:', 'visual-portfolio')}
|
||||
</strong>{' '}
|
||||
{imageData?.mime_type || '-'}
|
||||
<br />{' '}
|
||||
<strong>
|
||||
{__('File size:', '@text_domain')}
|
||||
</strong>{' '}
|
||||
{imageData?.media_details?.filesize
|
||||
? getHumanFileSize(
|
||||
imageData.media_details.filesize
|
||||
)
|
||||
: '-'}
|
||||
{imageData?.media_details?.width ? (
|
||||
<>
|
||||
<br />{' '}
|
||||
<strong>
|
||||
{__('Dimensions:', '@text_domain')}
|
||||
</strong>{' '}
|
||||
{imageData.media_details.width} by{' '}
|
||||
{imageData.media_details.height} pixels
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<TextControl
|
||||
label={__('File URL:', 'visual-portfolio')}
|
||||
value={imageData?.source_url || ''}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
imageData?.source_url || ''
|
||||
)
|
||||
.then(() => {
|
||||
setLinkCopied(true);
|
||||
});
|
||||
}}
|
||||
isSecondary
|
||||
isSmall
|
||||
>
|
||||
{__(
|
||||
'Copy URL to Clipboard',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</Button>
|
||||
{linkCopied ? (
|
||||
<span className="vpf-component-gallery-control-item-modal-image-additional-info-copied">
|
||||
{__('Copied!', 'visual-portfolio')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{imageData?.link ? (
|
||||
<div>
|
||||
<a
|
||||
href={imageData?.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{__(
|
||||
'View attachment page',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href={`${VPGutenbergVariables.admin_url}post.php?post=${imageData.id}&action=edit`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{__(
|
||||
'Edit more details',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</MediaUploadCheck>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableItem = function (props) {
|
||||
const {
|
||||
img,
|
||||
items,
|
||||
index,
|
||||
onChange,
|
||||
imageControls,
|
||||
controlName,
|
||||
focalPoint,
|
||||
clientId,
|
||||
isSetupWizard,
|
||||
} = props;
|
||||
|
||||
const idx = index - 1;
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
isSorting,
|
||||
} = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition: isSorting ? transition : '',
|
||||
};
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const openModal = () => setOpen(true);
|
||||
const closeModal = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classnames(
|
||||
'vpf-component-gallery-control-item',
|
||||
isDragging
|
||||
? 'vpf-component-gallery-control-item-dragging'
|
||||
: ''
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Button
|
||||
className="vpf-component-gallery-control-item-button"
|
||||
onClick={openModal}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<img
|
||||
src={img.imgThumbnailUrl || img.imgUrl}
|
||||
alt={img.alt || img.imgThumbnailUrl || img.imgUrl}
|
||||
loading="lazy"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className="vpf-component-gallery-control-item-remove"
|
||||
onClick={() => {
|
||||
const newImages = [...items];
|
||||
|
||||
if (newImages[idx]) {
|
||||
newImages.splice(idx, 1);
|
||||
|
||||
onChange(newImages);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 5.5H7.5M16.5 5.5H12.5M12.5 5.5V2.5H7.5V5.5M12.5 5.5H7.5M5 8.5L6 17H14L15 8.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<Modal
|
||||
title={__('Image Settings', 'visual-portfolio')}
|
||||
onRequestClose={(e) => {
|
||||
if (
|
||||
e?.relatedTarget?.classList?.contains('media-modal')
|
||||
) {
|
||||
// Don't close modal if opened media modal.
|
||||
} else {
|
||||
closeModal(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="vpf-component-gallery-control-item-modal">
|
||||
{focalPoint && img.id ? (
|
||||
<SelectedImageData
|
||||
showFocalPoint={focalPoint}
|
||||
focalPoint={img.focalPoint}
|
||||
imgId={img.id}
|
||||
imgUrl={img.imgThumbnailUrl || img.imgUrl}
|
||||
onChangeFocalPoint={(val) => {
|
||||
const newImages = [...items];
|
||||
|
||||
if (newImages[idx]) {
|
||||
newImages[idx] = {
|
||||
...newImages[idx],
|
||||
focalPoint: val,
|
||||
};
|
||||
|
||||
onChange(newImages);
|
||||
}
|
||||
}}
|
||||
onChangeImage={(imgData) => {
|
||||
const newImages = [...items];
|
||||
|
||||
if (!newImages[idx]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imgData === false) {
|
||||
newImages.splice(idx, 1);
|
||||
|
||||
onChange(newImages);
|
||||
|
||||
closeModal();
|
||||
} else {
|
||||
newImages[idx] = {
|
||||
...newImages[idx],
|
||||
...imgData,
|
||||
};
|
||||
|
||||
onChange(newImages);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div>
|
||||
{Object.keys(imageControls).map((name) => {
|
||||
const newCondition = [];
|
||||
|
||||
// prepare name.
|
||||
const imgControlName = `${controlName}[${idx}].${name}`;
|
||||
|
||||
// prepare conditions for the current item.
|
||||
if (imageControls[name].condition.length) {
|
||||
imageControls[name].condition.forEach(
|
||||
(data) => {
|
||||
const newData = { ...data };
|
||||
|
||||
if (
|
||||
newData.control &&
|
||||
/SELF/g.test(newData.control)
|
||||
) {
|
||||
newData.control =
|
||||
newData.control.replace(
|
||||
/SELF/g,
|
||||
`${controlName}[${idx}]`
|
||||
);
|
||||
}
|
||||
|
||||
newCondition.push(newData);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return applyFilters(
|
||||
'vpf.editor.gallery-controls-render',
|
||||
<ControlsRender.Control
|
||||
key={`${
|
||||
img.id ||
|
||||
img.imgThumbnailUrl ||
|
||||
img.imgUrl
|
||||
}-${idx}-${name}`}
|
||||
attributes={props.attributes}
|
||||
onChange={(val) => {
|
||||
const newImages = [...items];
|
||||
|
||||
if (newImages[idx]) {
|
||||
newImages[idx] = {
|
||||
...newImages[idx],
|
||||
[name]: val,
|
||||
};
|
||||
|
||||
onChange(newImages);
|
||||
}
|
||||
}}
|
||||
{...imageControls[name]}
|
||||
name={imgControlName}
|
||||
value={img[name]}
|
||||
condition={newCondition}
|
||||
clientId={clientId}
|
||||
isSetupWizard={isSetupWizard}
|
||||
/>,
|
||||
imageControls[name],
|
||||
props,
|
||||
{
|
||||
name,
|
||||
fullName: imgControlName,
|
||||
index: idx,
|
||||
condition: newCondition,
|
||||
}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableList = function (props) {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onSortEnd,
|
||||
imageControls,
|
||||
controlName,
|
||||
attributes,
|
||||
focalPoint,
|
||||
isSetupWizard,
|
||||
} = props;
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
||||
);
|
||||
|
||||
// Automatically open images selector when first time select Images in Setup Wizard.
|
||||
const [isOpenedInSetupWizard, setOpenInSetupWizard] = useState(
|
||||
!isSetupWizard
|
||||
);
|
||||
const [showingItems, setShowingItems] = useState(18);
|
||||
|
||||
const sortableItems = [];
|
||||
|
||||
if (items && items.length) {
|
||||
items.forEach((data, i) => {
|
||||
if (i < showingItems) {
|
||||
sortableItems.push({
|
||||
id: i + 1,
|
||||
data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const editGalleryButton = (
|
||||
<MediaUpload
|
||||
multiple="add"
|
||||
onSelect={(images) => {
|
||||
onChange(prepareImages(images, items));
|
||||
}}
|
||||
allowedTypes={ALLOWED_MEDIA_TYPES}
|
||||
value={items && items.length ? items.map((img) => img.id) : false}
|
||||
render={({ open }) => {
|
||||
if (!isOpenedInSetupWizard && (!items || !items.length)) {
|
||||
setOpenInSetupWizard(true);
|
||||
open();
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="vpf-component-gallery-control-item-fullwidth vpf-component-gallery-control-item-add"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
open();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
height="20"
|
||||
width="20"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{items && items.length ? (
|
||||
<path d="M9 14h10l-3.45-4.5-2.3 3-1.55-2Zm-1 4q-.825 0-1.412-.587Q6 16.825 6 16V4q0-.825.588-1.413Q7.175 2 8 2h12q.825 0 1.413.587Q22 3.175 22 4v12q0 .825-.587 1.413Q20.825 18 20 18Zm0-2h12V4H8v12Zm-4 6q-.825 0-1.412-.587Q2 20.825 2 20V6h2v14h14v2ZM8 4v12V4Z" />
|
||||
) : (
|
||||
<path d="M18 11.2h-5.2V6h-1.6v5.2H6v1.6h5.2V18h1.6v-5.2H18z" />
|
||||
)}
|
||||
</svg>
|
||||
<span>
|
||||
{items && items.length
|
||||
? __('Edit Gallery', 'visual-portfolio')
|
||||
: __('Add Images', 'visual-portfolio')}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="vpf-component-gallery-control-items">
|
||||
{items && items.length && items.length > 9
|
||||
? editGalleryButton
|
||||
: null}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over.id) {
|
||||
onSortEnd(active.id - 1, over.id - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SortableContext
|
||||
items={sortableItems}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
{sortableItems.map(({ data, id }) => (
|
||||
<SortableItem
|
||||
key={`vpf-component-gallery-control-items-sortable-${id}`}
|
||||
index={id}
|
||||
id={id}
|
||||
img={data}
|
||||
items={items}
|
||||
onChange={onChange}
|
||||
imageControls={imageControls}
|
||||
controlName={controlName}
|
||||
attributes={attributes}
|
||||
focalPoint={focalPoint}
|
||||
isSetupWizard={isSetupWizard}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{items && items.length ? (
|
||||
<span className="vpf-component-gallery-control-item-fullwidth vpf-component-gallery-control-item-pagination">
|
||||
<span>
|
||||
{sprintf(
|
||||
_n(
|
||||
'Showing %1$s of %2$s Image',
|
||||
'Showing %1$s of %2$s Images',
|
||||
items.length,
|
||||
'visual-portfolio'
|
||||
),
|
||||
showingItems > items.length
|
||||
? items.length
|
||||
: showingItems,
|
||||
items.length
|
||||
)}
|
||||
</span>
|
||||
{items.length > showingItems ? (
|
||||
<div className="vpf-component-gallery-control-item-pagination-buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
isSmall
|
||||
onClick={() => {
|
||||
setShowingItems(showingItems + 18);
|
||||
}}
|
||||
>
|
||||
{__('Show More', 'visual-portfolio')}
|
||||
</Button>
|
||||
<Button
|
||||
isLink
|
||||
isSmall
|
||||
onClick={() => {
|
||||
setShowingItems(items.length);
|
||||
}}
|
||||
>
|
||||
{__('Show All', 'visual-portfolio')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{editGalleryButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function GalleryControl(props) {
|
||||
const {
|
||||
imageControls,
|
||||
attributes,
|
||||
name: controlName,
|
||||
value,
|
||||
onChange,
|
||||
focalPoint,
|
||||
isSetupWizard,
|
||||
} = props;
|
||||
|
||||
const filteredValue = value.filter((img) => img.id);
|
||||
|
||||
return (
|
||||
<div className="vpf-component-gallery-control">
|
||||
<MediaUpload
|
||||
onSelect={(images) => {
|
||||
onChange(prepareImages(images));
|
||||
}}
|
||||
allowedTypes={ALLOWED_MEDIA_TYPES}
|
||||
multiple="add"
|
||||
value={
|
||||
filteredValue && Object.keys(filteredValue).length
|
||||
? filteredValue.map((img) => img.id)
|
||||
: []
|
||||
}
|
||||
render={() => (
|
||||
<SortableList
|
||||
items={filteredValue}
|
||||
onChange={onChange}
|
||||
imageControls={imageControls}
|
||||
controlName={controlName}
|
||||
attributes={attributes}
|
||||
focalPoint={focalPoint}
|
||||
isSetupWizard={isSetupWizard}
|
||||
onSortEnd={(oldIndex, newIndex) => {
|
||||
const newImages = arrayMove(
|
||||
filteredValue,
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
onChange(newImages);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
@import "../../variables";
|
||||
|
||||
.vpf-component-gallery-control {
|
||||
.vpf-component-gallery-control-items {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
|
||||
.vpf-component-gallery-control-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.vpf-component-gallery-control-item-button {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding-bottom: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f7f7f7 !important;
|
||||
border-radius: 5px;
|
||||
box-shadow: none !important;
|
||||
transition: 0.2s background-color;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 5px;
|
||||
transition: 0.2s opacity;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
fill: none;
|
||||
transition: 0.2s opacity;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #000 !important;
|
||||
|
||||
img {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-remove {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
background-color: #272727;
|
||||
border-radius: 20px;
|
||||
opacity: 0;
|
||||
transition: 0.2s opacity, 0.2s background-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #d51515;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 70%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item:hover .vpf-component-gallery-control-item-remove,
|
||||
.vpf-component-gallery-control-item:focus .vpf-component-gallery-control-item-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-dragging {
|
||||
z-index: 2;
|
||||
|
||||
.vpf-component-gallery-control-item-button {
|
||||
img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-remove {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-fullwidth {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-pagination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-pagination-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-add.components-button {
|
||||
border: 1px solid $gray-900;
|
||||
|
||||
> span {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-modal {
|
||||
max-width: 540px;
|
||||
|
||||
.vpf-component-gallery-control-item-modal-image-info {
|
||||
margin-bottom: 30px;
|
||||
|
||||
img {
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-modal-image-additional-info {
|
||||
margin-top: 30px;
|
||||
|
||||
> .components-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #7d7d7d;
|
||||
text-decoration: none;
|
||||
background-color: #ebebeb;
|
||||
|
||||
svg {
|
||||
width: 13px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-modal-image-additional-info-copied {
|
||||
margin-left: 10px;
|
||||
color: #15b11d;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
|
||||
.vpf-component-gallery-control-item-modal-image-info {
|
||||
position: sticky;
|
||||
top: 30px;
|
||||
flex: 1;
|
||||
width: 230px;
|
||||
margin-bottom: 0;
|
||||
|
||||
+ div {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-gallery-control-item-modal-image-info-sticky-bottom {
|
||||
position: sticky;
|
||||
top: auto;
|
||||
bottom: 30px;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
import $ from 'jquery';
|
||||
|
||||
import { Button, Spinner } from '@wordpress/components';
|
||||
import { Component, RawHTML } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const { ajaxurl, VPGutenbergVariables } = window;
|
||||
|
||||
const cachedOptions = {};
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class IconsSelector extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
const { callback } = this.props;
|
||||
|
||||
this.state = {
|
||||
options: { ...this.props.options },
|
||||
ajaxStatus: !!callback,
|
||||
collapsed: true,
|
||||
};
|
||||
|
||||
cachedOptions[this.props.controlName] = { ...this.props.options };
|
||||
|
||||
this.requestAjax = this.requestAjax.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { callback } = this.props;
|
||||
|
||||
if (callback) {
|
||||
this.requestAjax({}, (result) => {
|
||||
if (result.options) {
|
||||
this.setState({
|
||||
options: result.options,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request AJAX dynamic data.
|
||||
*
|
||||
* @param {Object} additionalData - additional data for AJAX call.
|
||||
* @param {Function} callback - callback.
|
||||
* @param {boolean} useStateLoading - use state change when loading.
|
||||
*/
|
||||
requestAjax(
|
||||
additionalData = {},
|
||||
callback = () => {},
|
||||
useStateLoading = true
|
||||
) {
|
||||
const { controlName, attributes } = this.props;
|
||||
|
||||
if (this.isAJAXinProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAJAXinProgress = true;
|
||||
|
||||
if (useStateLoading) {
|
||||
this.setState({
|
||||
ajaxStatus: 'progress',
|
||||
});
|
||||
}
|
||||
|
||||
const ajaxData = {
|
||||
action: 'vp_dynamic_control_callback',
|
||||
nonce: VPGutenbergVariables.nonce,
|
||||
vp_control_name: controlName,
|
||||
vp_attributes: attributes,
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
complete: (data) => {
|
||||
const json = data.responseJSON;
|
||||
|
||||
if (callback && json.response) {
|
||||
if (json.response.options) {
|
||||
cachedOptions[controlName] = {
|
||||
...cachedOptions[controlName],
|
||||
...json.response.options,
|
||||
};
|
||||
}
|
||||
|
||||
callback(json.response);
|
||||
}
|
||||
|
||||
if (useStateLoading) {
|
||||
this.setState({
|
||||
ajaxStatus: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.isAJAXinProgress = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { controlName, value, onChange, collapseRows, isSetupWizard } =
|
||||
this.props;
|
||||
|
||||
const { options, ajaxStatus, collapsed } = this.state;
|
||||
|
||||
const isLoading = ajaxStatus && ajaxStatus === 'progress';
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="vpf-component-icon-selector">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const optionsArray = Object.keys(options);
|
||||
const fromIndex = optionsArray.indexOf(value);
|
||||
|
||||
const itemsPerRow = isSetupWizard ? 5 : 3;
|
||||
const allowedItems = collapseRows * itemsPerRow;
|
||||
const allowCollapsing =
|
||||
collapseRows !== false && optionsArray.length > allowedItems;
|
||||
const visibleCollapsedItems = allowedItems - 1;
|
||||
|
||||
// Move the selected option to the end of collapsed list
|
||||
// in case this item is not visible.
|
||||
if (
|
||||
allowCollapsing &&
|
||||
collapsed &&
|
||||
fromIndex >= visibleCollapsedItems
|
||||
) {
|
||||
const toIndex = visibleCollapsedItems - 1;
|
||||
const element = optionsArray[fromIndex];
|
||||
optionsArray.splice(fromIndex, 1);
|
||||
optionsArray.splice(toIndex, 0, element);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'vpf-component-icon-selector',
|
||||
allowCollapsing
|
||||
? 'vpf-component-icon-selector-allow-collapsing'
|
||||
: ''
|
||||
)}
|
||||
data-control-name={controlName}
|
||||
>
|
||||
{optionsArray
|
||||
.filter((elm, i) => {
|
||||
if (allowCollapsing) {
|
||||
return collapsed ? i < visibleCollapsedItems : true;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((k) => {
|
||||
const option = options[k];
|
||||
let { icon } = option;
|
||||
|
||||
if (isSetupWizard) {
|
||||
if (option.image_preview_wizard) {
|
||||
icon = `<img src="${option.image_preview_wizard}" alt="${option.title} Preview">`;
|
||||
} else if (option.icon_wizard) {
|
||||
icon = option.icon_wizard;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={`icon-selector-${option.title}-${option.value}`}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={classnames(
|
||||
'vpf-component-icon-selector-item',
|
||||
value === option.value
|
||||
? 'vpf-component-icon-selector-item-active'
|
||||
: '',
|
||||
option.className
|
||||
)}
|
||||
>
|
||||
{icon ? <RawHTML>{icon}</RawHTML> : ''}
|
||||
{option.title ? (
|
||||
<span>{option.title}</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{allowCollapsing ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
collapsed: !collapsed,
|
||||
});
|
||||
}}
|
||||
className={classnames(
|
||||
'vpf-component-icon-selector-item',
|
||||
'vpf-component-icon-selector-item-collapse',
|
||||
collapsed
|
||||
? ''
|
||||
: 'vpf-component-icon-selector-item-expanded'
|
||||
)}
|
||||
>
|
||||
<div className="vpf-component-icon-selector-item-collapse">
|
||||
<svg
|
||||
width="11"
|
||||
height="6"
|
||||
viewBox="0 0 11 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 1.25L5.5 4.75L1 1.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
{collapsed
|
||||
? __('More', 'visual-portfolio')
|
||||
: __('Less', 'visual-portfolio')}
|
||||
</span>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
%vpf-icons-selector-item {
|
||||
position: relative;
|
||||
height: auto;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
color: #000;
|
||||
border-radius: 2px;
|
||||
transition: 0.1s color ease-in-out;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background: var(--wp-admin-theme-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition: 0.1s opacity ease-in-out;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--wp-admin-theme-color);
|
||||
|
||||
&::after {
|
||||
opacity: 0.04;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove default Gutenberg box shadow.
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
%vpf-icons-selector-item-active {
|
||||
color: var(--wp-admin-theme-color);
|
||||
outline: 1px solid var(--wp-admin-theme-color);
|
||||
|
||||
&::after {
|
||||
opacity: 0.04;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
@import "./selector-placeholder.scss";
|
||||
|
||||
.vpf-control-wrap-icons_selector:has(+ .vpf-control-wrap-category_tabs),
|
||||
.vpf-control-wrap-icons_selector:has(+ .vpf-control-wrap-category_collapse) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.vpf-control-wrap-icons_selector:has(+ .vpf-control-wrap-category_navigator) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vpf-component-icon-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-gap: 5px;
|
||||
width: 100%;
|
||||
|
||||
.vpf-component-icon-selector-item {
|
||||
@extend %vpf-icons-selector-item;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
transition: 0.2s border, 0.2s background-color, 0.2s box-shadow;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
max-width: 18px;
|
||||
height: auto;
|
||||
color: inherit;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
&.vpf-component-icon-selector-item-active {
|
||||
@extend %vpf-icons-selector-item-active;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-right: -8px;
|
||||
margin-left: -8px;
|
||||
font-size: 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
div + span {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-icon-selector-item-collapse {
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
.vpf-component-icon-selector-item-collapse {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: var(--wp-admin-theme-color);
|
||||
border-radius: 15px;
|
||||
opacity: 0.05;
|
||||
transition: 0.1s opacity ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.vpf-component-icon-selector-item-collapse::after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&.vpf-component-icon-selector-item-expanded {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,437 @@
|
||||
import './style.scss';
|
||||
import './live-reload-conditions';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
import iframeResizer from 'iframe-resizer/js/iframeResizer';
|
||||
import $ from 'jquery';
|
||||
import { isEqual, uniq } from 'lodash';
|
||||
import rafSchd from 'raf-schd';
|
||||
import { debounce, throttle } from 'throttle-debounce';
|
||||
|
||||
import { Spinner } from '@wordpress/components';
|
||||
import { dispatch, withSelect } from '@wordpress/data';
|
||||
import { Component, createRef, Fragment } from '@wordpress/element';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
|
||||
import getDynamicCSS, { hasDynamicCSS } from '../../utils/controls-dynamic-css';
|
||||
|
||||
const {
|
||||
VPAdminGutenbergVariables: variables,
|
||||
VPGutenbergVariables: { controls: registeredControls },
|
||||
} = window;
|
||||
|
||||
let uniqueIdCount = 1;
|
||||
|
||||
function getUpdatedKeys(oldData, newData) {
|
||||
const keys = uniq([...Object.keys(oldData), ...Object.keys(newData)]);
|
||||
const changedKeys = [];
|
||||
|
||||
keys.forEach((k) => {
|
||||
if (!isEqual(oldData[k], newData[k])) {
|
||||
changedKeys.push(k);
|
||||
}
|
||||
});
|
||||
|
||||
return changedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
class IframePreview extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
uniqueId: `vpf-preview-${uniqueIdCount}`,
|
||||
currentIframeHeight: 0,
|
||||
latestIframeHeight: 0,
|
||||
};
|
||||
|
||||
uniqueIdCount += 1;
|
||||
|
||||
this.frameRef = createRef();
|
||||
this.formRef = createRef();
|
||||
|
||||
this.maybePreviewTypeChanged = this.maybePreviewTypeChanged.bind(this);
|
||||
this.maybeAttributesChanged = this.maybeAttributesChanged.bind(this);
|
||||
this.onFrameLoad = this.onFrameLoad.bind(this);
|
||||
this.maybeReload = this.maybeReload.bind(this);
|
||||
this.maybeReloadDebounce = debounce(
|
||||
300,
|
||||
rafSchd(this.maybeReload.bind(this))
|
||||
);
|
||||
this.maybeResizePreviews = this.maybeResizePreviews.bind(this);
|
||||
this.maybeResizePreviewsThrottle = throttle(
|
||||
100,
|
||||
rafSchd(this.maybeResizePreviews)
|
||||
);
|
||||
this.updateIframeHeight = this.updateIframeHeight.bind(this);
|
||||
|
||||
this.updateIframeHeightThrottle = throttle(
|
||||
100,
|
||||
rafSchd(this.updateIframeHeight)
|
||||
);
|
||||
this.printInput = this.printInput.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const self = this;
|
||||
|
||||
const { clientId } = self.props;
|
||||
|
||||
iframeResizer(
|
||||
{
|
||||
interval: 10,
|
||||
warningTimeout: 60000,
|
||||
checkOrigin: false,
|
||||
onMessage({ message }) {
|
||||
// select current block on click message.
|
||||
if (message === 'clicked') {
|
||||
dispatch('core/block-editor').selectBlock(clientId);
|
||||
|
||||
window.focus();
|
||||
}
|
||||
},
|
||||
onResized({ height }) {
|
||||
self.updateIframeHeightThrottle(`${height}px`);
|
||||
},
|
||||
},
|
||||
self.frameRef.current
|
||||
);
|
||||
|
||||
self.frameRef.current.addEventListener('load', self.onFrameLoad);
|
||||
window.addEventListener('resize', self.maybeResizePreviewsThrottle);
|
||||
|
||||
self.maybeReload();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.maybePreviewTypeChanged(prevProps);
|
||||
this.maybeAttributesChanged(prevProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.frameRef.current.removeEventListener('load', this.onFrameLoad);
|
||||
window.removeEventListener('resize', this.maybeResizePreviewsThrottle);
|
||||
|
||||
if (this.frameRef.current.iframeResizer) {
|
||||
this.frameRef.current.iframeResizer.close();
|
||||
this.frameRef.current.iframeResizer.removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On frame load event.
|
||||
*
|
||||
* @param {Object} e - event data.
|
||||
*/
|
||||
onFrameLoad(e) {
|
||||
this.frameWindow = e.target.contentWindow;
|
||||
this.frameJQuery = e.target.contentWindow.jQuery;
|
||||
|
||||
if (this.frameJQuery) {
|
||||
this.$framePortfolio = this.frameJQuery('.vp-portfolio');
|
||||
|
||||
this.maybeResizePreviews();
|
||||
|
||||
if (this.frameTimeout) {
|
||||
clearTimeout(this.frameTimeout);
|
||||
}
|
||||
|
||||
// We need this timeout, since we resize iframe size and layouts resized with transitions.
|
||||
this.frameTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
maybePreviewTypeChanged(prevProps) {
|
||||
if (prevProps.previewDeviceType === this.props.previewDeviceType) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.maybeResizePreviews();
|
||||
}
|
||||
|
||||
maybeAttributesChanged(prevProps) {
|
||||
if (this.busyReload) {
|
||||
return;
|
||||
}
|
||||
this.busyReload = true;
|
||||
|
||||
const { attributes: newAttributes } = this.props;
|
||||
|
||||
const { attributes: oldAttributes } = prevProps;
|
||||
|
||||
const frame = this.frameRef.current;
|
||||
|
||||
const changedAttributes = {};
|
||||
const changedAttributeKeys = getUpdatedKeys(
|
||||
oldAttributes,
|
||||
newAttributes
|
||||
);
|
||||
|
||||
// check changed attributes.
|
||||
changedAttributeKeys.forEach((name) => {
|
||||
if (typeof newAttributes[name] !== 'undefined') {
|
||||
changedAttributes[name] = newAttributes[name];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(changedAttributes).length) {
|
||||
let reload = false;
|
||||
|
||||
Object.keys(changedAttributes).forEach((name) => {
|
||||
// Don't reload if block has dynamic styles.
|
||||
const hasStyles = hasDynamicCSS(name);
|
||||
|
||||
// Don't reload if reloading disabled in control attributes.
|
||||
const hasReloadAttribute =
|
||||
registeredControls[name] &&
|
||||
registeredControls[name].reload_iframe;
|
||||
|
||||
reload = reload || (!hasStyles && hasReloadAttribute);
|
||||
});
|
||||
|
||||
const data = applyFilters('vpf.editor.changed-attributes', {
|
||||
attributes: changedAttributes,
|
||||
reload,
|
||||
$frame: this.frameRef.current,
|
||||
frameWindow: this.frameWindow,
|
||||
frameJQuery: this.frameJQuery,
|
||||
$framePortfolio: this.$framePortfolio,
|
||||
});
|
||||
|
||||
if (!data.reload) {
|
||||
// Update AJAX dynamic data.
|
||||
if (data.frameWindow && data.frameWindow.vp_preview_post_data) {
|
||||
data.frameWindow.vp_preview_post_data[data.name] =
|
||||
data.value;
|
||||
}
|
||||
|
||||
// Insert dynamic CSS.
|
||||
if (frame.iFrameResizer && newAttributes.block_id) {
|
||||
frame.iFrameResizer.sendMessage({
|
||||
name: 'dynamic-css',
|
||||
blockId: newAttributes.block_id,
|
||||
styles: getDynamicCSS(newAttributes),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.reload) {
|
||||
this.maybeReloadDebounce();
|
||||
}
|
||||
this.busyReload = false;
|
||||
} else {
|
||||
this.busyReload = false;
|
||||
}
|
||||
}
|
||||
|
||||
maybeReload() {
|
||||
let latestIframeHeight = 0;
|
||||
|
||||
if (this.frameRef.current) {
|
||||
latestIframeHeight = this.state.currentIframeHeight;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
latestIframeHeight,
|
||||
});
|
||||
this.formRef.current.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize frame to properly work with @media.
|
||||
*/
|
||||
maybeResizePreviews() {
|
||||
const contentWidth = $(
|
||||
'.editor-styles-wrapper, .edit-post-visual-editor__content-area'
|
||||
)
|
||||
.eq(0)
|
||||
.width();
|
||||
|
||||
if (!contentWidth || !this.frameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = this.frameRef.current;
|
||||
const $frame = $(frame);
|
||||
const parentWidth = $frame
|
||||
.closest('.visual-portfolio-gutenberg-preview')
|
||||
.width();
|
||||
|
||||
$frame.css({
|
||||
width: contentWidth,
|
||||
});
|
||||
|
||||
if (frame.iFrameResizer) {
|
||||
frame.iFrameResizer.sendMessage({
|
||||
name: 'resize',
|
||||
width: parentWidth,
|
||||
});
|
||||
frame.iFrameResizer.resize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update iframe height.
|
||||
*
|
||||
* @param newHeight
|
||||
*/
|
||||
updateIframeHeight(newHeight) {
|
||||
this.setState({
|
||||
currentIframeHeight: newHeight,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare form input for POST variables.
|
||||
*
|
||||
* @param {string} name - option name.
|
||||
* @param {Mixed} val - option value.
|
||||
*
|
||||
* @return {JSX} - form control.
|
||||
*/
|
||||
printInput(name, val) {
|
||||
const params = {
|
||||
type: 'text',
|
||||
name,
|
||||
value: val,
|
||||
readOnly: true,
|
||||
};
|
||||
|
||||
if (typeof val === 'number') {
|
||||
params.type = 'number';
|
||||
} else if (typeof val === 'boolean') {
|
||||
params.type = 'number';
|
||||
params.value = val ? 1 : 0;
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(val).map((i) => (
|
||||
<Fragment key={`${name}[${i}]`}>
|
||||
{this.printInput(`${name}[${i}]`, val[i])}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
params.value = params.value || '';
|
||||
}
|
||||
|
||||
return <input {...params} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attributes, postType, postId } = this.props;
|
||||
|
||||
const { loading, uniqueId, currentIframeHeight, latestIframeHeight } =
|
||||
this.state;
|
||||
|
||||
const { id, content_source: contentSource } = attributes;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'visual-portfolio-gutenberg-preview',
|
||||
loading ? 'visual-portfolio-gutenberg-preview-loading' : ''
|
||||
)}
|
||||
style={{
|
||||
height: loading ? latestIframeHeight : currentIframeHeight,
|
||||
}}
|
||||
>
|
||||
<div className="visual-portfolio-gutenberg-preview-inner">
|
||||
<form
|
||||
action={variables.preview_url}
|
||||
target={uniqueId}
|
||||
method="POST"
|
||||
style={{ display: 'none' }}
|
||||
ref={this.formRef}
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="vp_preview_frame"
|
||||
value="true"
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="vp_preview_type"
|
||||
value="gutenberg"
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="vp_preview_post_type"
|
||||
value={postType}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="vp_preview_post_id"
|
||||
value={postId}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="vp_preview_nonce"
|
||||
value={variables.nonce}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{contentSource === 'saved' ? (
|
||||
<input
|
||||
type="text"
|
||||
name="vp_id"
|
||||
value={id}
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{Object.keys(attributes).map((k) => {
|
||||
const val = attributes[k];
|
||||
|
||||
return (
|
||||
<Fragment key={`vp_${k}`}>
|
||||
{this.printInput(`vp_${k}`, val)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
<iframe
|
||||
title="vp-preview"
|
||||
id={uniqueId}
|
||||
name={uniqueId}
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
allowtransparency="true"
|
||||
ref={this.frameRef}
|
||||
/>
|
||||
</div>
|
||||
{loading ? <Spinner /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSelect((select) => {
|
||||
const { __experimentalGetPreviewDeviceType } =
|
||||
select('core/edit-post') || {};
|
||||
|
||||
const { getCurrentPost } = select('core/editor') || {};
|
||||
|
||||
return {
|
||||
previewDeviceType: __experimentalGetPreviewDeviceType
|
||||
? __experimentalGetPreviewDeviceType()
|
||||
: 'desktop',
|
||||
postType: getCurrentPost ? getCurrentPost().type : 'standard',
|
||||
postId: getCurrentPost ? getCurrentPost().id : 'widgets',
|
||||
};
|
||||
})(IframePreview);
|
@ -0,0 +1,152 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
// live reload
|
||||
addFilter(
|
||||
'vpf.editor.changed-attributes',
|
||||
'vpf/editor/changed-attributes/live-reload',
|
||||
(data) => {
|
||||
if (!data.$framePortfolio) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let reload = false;
|
||||
|
||||
Object.keys(data.attributes).forEach((name) => {
|
||||
const val = data.attributes[name];
|
||||
|
||||
switch (name) {
|
||||
case 'tiles_type':
|
||||
case 'masonry_columns':
|
||||
case 'masonry_images_aspect_ratio':
|
||||
case 'grid_columns':
|
||||
case 'grid_images_aspect_ratio':
|
||||
case 'justified_row_height':
|
||||
case 'justified_row_height_tolerance':
|
||||
case 'justified_max_rows_count':
|
||||
case 'justified_last_row':
|
||||
case 'slider_effect':
|
||||
case 'slider_speed':
|
||||
case 'slider_autoplay':
|
||||
case 'slider_autoplay_hover_pause':
|
||||
case 'slider_centered_slides':
|
||||
case 'slider_loop':
|
||||
case 'slider_free_mode':
|
||||
case 'slider_free_mode_sticky':
|
||||
case 'slider_bullets_dynamic':
|
||||
case 'items_gap':
|
||||
case 'items_gap_vertical': {
|
||||
data.$framePortfolio.attr(
|
||||
`data-vp-${name.replace(/_/g, '-')}`,
|
||||
val
|
||||
);
|
||||
data.$framePortfolio.vpf('init');
|
||||
|
||||
break;
|
||||
}
|
||||
case 'items_style_default__align':
|
||||
case 'items_style_fade__align':
|
||||
case 'items_style_fly__align':
|
||||
case 'items_style_emerge__align': {
|
||||
let allAlignClasses = '';
|
||||
|
||||
[
|
||||
'left',
|
||||
'center',
|
||||
'right',
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
].forEach((alignName) => {
|
||||
allAlignClasses += `${
|
||||
allAlignClasses ? ' ' : ''
|
||||
}vp-portfolio__item-align-${alignName}`;
|
||||
});
|
||||
|
||||
data.$framePortfolio
|
||||
.find('.vp-portfolio__item-overlay')
|
||||
.removeClass(allAlignClasses)
|
||||
.addClass(`vp-portfolio__item-align-${val}`);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'items_style_default__caption_text_align':
|
||||
case 'items_style_fade__caption_text_align':
|
||||
case 'items_style_fly__caption_text_align':
|
||||
case 'items_style_emerge__caption_text_align': {
|
||||
let allAlignClasses = '';
|
||||
|
||||
[
|
||||
'left',
|
||||
'center',
|
||||
'right',
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
].forEach((alignName) => {
|
||||
allAlignClasses += `${
|
||||
allAlignClasses ? ' ' : ''
|
||||
}vp-portfolio__item-caption-text-align-${alignName}`;
|
||||
});
|
||||
|
||||
data.$framePortfolio
|
||||
.find('.vp-portfolio__item-caption')
|
||||
.removeClass(allAlignClasses)
|
||||
.addClass(
|
||||
`vp-portfolio__item-caption-text-align-${val}`
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'items_style_default__overlay_text_align':
|
||||
case 'items_style_fade__overlay_text_align':
|
||||
case 'items_style_fly__overlay_text_align':
|
||||
case 'items_style_emerge__overlay_text_align': {
|
||||
let allAlignClasses = '';
|
||||
|
||||
[
|
||||
'left',
|
||||
'center',
|
||||
'right',
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
].forEach((alignName) => {
|
||||
allAlignClasses += `${
|
||||
allAlignClasses ? ' ' : ''
|
||||
}vp-portfolio__item-overlay-text-align-${alignName}`;
|
||||
});
|
||||
|
||||
data.$framePortfolio
|
||||
.find('.vp-portfolio__item-overlay')
|
||||
.removeClass(allAlignClasses)
|
||||
.addClass(
|
||||
`vp-portfolio__item-overlay-text-align-${val}`
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
// prevent some options reload
|
||||
case 'list_name':
|
||||
// no reload
|
||||
break;
|
||||
default:
|
||||
reload = reload || data.reload;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
reload,
|
||||
};
|
||||
}
|
||||
);
|
@ -0,0 +1,45 @@
|
||||
.visual-portfolio-gutenberg-preview {
|
||||
position: relative;
|
||||
min-height: 40px;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading.
|
||||
.visual-portfolio-gutenberg-preview-loading {
|
||||
min-height: 150px;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: rgba(139, 139, 150, 10%);
|
||||
}
|
||||
|
||||
iframe {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
> .visual-portfolio-gutenberg-preview-inner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
> .components-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
|
||||
const { Masonry } = window;
|
||||
|
||||
export default function MasonryWrapper(props) {
|
||||
const { options, children, ...restProps } = props;
|
||||
|
||||
const ref = useRef();
|
||||
|
||||
// Init.
|
||||
useEffect(() => {
|
||||
const instance = new Masonry(ref.current, options);
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
};
|
||||
}, [ref, options, children]);
|
||||
|
||||
return (
|
||||
<div ref={ref} {...restProps}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import './style.scss';
|
||||
|
||||
import {
|
||||
__experimentalNavigatorBackButton,
|
||||
__experimentalNavigatorButton,
|
||||
__experimentalNavigatorProvider,
|
||||
__experimentalNavigatorScreen,
|
||||
NavigatorBackButton as __stableNavigatorBackButton,
|
||||
NavigatorButton as __stableNavigatorButton,
|
||||
NavigatorProvider as __stableNavigatorProvider,
|
||||
NavigatorScreen as __stableNavigatorScreen,
|
||||
} from '@wordpress/components';
|
||||
|
||||
const NavigatorProvider =
|
||||
__stableNavigatorProvider || __experimentalNavigatorProvider;
|
||||
const NavigatorScreen =
|
||||
__stableNavigatorScreen || __experimentalNavigatorScreen;
|
||||
const NavigatorButton =
|
||||
__stableNavigatorButton || __experimentalNavigatorButton;
|
||||
const NavigatorBackButton =
|
||||
__stableNavigatorBackButton || __experimentalNavigatorBackButton;
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function NavigatorControl(props) {
|
||||
const { children, options } = props;
|
||||
|
||||
return (
|
||||
<NavigatorProvider
|
||||
initialPath="/"
|
||||
className="vpf-component-navigator-control"
|
||||
>
|
||||
<NavigatorScreen
|
||||
path="/"
|
||||
className="vpf-component-navigator-control-toggles"
|
||||
>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<NavigatorButton
|
||||
key={option.category}
|
||||
path={`/${option.category}`}
|
||||
>
|
||||
<span>{option.title}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z" />
|
||||
</svg>
|
||||
</NavigatorButton>
|
||||
);
|
||||
})}
|
||||
</NavigatorScreen>
|
||||
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<NavigatorScreen
|
||||
key={option.category}
|
||||
path={`/${option.category}`}
|
||||
>
|
||||
<div className="vpf-component-navigator-control-screen-title">
|
||||
<NavigatorBackButton>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M14.6 7l-1.2-1L8 12l5.4 6 1.2-1-4.6-5z" />
|
||||
</svg>
|
||||
</NavigatorBackButton>
|
||||
<span>{option.title}</span>
|
||||
</div>
|
||||
{children(option)}
|
||||
</NavigatorScreen>
|
||||
);
|
||||
})}
|
||||
</NavigatorProvider>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
.vpf-control-wrap + .vpf-control-wrap-category_navigator {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.vpf-control-wrap-category_navigator:has(+ .vpf-control-wrap) {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.vpf-component-navigator-control {
|
||||
// Fix overflow problem.
|
||||
margin: -16px;
|
||||
|
||||
> .components-navigator-screen {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// Toggle.
|
||||
.vpf-component-navigator-control-toggles > button {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
font-weight: 500;
|
||||
|
||||
svg {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
// Remove default Gutenberg box shadow.
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Screen title.
|
||||
.vpf-component-navigator-control-screen-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
|
||||
button {
|
||||
padding: 6px;
|
||||
margin-left: -12px;
|
||||
|
||||
// Remove default Gutenberg box shadow.
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Screen content.
|
||||
.components-navigator-screen > .components-panel__body {
|
||||
padding: 16px 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
// Fix panel padding.
|
||||
.components-panel__body {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.components-panel__body:has(+ .vpf-component-collapse-control-toggle) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Notice } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function NoticeComponent(props) {
|
||||
const { className, ...allProps } = props;
|
||||
|
||||
return (
|
||||
<Notice
|
||||
className={classnames('vpf-component-notice', className)}
|
||||
{...allProps}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
@import "../../variables";
|
||||
|
||||
.vpf-component-notice {
|
||||
margin-top: 20px;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
|
||||
&.is-info {
|
||||
color: $blue-50;
|
||||
background-color: rgba($blue-30, 0.1);
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import './style.scss';
|
||||
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class ProNote extends Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
children,
|
||||
contentBefore = '',
|
||||
contentAfter = '',
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="vpf-pro-note-wrapper">
|
||||
{contentBefore}
|
||||
<div className="vpf-pro-note">
|
||||
{title ? <h3>{title}</h3> : ''}
|
||||
{children ? <div>{children}</div> : ''}
|
||||
</div>
|
||||
{contentAfter}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Button Component Class
|
||||
*/
|
||||
ProNote.Button = class ProNoteButton extends Component {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<a className="vpf-pro-note-button" {...this.props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,83 @@
|
||||
|
||||
.vpf-pro-note {
|
||||
position: relative;
|
||||
max-width: 280px;
|
||||
padding: 20px;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
color: #b65e96;
|
||||
background: linear-gradient(to left, rgba(#743ad5, 20%), rgba(#d53a9d, 20%));
|
||||
|
||||
.vpf-setup-wizard-panel & {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
z-index: 0;
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: #fff6fc;
|
||||
}
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: #d53a9d;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
|
||||
li::before {
|
||||
display: inline;
|
||||
margin-bottom: 5px;
|
||||
content: "- ";
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-pro-note-description,
|
||||
.components-base-control__help {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: inline-block;
|
||||
padding: 7px 15px;
|
||||
text-decoration: none;
|
||||
background: linear-gradient(to left, #743ad5, #d53a9d);
|
||||
border-radius: 3px;
|
||||
transition: 0.2s filter ease, 0.2s transform ease;
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
filter: contrast(1.5) drop-shadow(0 3px 3px rgba(213, 58, 157, 30%));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,412 @@
|
||||
import './style.scss';
|
||||
|
||||
import selectStyles from 'gutenberg-react-select-styles';
|
||||
import $ from 'jquery';
|
||||
import rafSchd from 'raf-schd';
|
||||
import Select, { components } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import {
|
||||
SortableContainer,
|
||||
SortableElement,
|
||||
sortableHandle,
|
||||
} from 'react-sortable-hoc';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const { Option } = components;
|
||||
|
||||
const { ajaxurl, VPGutenbergVariables } = window;
|
||||
|
||||
const cachedOptions = {};
|
||||
|
||||
const SortableSelect = SortableContainer(Select);
|
||||
const SortableCreatableSelect = SortableContainer(CreatableSelect);
|
||||
const SortableAsyncSelect = SortableContainer(AsyncSelect);
|
||||
const SortableMultiValueLabel = sortableHandle((props) => (
|
||||
<components.MultiValueLabel {...props} />
|
||||
));
|
||||
const SortableMultiValue = SortableElement((props) => {
|
||||
// this prevents the menu from being opened/closed when the user clicks
|
||||
// on a value to begin dragging it. ideally, detecting a click (instead of
|
||||
// a drag) would still focus the control and toggle the menu, but that
|
||||
// requires some magic with refs that are out of scope for this example
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const innerProps = { ...props.innerProps, onMouseDown };
|
||||
return <components.MultiValue {...props} innerProps={innerProps} />;
|
||||
});
|
||||
|
||||
function arrayMove(array, from, to) {
|
||||
array = array.slice();
|
||||
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class VpfSelectControl extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
const { callback } = this.props;
|
||||
|
||||
this.state = {
|
||||
options: {},
|
||||
ajaxStatus: !!callback,
|
||||
};
|
||||
|
||||
this.getOptions = this.getOptions.bind(this);
|
||||
this.getDefaultValue = this.getDefaultValue.bind(this);
|
||||
this.findValueData = this.findValueData.bind(this);
|
||||
this.requestAjax = this.requestAjax.bind(this);
|
||||
this.requestAjaxDebounce = debounce(300, rafSchd(this.requestAjax));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { callback } = this.props;
|
||||
|
||||
if (callback) {
|
||||
this.requestAjax({}, (result) => {
|
||||
if (result.options) {
|
||||
this.setState({
|
||||
options: result.options,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options list.
|
||||
*
|
||||
* @return {Object} - options list for React Select.
|
||||
*/
|
||||
getOptions() {
|
||||
const { controlName } = this.props;
|
||||
|
||||
if (cachedOptions[controlName]) {
|
||||
return cachedOptions[controlName];
|
||||
}
|
||||
|
||||
return Object.keys(this.state.options).length
|
||||
? this.state.options
|
||||
: this.props.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value in to support React Select attribute.
|
||||
*
|
||||
* @return {Object} - value object for React Select.
|
||||
*/
|
||||
getDefaultValue() {
|
||||
const { value, isMultiple } = this.props;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (isMultiple) {
|
||||
if ((!value && typeof value !== 'string') || !value.length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = [];
|
||||
|
||||
value.forEach((innerVal) => {
|
||||
result.push(this.findValueData(innerVal));
|
||||
});
|
||||
} else {
|
||||
if (!value && typeof value !== 'string') {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this.findValueData(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find option data by value.
|
||||
*
|
||||
* @param {string} findVal - value.
|
||||
*
|
||||
* @return {Object | boolean} - value object.
|
||||
*/
|
||||
findValueData(findVal) {
|
||||
let result = {
|
||||
value: findVal,
|
||||
label: findVal,
|
||||
};
|
||||
|
||||
const options = this.getOptions();
|
||||
|
||||
// Find value in options.
|
||||
if (options) {
|
||||
Object.keys(options).forEach((val) => {
|
||||
const data = options[val];
|
||||
|
||||
if (val === findVal) {
|
||||
if (typeof data === 'string') {
|
||||
result.label = data;
|
||||
} else {
|
||||
result = data;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request AJAX dynamic data.
|
||||
*
|
||||
* @param {Object} additionalData - additional data for AJAX call.
|
||||
* @param {Function} callback - callback.
|
||||
* @param {boolean} useStateLoading - use state change when loading.
|
||||
*/
|
||||
requestAjax(
|
||||
additionalData = {},
|
||||
callback = () => {},
|
||||
useStateLoading = true
|
||||
) {
|
||||
const { controlName, attributes } = this.props;
|
||||
|
||||
if (this.isAJAXinProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAJAXinProgress = true;
|
||||
|
||||
if (useStateLoading) {
|
||||
this.setState({
|
||||
ajaxStatus: 'progress',
|
||||
});
|
||||
}
|
||||
|
||||
const ajaxData = {
|
||||
action: 'vp_dynamic_control_callback',
|
||||
nonce: VPGutenbergVariables.nonce,
|
||||
vp_control_name: controlName,
|
||||
vp_attributes: attributes,
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: ajaxData,
|
||||
complete: (data) => {
|
||||
const json = data.responseJSON;
|
||||
|
||||
if (callback && json.response) {
|
||||
if (json.response.options) {
|
||||
cachedOptions[controlName] = {
|
||||
...cachedOptions[controlName],
|
||||
...json.response.options,
|
||||
};
|
||||
}
|
||||
|
||||
callback(json.response);
|
||||
}
|
||||
|
||||
if (useStateLoading) {
|
||||
this.setState({
|
||||
ajaxStatus: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.isAJAXinProgress = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare options for React Select structure.
|
||||
*
|
||||
* @param {Object} options - options object.
|
||||
*
|
||||
* @return {Object} - prepared options.
|
||||
*/
|
||||
prepareOptions(options) {
|
||||
return Object.keys(options || {}).map((val) => {
|
||||
const option = options[val];
|
||||
let result = {
|
||||
value: val,
|
||||
label: options[val],
|
||||
};
|
||||
|
||||
if (typeof option === 'object') {
|
||||
result = { ...option };
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onChange, isMultiple, isSearchable, isCreatable, callback } =
|
||||
this.props;
|
||||
|
||||
const { ajaxStatus } = this.state;
|
||||
|
||||
const isAsync = !!callback && isSearchable;
|
||||
const isLoading = ajaxStatus && ajaxStatus === 'progress';
|
||||
|
||||
const selectProps = {
|
||||
// Test opened menu items:
|
||||
// menuIsOpen: true,
|
||||
className: 'vpf-component-select',
|
||||
styles: {
|
||||
...selectStyles,
|
||||
menuPortal: (styles) => {
|
||||
return {
|
||||
...styles,
|
||||
zIndex: 1000000,
|
||||
};
|
||||
},
|
||||
},
|
||||
menuPortalTarget: document.body,
|
||||
components: {
|
||||
Option(optionProps) {
|
||||
const { data } = optionProps;
|
||||
|
||||
return (
|
||||
<Option {...optionProps}>
|
||||
{typeof data.img !== 'undefined' ? (
|
||||
<div className="vpf-component-select-option-img">
|
||||
{data.img ? (
|
||||
<img src={data.img} alt={data.label} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<span className="vpf-component-select-option-label">
|
||||
{data.label}
|
||||
</span>
|
||||
{data.category ? (
|
||||
<div className="vpf-component-select-option-category">
|
||||
{data.category}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
},
|
||||
value: this.getDefaultValue(),
|
||||
options: this.prepareOptions(this.getOptions()),
|
||||
onChange(val) {
|
||||
if (isMultiple) {
|
||||
if (Array.isArray(val)) {
|
||||
const result = [];
|
||||
|
||||
val.forEach((innerVal) => {
|
||||
result.push(innerVal ? innerVal.value : '');
|
||||
});
|
||||
|
||||
onChange(result);
|
||||
} else {
|
||||
onChange([]);
|
||||
}
|
||||
} else {
|
||||
onChange(val ? val.value : '');
|
||||
}
|
||||
},
|
||||
isMulti: isMultiple,
|
||||
isSearchable,
|
||||
isLoading,
|
||||
isClearable: false,
|
||||
placeholder: isSearchable
|
||||
? __('Type to search…', 'visual-portfolio')
|
||||
: __('Select…', 'visual-portfolio'),
|
||||
};
|
||||
|
||||
// Multiple select.
|
||||
if (isMultiple) {
|
||||
selectProps.useDragHandle = true;
|
||||
selectProps.axis = 'xy';
|
||||
selectProps.onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
const newValue = arrayMove(
|
||||
this.getDefaultValue(),
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
selectProps.onChange(newValue);
|
||||
};
|
||||
selectProps.distance = 4;
|
||||
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
||||
selectProps.getHelperDimensions = ({ node }) =>
|
||||
node.getBoundingClientRect();
|
||||
selectProps.components.MultiValue = SortableMultiValue;
|
||||
selectProps.components.MultiValueLabel = SortableMultiValueLabel;
|
||||
|
||||
// prevent closing options dropdown after select.
|
||||
selectProps.closeMenuOnSelect = false;
|
||||
}
|
||||
|
||||
// Creatable select.
|
||||
if (isCreatable) {
|
||||
selectProps.placeholder = __(
|
||||
'Type and press Enter…',
|
||||
'visual-portfolio'
|
||||
);
|
||||
selectProps.isSearchable = true;
|
||||
|
||||
if (isMultiple) {
|
||||
return <SortableCreatableSelect {...selectProps} />;
|
||||
}
|
||||
|
||||
return <CreatableSelect {...selectProps} />;
|
||||
}
|
||||
|
||||
// Async select.
|
||||
if (isAsync) {
|
||||
selectProps.loadOptions = (inputValue, cb) => {
|
||||
this.requestAjaxDebounce(
|
||||
{ q: inputValue },
|
||||
(result) => {
|
||||
const newOptions = [];
|
||||
|
||||
if (result && result.options) {
|
||||
Object.keys(result.options).forEach((k) => {
|
||||
newOptions.push(result.options[k]);
|
||||
});
|
||||
}
|
||||
|
||||
cb(newOptions.length ? newOptions : null);
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
selectProps.cacheOptions = true;
|
||||
selectProps.defaultOptions = selectProps.options;
|
||||
|
||||
delete selectProps.options;
|
||||
delete selectProps.isLoading;
|
||||
|
||||
if (isMultiple) {
|
||||
return <SortableAsyncSelect {...selectProps} />;
|
||||
}
|
||||
|
||||
return <AsyncSelect {...selectProps} />;
|
||||
}
|
||||
|
||||
// Default select.
|
||||
if (isMultiple) {
|
||||
return <SortableSelect {...selectProps} />;
|
||||
}
|
||||
|
||||
return <Select {...selectProps} />;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
.vpf-component-select {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vpf-component-select-option-img {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 10%);
|
||||
border-radius: 3px;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-select-option-label {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.vpf-component-select-option-category {
|
||||
padding: 2px 10px;
|
||||
margin-left: 10px;
|
||||
background-color: rgba(0, 0, 0, 10%);
|
||||
border-radius: 3px;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
import './style.scss';
|
||||
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
const SortableItem = function ({ id, element, sourceOptions, items, props }) {
|
||||
const { allowDisablingOptions, onChange } = props;
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
isSorting,
|
||||
} = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition: isSorting ? transition : '',
|
||||
};
|
||||
|
||||
const label = sourceOptions[element];
|
||||
return (
|
||||
<li
|
||||
className={classnames(
|
||||
'vpf-component-sortable-item',
|
||||
isDragging ? 'vpf-component-sortable-item-dragging' : ''
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
>
|
||||
<span {...attributes} {...listeners}>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 4.99976H8V6.99976H10V4.99976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10 10.9998H8V12.9998H10V10.9998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10 16.9998H8V18.9998H10V16.9998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16 4.99976H14V6.99976H16V4.99976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16 10.9998H14V12.9998H16V10.9998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16 16.9998H14V18.9998H16V16.9998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
{label}
|
||||
{allowDisablingOptions ? (
|
||||
<Button
|
||||
className="vpf-component-sortable-delete"
|
||||
onClick={() => {
|
||||
const updateValue = [...items];
|
||||
const findIndex = items.indexOf(element);
|
||||
updateValue.splice(findIndex, 1);
|
||||
|
||||
onChange(updateValue);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20 12H4"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableList = function ({
|
||||
items,
|
||||
sourceOptions,
|
||||
classes,
|
||||
onSortEnd,
|
||||
props,
|
||||
}) {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
return (
|
||||
<ul className={classes}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over.id) {
|
||||
onSortEnd(
|
||||
items.indexOf(active.id),
|
||||
items.indexOf(over.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SortableContext
|
||||
items={items}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{items.map((value) => (
|
||||
<SortableItem
|
||||
key={`item-${value}`}
|
||||
id={value}
|
||||
element={value}
|
||||
sourceOptions={sourceOptions}
|
||||
props={props}
|
||||
items={items}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class SortableControl extends Component {
|
||||
render() {
|
||||
const { options, defaultOptions, allowDisablingOptions, onChange } =
|
||||
this.props;
|
||||
|
||||
let { value } = this.props;
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
value = typeof defaultOptions !== 'undefined' ? defaultOptions : [];
|
||||
}
|
||||
|
||||
const disabledOptions = Object.keys(options).filter(
|
||||
(findValue) => !value.includes(findValue)
|
||||
);
|
||||
const classes = classnames(
|
||||
'vpf-component-sortable',
|
||||
disabledOptions.length > 0
|
||||
? 'vpf-dragging-has-disabled-options'
|
||||
: ''
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SortableList
|
||||
items={value}
|
||||
sourceOptions={options}
|
||||
classes={classes}
|
||||
props={this.props}
|
||||
onSortEnd={(oldIndex, newIndex) => {
|
||||
const updateValue = arrayMove(
|
||||
value,
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
onChange(updateValue);
|
||||
}}
|
||||
/>
|
||||
{disabledOptions.length > 0 ? (
|
||||
<ul className="vpf-component-sortable-disabled">
|
||||
{disabledOptions.map((el) => (
|
||||
<li key={`disabled-item-${el}`}>
|
||||
{allowDisablingOptions ? (
|
||||
<Button
|
||||
className="vpf-component-sortable-add"
|
||||
onClick={() => {
|
||||
const updateValue = [...value];
|
||||
|
||||
updateValue.push(el);
|
||||
|
||||
onChange(updateValue);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
) : null}
|
||||
{options[el]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
.vpf-component-sortable,
|
||||
.vpf-component-sortable-disabled {
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
border: 1px solid #7e8993;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.vpf-dragging-has-disabled-options {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.vpf-component-sortable > li {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
cursor: grab;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .vpf-component-sortable-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.vpf-component-sortable-item-dragging {
|
||||
z-index: 1;
|
||||
background-color: #f7f7f7;
|
||||
|
||||
.vpf-component-sortable-delete {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-sortable + ul.vpf-component-sortable-disabled {
|
||||
padding: 0 0 4px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background-color: #f6f6f6;
|
||||
border: 1px solid #7e8993;
|
||||
border-top-color: #dcdcdc;
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding: 3px 8px;
|
||||
margin: 0;
|
||||
color: #0000007a;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-sortable-delete,
|
||||
.vpf-component-sortable-add {
|
||||
display: inline-flex;
|
||||
height: 28px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 11px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-sortable-delete {
|
||||
padding: 0 5px;
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
|
||||
svg {
|
||||
width: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-sortable-add {
|
||||
padding: 0 4px;
|
||||
margin-right: 7px;
|
||||
color: #000;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import './style.scss';
|
||||
|
||||
import { Spinner } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class SpinnerComponent extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="vpf-component-spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
.vpf-component-spinner .components-spinner {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Render dynamic styles for editor.
|
||||
*
|
||||
* @param root0
|
||||
* @param root0.children
|
||||
* @return {null} nothing.
|
||||
*/
|
||||
export default function StylesRender({ children }) {
|
||||
useEffect(() => {
|
||||
const node = document.createElement('style');
|
||||
node.innerHTML = children;
|
||||
document.body.appendChild(node);
|
||||
|
||||
return () => document.body.removeChild(node);
|
||||
}, [children]);
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import './style.scss';
|
||||
|
||||
import { TabPanel } from '@wordpress/components';
|
||||
import { Component, RawHTML } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class TabsControl extends Component {
|
||||
render() {
|
||||
const { onChange, children, options } = this.props;
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="vpf-component-tabs-control"
|
||||
onSelect={onChange}
|
||||
tabs={options.map((item) => {
|
||||
return {
|
||||
name: item.category,
|
||||
title: item.title,
|
||||
icon: item.icon ? <RawHTML>{item.icon}</RawHTML> : null,
|
||||
};
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.vpf-component-tabs-control {
|
||||
margin: 0 -16px;
|
||||
margin-bottom: -26px;
|
||||
|
||||
.components-tab-panel__tabs-item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import MasonryWrapper from '../masonry-wrapper';
|
||||
import StylesRender from '../styles-render';
|
||||
import ToggleModal from '../toggle-modal';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class TilesSelector extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.renderPreview = this.renderPreview.bind(this);
|
||||
}
|
||||
|
||||
renderPreview(tilesType) {
|
||||
const settings = tilesType.split(/[:|]/);
|
||||
const selector = `[data-tiles-preview="${tilesType}"]`;
|
||||
let styles = '';
|
||||
|
||||
// remove last empty item
|
||||
if (
|
||||
typeof settings[settings.length - 1] !== 'undefined' &&
|
||||
!settings[settings.length - 1]
|
||||
) {
|
||||
settings.pop();
|
||||
}
|
||||
|
||||
// get columns number
|
||||
const columns = parseInt(settings[0], 10) || 1;
|
||||
settings.shift();
|
||||
|
||||
// set columns
|
||||
styles += `${selector} .vpf-tiles-preview-item-wrap { width: ${
|
||||
100 / columns
|
||||
}%; }`;
|
||||
|
||||
// set items sizes
|
||||
if (settings && settings.length) {
|
||||
for (let k = 0; k < settings.length; k += 1) {
|
||||
const size = settings[k].split(',');
|
||||
const w = parseFloat(size[0]) || 1;
|
||||
const h = parseFloat(size[1]) || 1;
|
||||
|
||||
let itemSelector = '.vpf-tiles-preview-item-wrap';
|
||||
if (settings.length > 1) {
|
||||
itemSelector += `:nth-of-type(${settings.length}n+${
|
||||
k + 1
|
||||
})`;
|
||||
}
|
||||
|
||||
if (w && w !== 1) {
|
||||
styles += `${selector} ${itemSelector} { width: ${
|
||||
(w * 100) / columns
|
||||
}%; }`;
|
||||
}
|
||||
styles += `${selector} ${itemSelector} .vpf-tiles-preview-item::after { padding-top: ${
|
||||
h * 100
|
||||
}%; }`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StylesRender>{styles}</StylesRender>
|
||||
<MasonryWrapper
|
||||
data-tiles-preview={tilesType}
|
||||
options={{
|
||||
transitionDuration: 0,
|
||||
}}
|
||||
>
|
||||
{Array(...Array(4 * columns)).map((i) => (
|
||||
<div key={i} className="vpf-tiles-preview-item-wrap">
|
||||
<div className="vpf-tiles-preview-item" />
|
||||
</div>
|
||||
))}
|
||||
</MasonryWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, options, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div className="vpf-component-tiles-selector">
|
||||
<ToggleModal
|
||||
modalTitle={__('Tiles', 'visual-portfolio')}
|
||||
buttonLabel={__('Edit Tiles', 'visual-portfolio')}
|
||||
>
|
||||
<div className="vpf-component-tiles-selector-items">
|
||||
{options.map((data) => (
|
||||
<Button
|
||||
key={data.value}
|
||||
onClick={() => onChange(data.value)}
|
||||
className={classnames(
|
||||
'vpf-tiles-preview-button',
|
||||
value === data.value
|
||||
? 'vpf-tiles-preview-button-active'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{this.renderPreview(data.value)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ToggleModal>
|
||||
<div className="vpf-tiles-preview-button">
|
||||
{this.renderPreview(value)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
@import "../icons-selector/selector-placeholder.scss";
|
||||
|
||||
$tiles_selector_gap: 5px !default;
|
||||
$tiles_selector_button_padding: 10px !default;
|
||||
|
||||
.vpf-component-tiles-selector,
|
||||
.vpf-component-tiles-selector-items {
|
||||
.vpf-tiles-preview-button {
|
||||
@extend %vpf-icons-selector-item;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
width: 33.33%;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1.25;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #efefef !important;
|
||||
}
|
||||
|
||||
.vpf-tiles-preview-button-active {
|
||||
@extend %vpf-icons-selector-item-active;
|
||||
}
|
||||
|
||||
[data-tiles-preview] {
|
||||
flex: 1;
|
||||
margin-top: $tiles_selector_button_padding - $tiles_selector_gap;
|
||||
margin-right: $tiles_selector_button_padding;
|
||||
margin-left: $tiles_selector_button_padding - $tiles_selector_gap;
|
||||
}
|
||||
|
||||
.vpf-tiles-preview-item-wrap {
|
||||
float: left;
|
||||
padding-top: $tiles_selector_gap;
|
||||
padding-left: $tiles_selector_gap;
|
||||
}
|
||||
|
||||
.vpf-tiles-preview-item {
|
||||
position: relative;
|
||||
background-color: #cacaca;
|
||||
border-radius: 2px;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-tiles-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.components-button {
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
margin-right: auto;
|
||||
color: #212121 !important;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
background: #f3f3f3;
|
||||
border-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.vpf-tiles-preview-button {
|
||||
flex: 0 0 80px;
|
||||
width: 80px;
|
||||
margin-left: 10px;
|
||||
pointer-events: none;
|
||||
background: #fff !important;
|
||||
border: 1px solid #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
.vpf-component-tiles-selector-items {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-gap: 15px;
|
||||
width: 510px;
|
||||
max-width: 100%;
|
||||
margin: 12px 0;
|
||||
|
||||
.vpf-tiles-preview-button {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import './style.scss';
|
||||
|
||||
import {
|
||||
__experimentalToggleGroupControl,
|
||||
__experimentalToggleGroupControlOption,
|
||||
ToggleGroupControl as __stableToggleGroupControl,
|
||||
ToggleGroupControlOption as __stableToggleGroupControlOption,
|
||||
} from '@wordpress/components';
|
||||
import { Fragment, useState } from '@wordpress/element';
|
||||
|
||||
const ToggleGroupControl =
|
||||
__stableToggleGroupControl || __experimentalToggleGroupControl;
|
||||
const ToggleGroupControlOption =
|
||||
__stableToggleGroupControlOption || __experimentalToggleGroupControlOption;
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function ToggleGroupCustomControl(props) {
|
||||
const { children, options } = props;
|
||||
|
||||
const [collapsed, setCollapsed] = useState(options[0].category);
|
||||
|
||||
return (
|
||||
<div className="vpf-component-toggle-group-control">
|
||||
<ToggleGroupControl
|
||||
className="vpf-component-toggle-group-control-toggle"
|
||||
value={collapsed}
|
||||
onChange={(val) => {
|
||||
setCollapsed(val);
|
||||
}}
|
||||
isBlock
|
||||
>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<ToggleGroupControlOption
|
||||
key={option.category}
|
||||
value={option.category}
|
||||
label={option.title}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ToggleGroupControl>
|
||||
|
||||
{options.map((option) => {
|
||||
if (collapsed === option.category) {
|
||||
return (
|
||||
<Fragment key={option.category}>
|
||||
{children(option)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
.vpf-component-toggle-group-control {
|
||||
> .components-base-control > .components-base-control__field {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> .components-panel__body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import './style.scss';
|
||||
|
||||
import classnames from 'classnames/dedupe';
|
||||
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Component Class
|
||||
*/
|
||||
export default class ToggleModal extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
isOpened: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, modalTitle, buttonLabel, size } = this.props;
|
||||
|
||||
const { isOpened } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={() => this.setState({ isOpened: !isOpened })}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
{isOpened ? (
|
||||
<Modal
|
||||
title={modalTitle}
|
||||
onRequestClose={() =>
|
||||
this.setState({ isOpened: !isOpened })
|
||||
}
|
||||
className={classnames(
|
||||
'vpf-component-modal',
|
||||
size ? `vpf-component-modal-size-${size}` : ''
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
.vpf-component-modal {
|
||||
&.vpf-component-modal-size-sm {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
&.vpf-component-modal-size-md {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
// size
|
||||
@media (min-width: 840px) {
|
||||
&.vpf-component-modal-size-lg {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
import './custom-post-meta/video';
|
||||
import './custom-post-meta/image-focal-point';
|
@ -0,0 +1,113 @@
|
||||
import {
|
||||
__experimentalUnitControl,
|
||||
PanelRow,
|
||||
UnitControl as __stableUnitControl,
|
||||
} from '@wordpress/components';
|
||||
import { compose, withInstanceId } from '@wordpress/compose';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const UnitControl = __stableUnitControl || __experimentalUnitControl;
|
||||
|
||||
/**
|
||||
* Component
|
||||
*/
|
||||
class VpImageFocalPointComponent extends Component {
|
||||
render() {
|
||||
const { getMeta, featuredImageId, updateMeta } = this.props;
|
||||
|
||||
if (!featuredImageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let focalPoint = getMeta('_vp_image_focal_point');
|
||||
|
||||
if (!focalPoint || !focalPoint.x || !focalPoint.y) {
|
||||
focalPoint = {
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vpf-post-image-focal-point-panel">
|
||||
<PanelRow>
|
||||
<p className="description">
|
||||
{__(
|
||||
'Focal point will be used in Visual Portfolio layouts only:',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</p>
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<UnitControl
|
||||
label={__('Left', 'visual-portfolio')}
|
||||
value={100 * focalPoint.x + '%'}
|
||||
onChange={(val) => {
|
||||
const newFocalPoint = { ...focalPoint };
|
||||
newFocalPoint.x = parseFloat(val) / 100;
|
||||
|
||||
updateMeta('_vp_image_focal_point', newFocalPoint);
|
||||
}}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
units={[{ value: '%', label: '%' }]}
|
||||
/>
|
||||
<UnitControl
|
||||
label={__('Top', 'visual-portfolio')}
|
||||
value={100 * focalPoint.y + '%'}
|
||||
onChange={(val) => {
|
||||
const newFocalPoint = { ...focalPoint };
|
||||
newFocalPoint.y = parseFloat(val) / 100;
|
||||
|
||||
updateMeta('_vp_image_focal_point', newFocalPoint);
|
||||
}}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
units={[{ value: '%', label: '%' }]}
|
||||
/>
|
||||
</PanelRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VpImageFocalPoint = compose([
|
||||
withSelect((select) => {
|
||||
const { getEditedPostAttribute } = select('core/editor');
|
||||
|
||||
const featuredImageId = getEditedPostAttribute('featured_media');
|
||||
const meta = getEditedPostAttribute('meta') || {};
|
||||
|
||||
return {
|
||||
featuredImageId,
|
||||
getMeta(name) {
|
||||
return meta[name];
|
||||
},
|
||||
};
|
||||
}),
|
||||
withDispatch((dispatch) => ({
|
||||
updateMeta(name, val) {
|
||||
dispatch('core/editor').editPost({ meta: { [name]: val } });
|
||||
},
|
||||
})),
|
||||
withInstanceId,
|
||||
])(VpImageFocalPointComponent);
|
||||
|
||||
addFilter(
|
||||
'editor.PostFeaturedImage',
|
||||
'vpf/post-featured-image-focal-point',
|
||||
(OriginalComponent) =>
|
||||
function (props) {
|
||||
return (
|
||||
<>
|
||||
<OriginalComponent {...props} />
|
||||
<VpImageFocalPoint />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
@ -0,0 +1,198 @@
|
||||
import $ from 'jquery';
|
||||
import rafSchd from 'raf-schd';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
import { PanelRow, TextControl } from '@wordpress/components';
|
||||
import { compose, withInstanceId } from '@wordpress/compose';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
|
||||
const { ajaxurl, VPGutenbergMetaVariables } = window;
|
||||
|
||||
/**
|
||||
* Component
|
||||
*/
|
||||
class VpVideoComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
oembedQuery: '',
|
||||
oembedHTML: '',
|
||||
};
|
||||
|
||||
this.maybePrepareOembed = debounce(
|
||||
300,
|
||||
rafSchd(this.maybePrepareOembed.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybePrepareOembed();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.maybePrepareOembed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare oEmbed HTML.
|
||||
*/
|
||||
maybePrepareOembed() {
|
||||
const { oembedQuery, oembedHTML } = this.state;
|
||||
|
||||
const { getMeta, postFormat } = this.props;
|
||||
|
||||
if (postFormat !== 'video') {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoUrl = getMeta('_vp_format_video_url');
|
||||
|
||||
if (oembedQuery === videoUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort AJAX.
|
||||
if (this.oembedAjax && this.oembedAjax.abort) {
|
||||
this.oembedAjax.abort();
|
||||
}
|
||||
|
||||
if (!oembedQuery && oembedHTML) {
|
||||
this.setState({
|
||||
oembedHTML: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.oembedAjax = $.ajax({
|
||||
url: ajaxurl,
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
action: 'vp_find_oembed',
|
||||
q: videoUrl,
|
||||
nonce: VPGutenbergMetaVariables.nonce,
|
||||
},
|
||||
complete: (data) => {
|
||||
const json = data.responseJSON;
|
||||
const newState = {
|
||||
oembedQuery: videoUrl,
|
||||
oembedHTML: '',
|
||||
};
|
||||
|
||||
if (json && typeof json.html !== 'undefined') {
|
||||
newState.oembedHTML = json.html;
|
||||
}
|
||||
this.setState(newState);
|
||||
|
||||
this.oembedAjax = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getMeta, postFormat, updateMeta } = this.props;
|
||||
|
||||
const { oembedHTML } = this.state;
|
||||
|
||||
if (postFormat !== 'video') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PluginDocumentSettingPanel
|
||||
name="VPVideo"
|
||||
title={__('Video', 'visual-portfolio')}
|
||||
icon={
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.25 10C19.25 15.1086 15.1086 19.25 10 19.25C4.89137 19.25 0.75 15.1086 0.75 10C0.75 4.89137 4.89137 0.75 10 0.75C15.1086 0.75 19.25 4.89137 19.25 10Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
d="M8 12.6326V7.36671C8.00011 7.30082 8.01856 7.23618 8.05342 7.17955C8.08828 7.12293 8.13826 7.0764 8.19812 7.04485C8.25798 7.0133 8.32552 6.99789 8.39367 7.00023C8.46181 7.00257 8.52805 7.02258 8.58544 7.05816L12.8249 9.69035C12.8786 9.72358 12.9228 9.76933 12.9534 9.82337C12.984 9.87742 13 9.93803 13 9.99963C13 10.0612 12.984 10.1218 12.9534 10.1759C12.9228 10.2299 12.8786 10.2757 12.8249 10.3089L8.58544 12.9418C8.52805 12.9774 8.46181 12.9974 8.39367 12.9998C8.32552 13.0021 8.25798 12.9867 8.19812 12.9551C8.13826 12.9236 8.08828 12.8771 8.05342 12.8204C8.01856 12.7638 8.00011 12.6992 8 12.6333V12.6326Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
className="vpf-meta-video-panel"
|
||||
>
|
||||
<PanelRow>
|
||||
<p className="description">
|
||||
{sprintf(
|
||||
__(
|
||||
'Video will be used in %s layouts only. Full list of supported links',
|
||||
'visual-portfolio'
|
||||
),
|
||||
VPGutenbergMetaVariables.plugin_name
|
||||
)}
|
||||
|
||||
<a
|
||||
href="https://visualportfolio.co/docs/projects/video-project/#supported-video-vendors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__('see here', 'visual-portfolio')}
|
||||
</a>
|
||||
</p>
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<TextControl
|
||||
label={__('Video URL', 'visual-portfolio')}
|
||||
value={getMeta('_vp_format_video_url') || ''}
|
||||
onChange={(val) => {
|
||||
updateMeta('_vp_format_video_url', val);
|
||||
}}
|
||||
type="url"
|
||||
placeholder="https://"
|
||||
/>
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<div
|
||||
className="vp-oembed-preview"
|
||||
dangerouslySetInnerHTML={{ __html: oembedHTML }}
|
||||
/>
|
||||
</PanelRow>
|
||||
</PluginDocumentSettingPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VpVideo = compose([
|
||||
withSelect((select) => ({
|
||||
getMeta(name) {
|
||||
const meta =
|
||||
select('core/editor').getEditedPostAttribute('meta') || {};
|
||||
return meta[name];
|
||||
},
|
||||
postFormat: select('core/editor').getEditedPostAttribute('format'),
|
||||
})),
|
||||
withDispatch((dispatch) => ({
|
||||
updateMeta(name, val) {
|
||||
dispatch('core/editor').editPost({ meta: { [name]: val } });
|
||||
},
|
||||
})),
|
||||
withInstanceId,
|
||||
])(VpVideoComponent);
|
||||
|
||||
// Check if editPost available.
|
||||
// For example, on the Widgets screen this variable is not defined.
|
||||
if (wp.editPost) {
|
||||
registerPlugin('vp-video', {
|
||||
render: VpVideo,
|
||||
});
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import shorthash from 'shorthash';
|
||||
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { withSelect } from '@wordpress/data';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
// List of used IDs to prevent duplicates.
|
||||
const usedIds = {};
|
||||
|
||||
/**
|
||||
* Override the default edit UI to include a new block inspector control for
|
||||
* assigning the custom styles if needed.
|
||||
*
|
||||
* @param {Function | Component} BlockEdit Original component.
|
||||
*
|
||||
* @return {string} Wrapped component.
|
||||
*/
|
||||
const withUniqueBlockId = createHigherOrderComponent((BlockEdit) => {
|
||||
class newEdit extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
const { attributes, clientId } = this.props;
|
||||
|
||||
// fix duplicated classes after block clone.
|
||||
if (
|
||||
clientId &&
|
||||
attributes.block_id &&
|
||||
typeof usedIds[attributes.block_id] === 'undefined'
|
||||
) {
|
||||
usedIds[attributes.block_id] = clientId;
|
||||
}
|
||||
|
||||
this.maybeCreateBlockId = this.maybeCreateBlockId.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeCreateBlockId();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.maybeCreateBlockId();
|
||||
}
|
||||
|
||||
maybeCreateBlockId() {
|
||||
if (this.props.blockName !== 'visual-portfolio/block') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { setAttributes, attributes, clientId } = this.props;
|
||||
|
||||
const { block_id: blockId } = attributes;
|
||||
|
||||
if (!blockId || usedIds[blockId] !== clientId) {
|
||||
let newBlockId = '';
|
||||
|
||||
// check if ID already exist.
|
||||
let tryCount = 10;
|
||||
while (
|
||||
!newBlockId ||
|
||||
(typeof usedIds[newBlockId] !== 'undefined' &&
|
||||
usedIds[newBlockId] !== clientId &&
|
||||
tryCount > 0)
|
||||
) {
|
||||
newBlockId = shorthash.unique(clientId);
|
||||
tryCount -= 1;
|
||||
}
|
||||
|
||||
if (newBlockId && typeof usedIds[newBlockId] === 'undefined') {
|
||||
usedIds[newBlockId] = clientId;
|
||||
}
|
||||
|
||||
if (newBlockId !== blockId) {
|
||||
setAttributes({
|
||||
block_id: newBlockId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <BlockEdit {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
return withSelect((select, ownProps) => ({
|
||||
blockName: ownProps.name,
|
||||
}))(newEdit);
|
||||
}, 'withUniqueBlockId');
|
||||
|
||||
addFilter('editor.BlockEdit', 'vpf/editor/unique-block-id', withUniqueBlockId);
|
@ -0,0 +1,21 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Add overlay automatically for Classic style, when Display Icon option enabled.
|
||||
*/
|
||||
addFilter(
|
||||
'vpf.editor.controls-on-change',
|
||||
'vpf/editor/controls-on-change/classic-icon-with-overlay',
|
||||
(newAttributes, control, val, attributes) => {
|
||||
if (
|
||||
control.name === 'items_style_default__show_icon' &&
|
||||
val &&
|
||||
!attributes.items_style_default__bg_color
|
||||
) {
|
||||
newAttributes.items_style_default__bg_color = '#000';
|
||||
newAttributes.items_style_default__text_color = '#fff';
|
||||
}
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
);
|
@ -0,0 +1,259 @@
|
||||
import classnames from 'classnames/dedupe';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import {
|
||||
BaseControl,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
TextControl,
|
||||
} from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { RawHTML, useState } from '@wordpress/element';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import Notice from '../components/notice';
|
||||
import controlGetValue from '../utils/control-get-value';
|
||||
|
||||
const { VPGutenbergVariables } = window;
|
||||
|
||||
const NOTICE_LIMIT = parseInt(
|
||||
VPGutenbergVariables.items_count_notice_limit,
|
||||
10
|
||||
);
|
||||
const DISPLAY_NOTICE_AFTER = NOTICE_LIMIT + 5;
|
||||
|
||||
function getNoticeState() {
|
||||
return VPGutenbergVariables.items_count_notice;
|
||||
}
|
||||
|
||||
const maybeUpdateNoticeStateMeta = debounce(3000, (postId) => {
|
||||
apiFetch({
|
||||
path: '/visual-portfolio/v1/update_gallery_items_count_notice_state',
|
||||
method: 'POST',
|
||||
data: {
|
||||
notice_state: getNoticeState(),
|
||||
post_id: postId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function updateNoticeState(postId) {
|
||||
const newState = getNoticeState() === 'hide' ? 'show' : 'hide';
|
||||
|
||||
VPGutenbergVariables.items_count_notice = newState;
|
||||
|
||||
maybeUpdateNoticeStateMeta(postId);
|
||||
}
|
||||
|
||||
function CountNotice(props) {
|
||||
const { onToggle, postId } = props;
|
||||
|
||||
return (
|
||||
<Notice status="warning" isDismissible={false}>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: __(
|
||||
'Using large galleries may <u>decrease page loading speed</u>. We recommend you add these improvements:',
|
||||
'visual-portfolio'
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ol className="ol-decimal">
|
||||
<li
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sprintf(
|
||||
__(
|
||||
'Set the items per page to <u>less than %d</u>',
|
||||
'visual-portfolio'
|
||||
),
|
||||
NOTICE_LIMIT
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<li
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: __(
|
||||
'Add <em>`Load More`</em> or <em>`Infinite Scroll`</em> pagination for best results.',
|
||||
'visual-portfolio'
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ol>
|
||||
<p>
|
||||
<Button
|
||||
isLink
|
||||
onClick={() => {
|
||||
updateNoticeState(postId);
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
{__('Ok, I understand', 'visual-portfolio')}
|
||||
</Button>
|
||||
</p>
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
function shouldDisplayNotice(count, attributes) {
|
||||
let display = false;
|
||||
|
||||
// When selected images number is lower, then needed, don't display notice, even is count is large.
|
||||
if (attributes.content_source === 'images') {
|
||||
display =
|
||||
attributes?.images?.length > DISPLAY_NOTICE_AFTER &&
|
||||
(count > DISPLAY_NOTICE_AFTER || count === -1);
|
||||
} else {
|
||||
display = count > DISPLAY_NOTICE_AFTER || count === -1;
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
function ItemsCountControl({ data }) {
|
||||
const { description, attributes, onChange } = data;
|
||||
|
||||
const [maybeReRender, setMaybeReRender] = useState(1);
|
||||
|
||||
const { postId } = useSelect(
|
||||
(select) => ({
|
||||
postId: select('core/editor')?.getCurrentPostId() || false,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderControlHelp = description ? (
|
||||
<RawHTML>{description}</RawHTML>
|
||||
) : (
|
||||
false
|
||||
);
|
||||
const renderControlClassName = classnames(
|
||||
'vpf-control-wrap',
|
||||
`vpf-control-wrap-${data.type}`
|
||||
);
|
||||
const controlVal = parseInt(controlGetValue(data.name, attributes), 10);
|
||||
|
||||
return (
|
||||
<BaseControl
|
||||
id="vpf-control-items-count-all"
|
||||
label={
|
||||
<>
|
||||
{data.label}
|
||||
{getNoticeState() === 'hide' &&
|
||||
shouldDisplayNotice(controlVal, attributes) ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateNoticeState(postId);
|
||||
setMaybeReRender(maybeReRender + 1);
|
||||
}}
|
||||
isSmall
|
||||
style={{
|
||||
position: 'absolute',
|
||||
marginTop: '-5px',
|
||||
padding: '0 4px',
|
||||
color: '#cd7a0f',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="16"
|
||||
width="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="red"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
help={renderControlHelp}
|
||||
className={renderControlClassName}
|
||||
>
|
||||
<div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
isSmall
|
||||
isPrimary={controlVal !== -1}
|
||||
isPressed={controlVal !== -1}
|
||||
onClick={() => {
|
||||
if (controlVal === -1) {
|
||||
onChange(parseFloat(data.default || 6));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{__('Custom Count', 'visual-portfolio')}
|
||||
</Button>
|
||||
<Button
|
||||
isSmall
|
||||
isPrimary={controlVal === -1}
|
||||
isPressed={controlVal === -1}
|
||||
onClick={() => {
|
||||
if (
|
||||
controlVal !== -1 &&
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm(
|
||||
__(
|
||||
'Be careful, the output of all your items can adversely affect the performance of your site, this option may be helpful for image galleries.',
|
||||
'visual-portfolio'
|
||||
)
|
||||
)
|
||||
) {
|
||||
onChange(-1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{__('All Items', 'visual-portfolio')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{controlVal !== -1 ? (
|
||||
<>
|
||||
<br />
|
||||
<TextControl
|
||||
type="number"
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
step={data.step}
|
||||
value={controlVal}
|
||||
onChange={(val) => onChange(parseFloat(val))}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{getNoticeState() === 'show' &&
|
||||
shouldDisplayNotice(controlVal, attributes) ? (
|
||||
<div>
|
||||
<CountNotice
|
||||
postId={postId}
|
||||
onToggle={() => {
|
||||
setMaybeReRender(maybeReRender + 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</BaseControl>
|
||||
);
|
||||
}
|
||||
|
||||
// Items count with "All Items" button.
|
||||
addFilter(
|
||||
'vpf.editor.controls-render',
|
||||
'vpf/editor/controls-render/customize-controls',
|
||||
(render, data) => {
|
||||
if (data.name !== 'items_count') {
|
||||
return render;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemsCountControl
|
||||
// we should use key prop, since `vpf.editor.controls-render` will use the result in array.
|
||||
key={`control-${data.name}-${data.label}`}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
@ -0,0 +1,23 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
const NOOPENER_DEFAULT = 'noopener noreferrer';
|
||||
|
||||
addFilter(
|
||||
'vpf.editor.controls-on-change',
|
||||
'vpf/editor/controls-on-change/link-rel',
|
||||
(newAttributes, control, val, attributes) => {
|
||||
if (control.name === 'items_click_action_url_target') {
|
||||
if (val === '_blank' && !attributes.items_click_action_url_rel) {
|
||||
newAttributes.items_click_action_url_rel = NOOPENER_DEFAULT;
|
||||
}
|
||||
if (
|
||||
val !== '_blank' &&
|
||||
NOOPENER_DEFAULT === attributes.items_click_action_url_rel
|
||||
) {
|
||||
newAttributes.items_click_action_url_rel = '';
|
||||
}
|
||||
}
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
);
|
@ -0,0 +1,17 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
// Allow Stretch control on Saved Layouts editor only.
|
||||
addFilter(
|
||||
'vpf.editor.controls-render-data',
|
||||
'vpf/editor/controls-render-data/customize-controls',
|
||||
(data) => {
|
||||
if (data.name === 'stretch' && !window.VPSavedLayoutVariables) {
|
||||
data = {
|
||||
...data,
|
||||
skip: true,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
);
|
21
wp-content/plugins/visual-portfolio/gutenberg/index.js
Normal file
21
wp-content/plugins/visual-portfolio/gutenberg/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import './block';
|
||||
import './block-saved';
|
||||
import './extensions/block-id';
|
||||
import './extensions/classic-icon-with-overlay';
|
||||
import './extensions/items-count-all';
|
||||
import './extensions/link-rel';
|
||||
import './extensions/stretch-for-saved-only';
|
||||
import './store';
|
||||
import './components/dropdown';
|
||||
|
||||
import { registerBlockCollection } from '@wordpress/blocks';
|
||||
|
||||
import { ReactComponent as ElementIcon } from '../assets/admin/images/icon-gutenberg.svg';
|
||||
|
||||
const { plugin_name: pluginName } = window.VPGutenbergVariables;
|
||||
|
||||
// Collection.
|
||||
registerBlockCollection('visual-portfolio', {
|
||||
title: pluginName,
|
||||
icon: <ElementIcon width="20" height="20" />,
|
||||
});
|
@ -0,0 +1 @@
|
||||
import './layouts-editor/index';
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "visual-portfolio/saved-editor",
|
||||
"category": "common",
|
||||
"title": "Visual Portfolio Editor",
|
||||
"description": "Edit saved Visual Portfolio layouts.",
|
||||
"textdomain": "visual-portfolio",
|
||||
"supports": {
|
||||
"anchor": false,
|
||||
"className": false,
|
||||
"customClassName": false,
|
||||
"html": false,
|
||||
"inserter": false
|
||||
}
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Button, PanelBody } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const { navigator } = window;
|
||||
|
||||
let copiedTimeout;
|
||||
|
||||
function ShortcodeRender(props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="vpf-layout-shortcode-copy">
|
||||
<strong>{props.label}:</strong>
|
||||
<div>
|
||||
<pre>{props.content}</pre>
|
||||
<Button
|
||||
isSmall
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(props.content)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
|
||||
clearTimeout(copiedTimeout);
|
||||
|
||||
copiedTimeout = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 450);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
|
||||
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
|
||||
</svg>
|
||||
{copied ? (
|
||||
<div className="vpf-layout-shortcode-copied">
|
||||
{__('Copied!', 'visual-portfolio')}
|
||||
</div>
|
||||
) : null}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layouts Editor block
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
function LayoutsEditorBlock(props) {
|
||||
const { clientId } = props;
|
||||
|
||||
const [additionalShortcodes, setAdditionalShortcodes] = useState(false);
|
||||
|
||||
const { postId, blockData, VisualPortfolioBlockEdit } = useSelect(
|
||||
(select) => {
|
||||
const { getBlockData } = select(
|
||||
'visual-portfolio/saved-layout-data'
|
||||
);
|
||||
const { getCurrentPostId } = select('core/editor');
|
||||
const { getBlockType } = select('core/blocks');
|
||||
|
||||
return {
|
||||
postId: getCurrentPostId(),
|
||||
blockData: getBlockData(),
|
||||
VisualPortfolioBlockEdit:
|
||||
getBlockType('visual-portfolio/block')?.edit ||
|
||||
(() => null),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const { updateBlockData } = useDispatch(
|
||||
'visual-portfolio/saved-layout-data'
|
||||
);
|
||||
|
||||
let shortcodes = [
|
||||
{
|
||||
label: __('This Saved Layout', 'visual-portfolio'),
|
||||
content: `[visual_portfolio id="${postId}"]`,
|
||||
},
|
||||
{
|
||||
label: __('Filter', 'visual-portfolio'),
|
||||
content: `[visual_portfolio_filter id="${postId}" type="minimal" align="center" show_count="false" text_all="All"]`,
|
||||
isOptional: true,
|
||||
},
|
||||
{
|
||||
label: __('Sort', 'visual-portfolio'),
|
||||
content: `[visual_portfolio_sort id="${postId}" type="minimal" align="center"]`,
|
||||
isOptional: true,
|
||||
},
|
||||
];
|
||||
|
||||
shortcodes = applyFilters(
|
||||
'vpf.layouts-editor.shortcodes-list',
|
||||
shortcodes,
|
||||
{ props, postId, blockData, updateBlockData, VisualPortfolioBlockEdit }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={__('Shortcodes', 'visual-portfolio')}
|
||||
scrollAfterOpen
|
||||
>
|
||||
<p>
|
||||
{__(
|
||||
'To output this saved layout and its components you can use the following shortcodes:'
|
||||
)}
|
||||
</p>
|
||||
{shortcodes.map((data) => {
|
||||
if (data.isOptional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShortcodeRender
|
||||
key={`shortcode-${data.label}`}
|
||||
{...data}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{additionalShortcodes ? (
|
||||
<>
|
||||
{shortcodes.map((data) => {
|
||||
if (!data.isOptional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShortcodeRender
|
||||
key={`shortcode-${data.label}`}
|
||||
{...data}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{applyFilters(
|
||||
'vpf.layouts-editor.shortcodes',
|
||||
'',
|
||||
this
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
isLink
|
||||
onClick={() => {
|
||||
setAdditionalShortcodes(!additionalShortcodes);
|
||||
}}
|
||||
>
|
||||
{__(
|
||||
'Show Additional Shortcodes',
|
||||
'visual-portfolio'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<VisualPortfolioBlockEdit
|
||||
attributes={{
|
||||
...blockData,
|
||||
block_id: blockData.id || clientId,
|
||||
}}
|
||||
setAttributes={(data) => {
|
||||
updateBlockData(data);
|
||||
}}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
registerBlockType('visual-portfolio/saved-editor', {
|
||||
icon: {
|
||||
foreground: '#2540CC',
|
||||
src: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<mask
|
||||
id="mask0"
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="9"
|
||||
y="8"
|
||||
width="5"
|
||||
height="6"
|
||||
>
|
||||
<path
|
||||
d="M11.1409 14L13.0565 8.49994H11.2789L9.55397 14H11.1409Z"
|
||||
fill="url(#paint0_linear)"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path
|
||||
d="M11.1409 14L13.0565 8.49994H11.2789L9.55397 14H11.1409Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M8.90795 14L6.9923 8.49994H8.76989L10.4948 14H8.90795Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M19 16.2222C19 16.6937 18.8104 17.1459 18.4728 17.4793C18.1352 17.8127 17.6774 18 17.2 18H2.8C2.32261 18 1.86477 17.8127 1.52721 17.4793C1.18964 17.1459 1 16.6937 1 16.2222V3.77778C1 3.30628 1.18964 2.8541 1.52721 2.5207C1.86477 2.1873 2.32261 2 2.8 2H7.3L9.1 4.66667H17.2C17.6774 4.66667 18.1352 4.85397 18.4728 5.18737C18.8104 5.52076 19 5.97295 19 6.44444V16.2222Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1="12.191"
|
||||
y1="8.49994"
|
||||
x2="7.44436"
|
||||
y2="15.1301"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
edit: LayoutsEditorBlock,
|
||||
save() {
|
||||
return null;
|
||||
},
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
import './style.scss';
|
||||
import './store';
|
||||
import './block';
|
||||
import './plugin';
|
@ -0,0 +1,183 @@
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
|
||||
function UpdateEditor() {
|
||||
const {
|
||||
isSavingPost,
|
||||
isAutosavingPost,
|
||||
selectedBlock,
|
||||
editorSettings,
|
||||
editorMode,
|
||||
blocks,
|
||||
postId,
|
||||
blockData,
|
||||
} = useSelect((select) => {
|
||||
const {
|
||||
isSavingPost: checkIsSavingPost,
|
||||
isAutosavingPost: checkIsAutosavingPost,
|
||||
getCurrentPostId,
|
||||
getEditorSettings,
|
||||
} = select('core/editor');
|
||||
|
||||
const { getSelectedBlock, getBlocks } = select('core/block-editor');
|
||||
|
||||
const { getEditorMode } = select('core/edit-post');
|
||||
|
||||
const { getBlockData } = select('visual-portfolio/saved-layout-data');
|
||||
|
||||
return {
|
||||
isSavingPost: checkIsSavingPost(),
|
||||
isAutosavingPost: checkIsAutosavingPost(),
|
||||
selectedBlock: getSelectedBlock(),
|
||||
editorSettings: getEditorSettings(),
|
||||
editorMode: getEditorMode(),
|
||||
blocks: getBlocks(),
|
||||
postId: getCurrentPostId(),
|
||||
blockData: getBlockData(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { selectBlock, insertBlocks, resetBlocks } =
|
||||
useDispatch('core/block-editor');
|
||||
const { editPost } = useDispatch('core/editor');
|
||||
const { switchEditorMode } = useDispatch('core/edit-post');
|
||||
|
||||
/**
|
||||
* Force change gutenberg edit mode to Visual.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (editorSettings.richEditingEnabled && editorMode === 'text') {
|
||||
switchEditorMode();
|
||||
}
|
||||
}, [editorSettings, editorMode, switchEditorMode]);
|
||||
|
||||
/**
|
||||
* Add default block to post if doesn't exist.
|
||||
*/
|
||||
const blocksRestoreBusy = useRef(false);
|
||||
useEffect(() => {
|
||||
if (blocksRestoreBusy.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidList =
|
||||
blocks.length === 1 &&
|
||||
blocks[0] &&
|
||||
blocks[0].name === 'visual-portfolio/saved-editor';
|
||||
|
||||
if (!isValidList) {
|
||||
blocksRestoreBusy.current = true;
|
||||
resetBlocks([]);
|
||||
insertBlocks(createBlock('visual-portfolio/saved-editor'));
|
||||
blocksRestoreBusy.current = false;
|
||||
}
|
||||
}, [blocks, blocksRestoreBusy, resetBlocks, insertBlocks]);
|
||||
|
||||
/**
|
||||
* Always select block.
|
||||
* TODO: we actually should check the title block selected inside iframe
|
||||
*/
|
||||
const isBlockSelected = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isBlockSelected.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if selected block, do nothing.
|
||||
if (
|
||||
selectedBlock &&
|
||||
selectedBlock.name === 'visual-portfolio/saved-editor'
|
||||
) {
|
||||
isBlockSelected.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// check if selected post title, also do nothing.
|
||||
if (
|
||||
document.querySelector(
|
||||
'.editor-post-title__block.is-selected, .editor-post-title.is-selected'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectBlockId = '';
|
||||
blocks.forEach((thisBlock) => {
|
||||
if (thisBlock.name === 'visual-portfolio/saved-editor') {
|
||||
selectBlockId = thisBlock.clientId;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectBlockId) {
|
||||
selectBlock(selectBlockId);
|
||||
}
|
||||
}, [selectedBlock, blocks, selectBlock]);
|
||||
|
||||
/**
|
||||
* Check if post meta data edited and allow to update the post.
|
||||
*/
|
||||
const defaultBlockData = useRef(false);
|
||||
const editorRefreshTimeout = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!blockData || !Object.keys(blockData).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSavingPost || isAutosavingPost || !defaultBlockData.current) {
|
||||
defaultBlockData.current = JSON.stringify(blockData);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(editorRefreshTimeout.current);
|
||||
editorRefreshTimeout.current = setTimeout(() => {
|
||||
if (defaultBlockData.current !== JSON.stringify(blockData)) {
|
||||
editPost({ edited: new Date() });
|
||||
}
|
||||
}, 150);
|
||||
}, [isSavingPost, isAutosavingPost, blockData, editPost]);
|
||||
|
||||
/**
|
||||
* Save meta data on post save.
|
||||
*/
|
||||
const wasSavingPost = useRef(false);
|
||||
const wasAutosavingPost = useRef(false);
|
||||
useEffect(() => {
|
||||
const shouldUpdate =
|
||||
wasSavingPost.current &&
|
||||
!isSavingPost &&
|
||||
!wasAutosavingPost.current;
|
||||
|
||||
// Save current state for next inspection.
|
||||
wasSavingPost.current = isSavingPost;
|
||||
wasAutosavingPost.current = isAutosavingPost;
|
||||
|
||||
if (shouldUpdate) {
|
||||
const prefixedBlockData = {};
|
||||
|
||||
Object.keys(blockData).forEach((name) => {
|
||||
prefixedBlockData[`vp_${name}`] = blockData[name];
|
||||
});
|
||||
|
||||
apiFetch({
|
||||
path: '/visual-portfolio/v1/update_layout/',
|
||||
method: 'POST',
|
||||
data: {
|
||||
data: prefixedBlockData,
|
||||
post_id: postId,
|
||||
},
|
||||
}).catch((response) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(response);
|
||||
});
|
||||
}
|
||||
}, [isSavingPost, isAutosavingPost, postId, blockData]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
registerPlugin('vpf-saved-layouts-editor', {
|
||||
render: UpdateEditor,
|
||||
});
|
@ -0,0 +1 @@
|
||||
import './saved-layout-data';
|
@ -0,0 +1,20 @@
|
||||
export function apiFetch(request) {
|
||||
return {
|
||||
type: 'API_FETCH',
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
export function setBlockData(data) {
|
||||
return {
|
||||
type: 'SET_BLOCK_DATA',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateBlockData(data) {
|
||||
return {
|
||||
type: 'UPDATE_BLOCK_DATA',
|
||||
data,
|
||||
};
|
||||
}
|
10
wp-content/plugins/visual-portfolio/gutenberg/layouts-editor/store/saved-layout-data/controls.js
vendored
Normal file
10
wp-content/plugins/visual-portfolio/gutenberg/layouts-editor/store/saved-layout-data/controls.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
const { apiFetch } = wp;
|
||||
|
||||
export function API_FETCH({ request }) {
|
||||
return apiFetch(request).then((fetchedData) => {
|
||||
if (fetchedData && fetchedData.success && fetchedData.response) {
|
||||
return fetchedData.response;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
import * as actions from './actions';
|
||||
import * as controls from './controls';
|
||||
import reducer from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
|
||||
const store = createReduxStore('visual-portfolio/saved-layout-data', {
|
||||
reducer,
|
||||
selectors,
|
||||
actions,
|
||||
controls,
|
||||
});
|
||||
|
||||
register(store);
|
@ -0,0 +1,35 @@
|
||||
const { VPSavedLayoutVariables } = window;
|
||||
|
||||
function reducer(state = { data: VPSavedLayoutVariables.data }, action = {}) {
|
||||
switch (action.type) {
|
||||
case 'SET_BLOCK_DATA':
|
||||
if (action.data) {
|
||||
if (state) {
|
||||
return {
|
||||
...state,
|
||||
data: action.data,
|
||||
};
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'UPDATE_BLOCK_DATA':
|
||||
if (action.data && state) {
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
...state.data,
|
||||
...action.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default reducer;
|
@ -0,0 +1,5 @@
|
||||
const { VPSavedLayoutVariables } = window;
|
||||
|
||||
export function getBlockData(state) {
|
||||
return state.data || VPSavedLayoutVariables.data;
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Visual Portfolio Layouts Editor styles.
|
||||
*/
|
||||
|
||||
.post-type-vp_lists {
|
||||
.block-editor-block-list__breadcrumb,
|
||||
.edit-post-layout__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// remove outline from block.
|
||||
.block-editor-block-list__block::before,
|
||||
.block-editor-block-contextual-toolbar,
|
||||
.block-editor-block-list__insertion-point {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Remove writing container.
|
||||
.block-editor-writing-flow__click-redirect {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Shortcodes settings.
|
||||
.vpf-layout-shortcode-copy {
|
||||
> div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> strong {
|
||||
display: block;
|
||||
padding-top: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
padding-right: 40px;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1.1em;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
background-color: #ebebeb;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.components-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 0;
|
||||
background-color: #ebebeb;
|
||||
opacity: 0;
|
||||
transition: 0.2s opacity;
|
||||
}
|
||||
|
||||
.components-button:hover,
|
||||
.components-button:focus,
|
||||
pre:hover + .components-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Copied element
|
||||
.vpf-layout-shortcode-copied {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
padding: 4px 6px;
|
||||
margin-left: 50%;
|
||||
color: #fff;
|
||||
background: #000;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
transform: translateY(5px) translateX(-50%);
|
||||
animation: 0.4s vpf-layout-shortcode-copied linear;
|
||||
}
|
||||
|
||||
@keyframes vpf-layout-shortcode-copied {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transition-timing-function: ease-out;
|
||||
transform: translateY(5px) translateX(-50%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateY(-5px) translateX(-50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transition-timing-function: ease-in;
|
||||
transform: translateY(-15px) translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
export function apiFetch(request) {
|
||||
return {
|
||||
type: 'API_FETCH',
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
export function setPortfolioLayouts(layouts) {
|
||||
return {
|
||||
type: 'SET_PORTFOLIO_LAYOUTS',
|
||||
layouts,
|
||||
};
|
||||
}
|
26
wp-content/plugins/visual-portfolio/gutenberg/store/base/controls.js
vendored
Normal file
26
wp-content/plugins/visual-portfolio/gutenberg/store/base/controls.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
export function API_FETCH({ request }) {
|
||||
return apiFetch(request)
|
||||
.catch((fetchedData) => {
|
||||
if (
|
||||
fetchedData &&
|
||||
fetchedData.error &&
|
||||
fetchedData.error_code === 'no_layouts_found'
|
||||
) {
|
||||
return {
|
||||
response: [],
|
||||
error: false,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.then((fetchedData) => {
|
||||
if (fetchedData && fetchedData.success && fetchedData.response) {
|
||||
return fetchedData.response;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
import * as actions from './actions';
|
||||
import * as controls from './controls';
|
||||
import reducer from './reducer';
|
||||
import * as resolvers from './resolvers';
|
||||
import * as selectors from './selectors';
|
||||
|
||||
const store = createReduxStore('visual-portfolio', {
|
||||
selectors,
|
||||
actions,
|
||||
controls,
|
||||
resolvers,
|
||||
reducer,
|
||||
});
|
||||
|
||||
register(store);
|
@ -0,0 +1,18 @@
|
||||
function reducer(state = { layouts: [] }, action = {}) {
|
||||
switch (action.type) {
|
||||
case 'SET_PORTFOLIO_LAYOUTS':
|
||||
if (
|
||||
!state.layouts.length &&
|
||||
action.layouts &&
|
||||
action.layouts.length
|
||||
) {
|
||||
state.layouts = action.layouts;
|
||||
}
|
||||
return state;
|
||||
// no default
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default reducer;
|
@ -0,0 +1,7 @@
|
||||
import * as actions from './actions';
|
||||
|
||||
export function* getPortfolioLayouts() {
|
||||
const query = '/visual-portfolio/v1/get_layouts/';
|
||||
const layouts = yield actions.apiFetch({ path: query });
|
||||
return actions.setPortfolioLayouts(layouts);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export function getPortfolioLayouts(state) {
|
||||
return state.layouts;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
import * as selectors from './selectors';
|
||||
|
||||
const store = createReduxStore('visual-portfolio/components', {
|
||||
selectors,
|
||||
reducer(state) {
|
||||
return state;
|
||||
},
|
||||
});
|
||||
|
||||
register(store);
|
@ -0,0 +1,23 @@
|
||||
import ClassesTree from '../../components/classes-tree';
|
||||
import ColorPicker from '../../components/color-picker';
|
||||
import ControlsRender from '../../components/controls-render';
|
||||
import VPDatePicker from '../../components/date-picker';
|
||||
import ElementsSelector from '../../components/elements-selector';
|
||||
import IconsSelector from '../../components/icons-selector';
|
||||
import VpfSelectControl from '../../components/select-control';
|
||||
import SpinnerComponent from '../../components/spinner';
|
||||
import ToggleModal from '../../components/toggle-modal';
|
||||
|
||||
export function get() {
|
||||
return {
|
||||
ClassesTree,
|
||||
ColorPicker,
|
||||
ControlsRender,
|
||||
VPDatePicker,
|
||||
ElementsSelector,
|
||||
IconsSelector,
|
||||
VpfSelectControl,
|
||||
SpinnerComponent,
|
||||
ToggleModal,
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import './base';
|
||||
import './components';
|
||||
import './utils';
|
@ -0,0 +1,12 @@
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
import * as selectors from './selectors';
|
||||
|
||||
const store = createReduxStore('visual-portfolio/utils', {
|
||||
selectors,
|
||||
reducer(state) {
|
||||
return state;
|
||||
},
|
||||
});
|
||||
|
||||
register(store);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user