This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -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";

View File

@ -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"]
}
}

View File

@ -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>
);
}

View File

@ -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,
},
});

View File

@ -0,0 +1,3 @@
export default function BlockSave() {
return null;
}

View File

@ -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;
}
}
}

View File

@ -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,
},
},
},
],
};

View File

@ -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"]
}
}

View File

@ -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,
},
];

View 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>
);
}

View 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);

View File

@ -0,0 +1,3 @@
export default function BlockSave() {
return null;
}

View File

@ -0,0 +1,7 @@
// Block preview example.
.vpf-example-preview {
img {
width: 100%;
height: auto;
}
}

View File

@ -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,
});
},
},
],
};

View File

@ -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,
};
}) || [];

View File

@ -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>
);
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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(),
})
}
/>
) : (
''
)}
&lt;
{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>
))}
{'" '}
</>
) : (
''
)}
&gt;
</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>
);
}
};

View File

@ -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;
}
}

View File

@ -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}
/>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
)}
/>
);
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>
)}
/>
);
}
}

View File

@ -0,0 +1 @@
import './style.scss';

View File

@ -0,0 +1,3 @@
.vpf-component-dropdown-no-padding .components-popover__content > div {
padding: 0;
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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>
);
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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,
};
}
);

View File

@ -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%);
}
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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}
/>
);
}

View File

@ -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;
}
}

View File

@ -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>
);
}
};

View File

@ -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);
}
}
}

View File

@ -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} />;
}
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -0,0 +1,4 @@
.vpf-component-spinner .components-spinner {
float: none;
margin-left: 0;
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -0,0 +1,8 @@
.vpf-component-tabs-control {
margin: 0 -16px;
margin-bottom: -26px;
.components-tab-panel__tabs-item {
flex: 1;
}
}

View File

@ -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>
);
}
}

View File

@ -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%;
}
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
) : (
''
)}
</>
);
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,2 @@
import './custom-post-meta/video';
import './custom-post-meta/image-focal-point';

View File

@ -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 />
</>
);
}
);

View File

@ -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
)}
&nbsp;
<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,
});
}

View File

@ -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);

View File

@ -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;
}
);

View File

@ -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}
/>
);
}
);

View File

@ -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;
}
);

View File

@ -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;
}
);

View 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" />,
});

View File

@ -0,0 +1 @@
import './layouts-editor/index';

View File

@ -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
}
}

View File

@ -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;
},
});

View File

@ -0,0 +1,4 @@
import './style.scss';
import './store';
import './block';
import './plugin';

View File

@ -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,
});

View File

@ -0,0 +1 @@
import './saved-layout-data';

View File

@ -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,
};
}

View 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;
});
}

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,5 @@
const { VPSavedLayoutVariables } = window;
export function getBlockData(state) {
return state.data || VPSavedLayoutVariables.data;
}

View File

@ -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%);
}
}
}

View File

@ -0,0 +1,13 @@
export function apiFetch(request) {
return {
type: 'API_FETCH',
request,
};
}
export function setPortfolioLayouts(layouts) {
return {
type: 'SET_PORTFOLIO_LAYOUTS',
layouts,
};
}

View 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;
});
}

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -0,0 +1,3 @@
export function getPortfolioLayouts(state) {
return state.layouts;
}

View File

@ -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);

View File

@ -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,
};
}

View File

@ -0,0 +1,3 @@
import './base';
import './components';
import './utils';

View File

@ -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