corvée(dépendances) ajoute Carbon Fields

This commit is contained in:
gcch 2024-08-09 18:45:01 +02:00
commit 62368587e5
459 changed files with 72750 additions and 26 deletions

View file

@ -0,0 +1,177 @@
/**
* External dependencies.
*/
import cx from 'classnames';
import { Component } from '@wordpress/element';
import {
map,
find,
kebabCase,
isPlainObject
} from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { getFieldType } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import './style.scss';
import Field from '../field';
class Container extends Component {
/**
* Local state.
*
* @type {Object}
*/
state = {
currentTab: null
};
/**
* Lifecycle hook.
*
* @return {void}
*/
componentDidMount() {
const { container } = this.props;
if ( this.isTabbed( container ) ) {
this.setState( {
currentTab: Object.keys( container.settings.tabs )[ 0 ]
} );
}
}
/**
* Returns whether the container uses tabs.
*
* @param {Object} container
* @return {boolean}
*/
isTabbed( container ) {
return isPlainObject( container.settings.tabs );
}
/**
* Renders the given field.
*
* @param {Object} field
* @return {Object}
*/
renderField = ( field ) => {
const FieldEdit = getFieldType( field.type, 'metabox' );
if ( ! FieldEdit ) {
return null;
}
return (
<Field key={ field.id } id={ field.id }>
<FieldEdit id={ field.id } containerId={ this.props.id } />
</Field>
);
}
/**
* Handles click on the tabs.
*
* @param {string} tab
* @return {void}
*/
handleTabClick = ( tab ) => {
this.setState( {
currentTab: tab
} );
}
/**
* Renders the component.
*
* @return {Object}
*/
render() {
const { currentTab } = this.state;
const { container } = this.props;
const hasTabs = this.isTabbed( container );
const classes = cx( [
'cf-container',
`cf-container-${ container.id }`,
`cf-container-${ kebabCase( container.type ) }`,
...container.classes,
{
'cf-container--plain': ! hasTabs,
[ `cf-container--tabbed cf-container--${ container.layout }` ]: hasTabs
}
] );
return (
<div className={ classes }>
<input
type="hidden"
name={ container.nonce.name }
value={ container.nonce.value }
/>
{ hasTabs && (
<div className={ `cf-container__tabs cf-container__tabs--${ container.layout }` }>
<ul className="cf-container__tabs-list">
{ map( container.settings.tabs, ( fieldNames, tabName ) => {
// eslint-disable-next-line no-shadow
const classes = cx(
'cf-container__tabs-item',
{
'cf-container__tabs-item--current': tabName === currentTab
}
);
return (
<li
key={ tabName }
className={ classes }
tabIndex={ -1 }
role="tab"
aria-selected={ currentTab === tabName }
>
<button
type="button"
onClick={ () => this.handleTabClick( tabName ) }
dangerouslySetInnerHTML={ { __html: tabName } }
/>
</li>
);
} ) }
</ul>
</div>
) }
{ hasTabs && (
map( container.settings.tabs, ( fieldNames, tabName ) => {
return (
<div className="cf-container__fields" key={ tabName } hidden={ tabName !== currentTab }>
{ map( fieldNames, ( fieldName ) => {
const field = find( container.fields, [ 'name', fieldName ] );
return this.renderField( field );
} ) }
</div>
);
} )
) }
{ ! hasTabs && (
<div className="cf-container__fields">
{ map( container.fields, this.renderField ) }
</div>
) }
</div>
);
}
}
export default Container;

View file

@ -0,0 +1,146 @@
/* ==========================================================================
Container
========================================================================== */
.carbon-box {
&.hide-if-js:not([hidden]) {
display: block;
}
}
#poststuff .carbon-box .inside,
.carbon-box .inside {
padding: 0;
margin: 0;
}
.cf-container {
&--plain {
display: block;
}
&--tabbed {
display: flex;
&-horizontal {
flex-direction: column;
}
&-vertical {
flex-direction: row;
}
}
}
.cf-container__fields {
display: flex;
flex-wrap: wrap;
flex: 1;
margin: 0 -1px 0 0;
background-color: $color-white;
&[hidden] {
display: none;
}
.block-editor & {
border-left: 1px solid $wp-color-gray-light-500;
}
.cf-container-term-meta &,
.cf-container-user-meta & {
border-width: 0 0 1px 1px;
border-style: solid;
border-color: $wp-color-gray-light-500;
margin: 0;
}
}
.cf-container__tabs {
position: relative;
z-index: 1;
background-color: $wp-color-gray-light-100;
&-list {
display: flex;
flex-wrap: wrap;
margin: 0 0 -1px;
.cf-container__tabs-item {
display: flex;
align-items: center;
border: 1px solid $wp-color-gray-light-500;
margin: 0 ($size-base * 2) 0 0;
background-color: $wp-color-gray-light-100;
font-size: 13px;
cursor: pointer;
transition: background-color $transition-base, border-color $transition-base;
button {
background: 0;
border: 0;
padding: ($size-base * 2.5) ($size-base * 3);
margin: 0;
flex: 1;
cursor: pointer;
display: flex;
align-items: center;
span {
margin-right: 5px;
}
}
&:hover {
background-color: $color-white;
}
&--current {
background-color: $color-white;
border-bottom-color: $color-white;
}
}
}
&--tabbed-horizontal {
padding: ($size-base * 3) ($size-base * 3) 0;
border-bottom: 1px solid $wp-color-gray-light-500;
.cf-container__tabs-list {
display: flex;
flex-direction: row;
}
}
&--tabbed-vertical {
width: 300px;
border-right: 1px solid $wp-color-gray-light-500;
.cf-container__tabs-list {
display: flex;
flex-direction: column;
.cf-container__tabs-item {
margin: 0;
justify-content: flex-start;
border: 0;
border-top: 1px solid $gb-light-gray-500;
border-bottom: 1px solid $gb-light-gray-500;
&:first-of-type {
border-top: 0;
}
button {
text-align: left;
font-weight: 500;
}
}
.cf-container__tabs-item + .cf-container__tabs-item {
border-top: 0;
}
}
}
}

View file

@ -0,0 +1,20 @@
/**
* External dependencies.
*/
import { compose } from '@wordpress/compose';
/**
* Carbon Fields dependencies.
*/
import { Field, withFilters } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import './style.scss';
import withField from '../../hocs/with-field';
export default compose(
withField,
withFilters( 'carbon-fields.field-wrapper.metabox' )
)( Field );

View file

@ -0,0 +1,45 @@
/* ==========================================================================
Field
========================================================================== */
.cf-field {
.cf-container & {
flex: 1 1 100%;
padding: $size-base * 3;
border-width: 0 1px 0 0;
border-style: solid;
border-color: $wp-color-gray-light-500;
}
.cf-container & + & {
border-top-width: 1px;
}
}
.cf-field__head {
.term-php .cf-container__fields > .cf-field:not(.cf-field--has-width) > &,
.cf-container-user-meta .cf-container__fields > .cf-field > & {
position: absolute;
left: 0;
font-weight: 600;
}
}
.cf-field__label {
.cf-container & {
padding-bottom: 6.5px;
font-weight: 600;
color: $wp-color-dark-gray;
}
.cf-container-term-meta &,
.cf-container-user-meta & {
margin: 0 !important;
}
}
.cf-field__help {
display: inline-block;
color: #666;
margin-top: 10px;
}

View file

@ -0,0 +1,25 @@
/**
* External dependencies.
*/
import { compose } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
/**
* Carbon Fields dependencies.
*/
import { withFilters } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import withContainer from '../hocs/with-container';
/**
* Extends the containers with necessary hooks.
*/
addFilter( 'carbon-fields.register-container-type', 'carbon-fields/metaboxes', ( type, context, component ) => {
return compose(
withContainer,
withFilters( `carbon-fields.${ type }.${ context }` )
)( component );
} );

View file

@ -0,0 +1,81 @@
/**
* External dependencies.
*/
import { render, createRoot } from '@wordpress/element';
import { select } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { forEach } from 'lodash';
/**
* Internal dependencies.
*/
import './hooks';
import './widget';
import './term-meta';
import './theme-options';
import './user-meta';
import Container from '../components/container';
import { getContainerType, registerContainerType } from './registry';
import { registerContainerRoot } from './root-registry';
/**
* Registers the containers.
*/
[
'post_meta',
'term_meta',
'user_meta',
'comment_meta',
'network',
'theme_options',
'nav_menu_item',
'widget'
].forEach( ( type ) => registerContainerType( type, Container ) );
/**
* Renders the given container.
*
* @param {Object} container
* @param {string} context
* @return {void}
*/
export function renderContainer( container, context ) {
const node = document.querySelector( `.container-${ container.id }` );
const Component = getContainerType( container.type, context );
if ( node ) {
const NodeComponent = <Component id={ container.id } />;
if ( createRoot ) {
const nodeRoot = createRoot( node );
nodeRoot.render( NodeComponent );
registerContainerRoot( container.id, nodeRoot );
} else {
render(
NodeComponent,
node,
() => {
node.dataset.mounted = true;
}
);
}
} else {
// eslint-disable-next-line no-console
console.error( sprintf( __( 'Could not find DOM element for container "%1$s".', 'carbon-fields-ui' ), container.id ) );
}
}
/**
* Initializes the containers.
*
* @param {string} context
* @return {void}
*/
export default function initializeContainers( context ) {
const containers = select( 'carbon-fields/metaboxes' ).getContainers();
forEach( containers, ( container ) => {
renderContainer( container, context );
} );
}

View file

@ -0,0 +1,13 @@
/**
* External dependencies.
*/
import { createRegistry } from '@carbon-fields/core';
export const {
registerContainerType,
getContainerType
} = createRegistry( 'container', [
'classic',
'gutenberg'
] );

View file

@ -0,0 +1,28 @@
const rootRegistry = {};
export function registerContainerRoot( containerId, root ) {
rootRegistry[ containerId ] = {
createdAt: Math.floor(Date.now() / 1000),
...root,
unmount() {
// Fix issues with race condition by delaying
// the onLoad unmounting of containers
// they would be unmounted later
if ( parseFloat( window.cf.config.wp_version ) >= 6.2 ) {
const currentTime = Math.floor(Date.now() / 1000);
if ( currentTime - rootRegistry[ containerId ].createdAt >= 3 ) {
root.unmount();
delete rootRegistry[ containerId ];
}
} else {
root.unmount();
delete rootRegistry[ containerId ];
}
}
};
}
export function getContainerRoot( containerId ) {
return rootRegistry[ containerId ] || null;
}

View file

@ -0,0 +1,68 @@
/**
* External dependencies.
*/
import _ from 'lodash';
import { addFilter } from '@wordpress/hooks';
import { dispatch } from '@wordpress/data';
import { withEffects } from 'refract-callbag';
import { pipe, filter } from 'callbag-basics';
/**
* Internal dependencies.
*/
import fromEventPattern from '../../utils/from-event-pattern';
import { normalizePreloadedState } from '../../store/helpers';
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return pipe(
fromEventPattern(
( handler ) => window.jQuery( document ).on( 'ajaxSuccess', handler ),
( handler ) => window.jQuery( document ).off( 'ajaxSuccess', handler ),
( e, xhr, options, data ) => ( {
options,
data
} )
),
filter( ( { options, data } ) => {
return options.data
&& options.data.indexOf( 'carbon_fields_container' ) > -1
&& options.data.indexOf( 'add-tag' ) > -1
&& ! data.documentElement.querySelector( 'wp_error' );
} )
);
}
/**
* The function that causes the side effects.
*
* @param {Object} props
* @return {Function}
*/
function handler( props ) {
return function() {
// Collects identifiers of current fields so we can remove them later.
const oldFieldIds = _.map( props.container.fields, 'id' );
// Get a fresh copy of the container and fields.
const { containers, fields } = normalizePreloadedState( _.get( window.cf, 'preloaded.containers', [] ) );
const container = _.find( containers, [ 'id', props.id ] );
const containerFields = _.filter( fields, [ 'container_id', props.id ] );
// Replace the container and add the new fields.
const { updateState, removeFields } = dispatch( 'carbon-fields/metaboxes' );
updateState(
_.keyBy( [ container ], 'id' ),
_.keyBy( containerFields, 'id' )
);
removeFields( oldFieldIds );
};
}
addFilter( 'carbon-fields.term_meta.classic', 'carbon-fields/metaboxes', withEffects( aperture, { handler } ) );

View file

@ -0,0 +1,52 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { withEffects } from 'refract-callbag';
import { map, pipe } from 'callbag-basics';
import fromEvent from 'callbag-from-event';
/**
* Internal dependencies.
*/
import './style.scss';
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return pipe(
fromEvent( window, 'scroll' ),
map( () => window.jQuery( window ).scrollTop() )
);
}
/**
* The function that causes the side effects.
*
* @param {Object} props
* @return {Function}
*/
function handler() {
return function( windowTopOffset ) {
const $container = window.jQuery( '.carbon-box:first' );
const $panel = window.jQuery( '#postbox-container-1' );
const $bar = window.jQuery( '#wpadminbar' );
const offset = $bar.height() + 10;
const threshold = $container.offset().top - offset;
// In some situations the threshold is negative number because
// the container element isn't rendered yet.
if ( threshold > 0 ) {
$panel
.toggleClass( 'fixed', windowTopOffset >= threshold )
.css( 'top', offset );
}
};
}
addFilter( 'carbon-fields.theme_options.classic', 'carbon-fields/metaboxes', withEffects( aperture, { handler } ) );

View file

@ -0,0 +1,10 @@
/* ==========================================================================
Theme Options
========================================================================== */
#postbox-container-1.fixed {
.carbon-theme-options #post-body.columns-2 &,
.carbon-network #post-body.columns-2 & {
position: fixed; right: 0; margin-right: 20px;
}
}

View file

@ -0,0 +1,4 @@
/**
* Internal dependencies.
*/
import './style.scss';

View file

@ -0,0 +1,7 @@
/* ==========================================================================
User Meta
========================================================================== */
.cf-container-user-meta {
max-width: 600px;
}

View file

@ -0,0 +1,50 @@
/**
* External dependencies.
*/
import { select } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { withEffects } from 'refract-callbag';
/**
* Internal dependencies.
*/
import './style.scss';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return fromSelector( select( 'carbon-fields/metaboxes' ).isFieldUpdated );
}
/**
* The function that causes the side effects.
*
* @param {Object} props
* @return {Function}
*/
function handler( props ) {
return function( { action } ) {
if ( ! action ) {
return;
}
const { container } = props;
const { payload } = action;
if ( container.fields.map( ( field ) => field.id ).indexOf( payload.fieldId ) >= 0 ) {
const $carbonContainer = window.jQuery( `.container-${ container.id }` );
$carbonContainer.closest( '.widget-inside' ).trigger( 'change' );
}
};
}
addFilter( 'carbon-fields.widget.classic', 'carbon-fields/metaboxes', withEffects( aperture, { handler } ) );

View file

@ -0,0 +1,31 @@
/* ==========================================================================
Widget
========================================================================== */
.cf-container-widget {
margin-bottom: 13px;
.cf-field {
margin: 1em 0 0;
padding: 0;
border-width: 0;
}
.cf-field + .cf-field {
border-top-width: 0;
}
.cf-complex__group-body {
border-width: 1px 1px 1px 1px;
margin-top: 0;
}
.cf-complex__group-body .cf-field {
padding: 12px;
}
.cf-complex__group-body .cf-field + .cf-field {
border-width: 1px 0 0 0;
padding-top: 1em;
}
}

View file

@ -0,0 +1,88 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import { get, find } from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { withProps } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import stripCompactInputPrefix from '../../utils/strip-compact-input-prefix';
/**
* Returns a field with the given name.
*
* @param {Object[]} fields
* @param {string} name
* @return {?Object}
*/
function findFieldByName( fields, name ) {
return find( fields, ( field ) => {
return field.name === name;
} );
}
addFilter( 'carbon-fields.association.metabox', 'carbon-fields/metaboxes', withProps( ( props ) => {
return {
hierarchyResolver() {
// Get all fields.
const container = select( 'carbon-fields/metaboxes' ).getContainerById( props.containerId );
const fields = select( 'carbon-fields/metaboxes' ).getFieldsByContainerId( props.containerId );
// Get a clean version of field's name.
const fieldName = stripCompactInputPrefix( props.name );
// Get the path.
let path = fieldName.split( /\[|\]/g );
// Remove chunks that are empty.
path = path.filter( ( chunk ) => chunk !== '' );
if ( container.type === 'widget' ) {
return props.field.base_name;
}
// Get the root field.
const rootFieldName = path.shift();
const rootField = findFieldByName( fields, rootFieldName );
// Get the hierarchy.
let accessor = fields.indexOf( rootField );
let hierarchy = rootField.base_name;
// Visit every branch in the tree so we can get the full hierarchy.
while ( path.length > 0 ) {
const chunk = path.shift();
const isGroup = ! isNaN( chunk );
const isSameField = chunk === props.field.base_name;
const isNestedComplex = ! isGroup && ! isSameField;
if ( isGroup ) {
accessor = `${ accessor }.value.${ chunk }.name`;
hierarchy = `${ hierarchy }[${ chunk }]:${ get( fields, accessor ) }/`;
}
if ( isNestedComplex ) {
const fieldReferences = get( fields, accessor.replace( /\.name$/, '.fields' ) );
const fieldReference = findFieldByName( fieldReferences, chunk );
const field = find( fields, [ 'id', fieldReference.id ] );
accessor = fields.indexOf( field );
hierarchy = `${ hierarchy }${ field.base_name }`;
}
if ( isSameField ) {
hierarchy = `${ hierarchy }${ chunk }`;
}
}
return hierarchy;
}
};
} ) );

View file

@ -0,0 +1,295 @@
/**
* External dependencies.
*/
import produce from 'immer';
import { Component } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
import { compose } from '@wordpress/compose';
import { withDispatch, withSelect } from '@wordpress/data';
import {
find,
assign,
without,
cloneDeep
} from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { uniqueId } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import './style.scss';
import Field from '../../components/field';
import flattenField from '../../utils/flatten-field';
class ComplexField extends Component {
/**
* Handles adding of group.
*
* @param {Object} group
* @param {Function} callback
* @return {Object}
*/
handleAddGroup = ( group, callback ) => {
const {
id,
field,
value,
addFields,
onChange
} = this.props;
// Create a copy of the group to prevent
// incidentally modifications.
group = cloneDeep( group );
// Get a flat list of all fields for this group.
const fields = [];
group.id = uniqueId();
group.container_id = field.container_id;
group.fields = group.fields.map( ( groupField ) => flattenField( groupField, field.container_id, fields ) );
// Make sure that the group is expanded even
// `set_collapsed(true)` is used.
group.collapsed = false;
// Push the group to the field.
addFields( fields );
onChange( id, value.concat( group ) );
callback( group );
}
/**
* Handles cloning of group.
*
* @param {Object} group
* @param {Function} callback
* @return {void}
*/
handleCloneGroup = ( group, callback ) => {
const {
id,
value,
cloneFields,
onChange
} = this.props;
const originFieldIds = group.fields.map( ( groupField ) => groupField.id );
const cloneFieldIds = originFieldIds.map( () => uniqueId() );
const clonedGroup = cloneDeep( group );
clonedGroup.id = uniqueId();
clonedGroup.fields.forEach( ( groupField, index ) => {
groupField.id = cloneFieldIds[ index ];
} );
cloneFields( originFieldIds, cloneFieldIds );
onChange( id, produce( value, ( draft ) => {
draft.splice( value.indexOf( group ) + 1, 0, clonedGroup );
} ) );
callback( clonedGroup );
}
/**
* Handles removing of group.
*
* @param {Object} group
* @return {void}
*/
handleRemoveGroup = ( group ) => {
const {
id,
value,
onChange
} = this.props;
onChange(
id,
without( value, group ),
group.fields.map( ( groupField ) => groupField.id )
);
}
/**
* Handles expanding/collapsing of group.
*
* @param {string} groupId
* @return {void}
*/
handleToggleGroup = ( groupId ) => {
const {
field,
value,
onChange
} = this.props;
onChange( field.id, produce( value, ( draft ) => {
const group = find( draft, [ 'id', groupId ] );
group.collapsed = ! group.collapsed;
} ) );
}
/**
* Handles expanding/collapsing of all groups.
*
* @param {boolean} collapsed
* @return {void}
*/
handleToggleAllGroups = ( collapsed ) => {
const {
field,
value,
onChange
} = this.props;
onChange( field.id, produce( value, ( draft ) => {
draft.forEach( ( group ) => {
group.collapsed = collapsed;
} );
} ) );
}
/**
* Handles setuping of group.
*
* @param {Object} group
* @param {Object} props
* @return {Object}
*/
handleGroupSetup = ( group, props ) => {
return assign( {}, props, {
id: group.id,
name: group.name,
prefix: `${ this.props.name }[${ props.index }]`,
fields: group.fields,
collapsed: group.collapsed,
context: 'metabox'
} );
}
/**
* Handles setuping of group's field.
*
* @param {Object} field
* @param {Object} props
* @param {Object} groupProps
* @return {Array}
*/
handleGroupFieldSetup = ( field, props, groupProps ) => {
return [ Field, assign( {}, props, {
key: field.id,
id: field.id,
containerId: this.props.containerId,
name: `${ groupProps.prefix }[${ field.name }]`
} ) ];
}
/**
* Renders the component.
*
* @return {Object}
*/
render() {
const {
handleGroupSetup,
handleGroupFieldSetup,
handleAddGroup,
handleCloneGroup,
handleRemoveGroup,
handleToggleGroup,
handleToggleAllGroups
} = this;
const { value, children } = this.props;
const allGroupsAreCollapsed = value.every( ( { collapsed } ) => collapsed );
return children( {
allGroupsAreCollapsed,
handleGroupSetup,
handleGroupFieldSetup,
handleAddGroup,
handleCloneGroup,
handleRemoveGroup,
handleToggleGroup,
handleToggleAllGroups
} );
}
}
const applyWithSelect = withSelect( ( select, props ) => {
const { getComplexGroupValues } = select( 'carbon-fields/metaboxes' );
const groupValues = props.value.map( ( group ) => {
const fieldIds = group.fields.map( ( field ) => field.id );
return [ group.name, getComplexGroupValues( fieldIds ) ];
} );
return {
groupValues
};
} );
const applyWithDispatch = withDispatch( ( dispatch ) => {
const { addFields, cloneFields } = dispatch( 'carbon-fields/metaboxes' );
return {
addFields,
cloneFields
};
} );
addFilter( 'carbon-fields.complex.metabox', 'carbon-fields/metaboxes', ( OriginalComplexField ) => compose(
applyWithSelect,
applyWithDispatch
)( ( props ) => {
const {
id,
field,
name,
value,
groupValues
} = props;
return (
<ComplexField { ...props }>
{ ( {
allGroupsAreCollapsed,
handleGroupSetup,
handleGroupFieldSetup,
handleAddGroup,
handleCloneGroup,
handleRemoveGroup,
handleToggleGroup,
handleToggleAllGroups
} ) => (
<OriginalComplexField
groupIdKey="id"
groupFilterKey="name"
id={ id }
field={ field }
name={ name }
value={ value }
groupValues={ groupValues }
allGroupsAreCollapsed={ allGroupsAreCollapsed }
onGroupSetup={ handleGroupSetup }
onGroupFieldSetup={ handleGroupFieldSetup }
onAddGroup={ handleAddGroup }
onCloneGroup={ handleCloneGroup }
onRemoveGroup={ handleRemoveGroup }
onToggleGroup={ handleToggleGroup }
onToggleAllGroups={ handleToggleAllGroups }
onChange={ props.onChange }
/>
) }
</ComplexField>
);
} ) );

View file

@ -0,0 +1,54 @@
/* ==========================================================================
Complex
========================================================================== */
.cf-complex__inserter-menu {
.postbox &,
body[class*="taxonomy-"] & {
margin: 0;
border-radius: $wp-radius-round;
background-color: $wp-color-ultra-dark-gray;
&:before {
position: absolute;
top: 50%;
right: 100%;
width: 0;
height: 0;
border-width: 5px 5px 5px 0;
border-style: solid;
border-color: transparent $wp-color-ultra-dark-gray;
margin-top: -5px;
content: '';
}
}
}
.cf-complex__inserter-item {
.postbox &,
body[class*="taxonomy-"] & {
font-weight: 600;
color: $color-white;
transition: color $transition-base;
&:hover {
color: $wp-color-medium-blue;
}
}
}
.cf-complex__inserter-button {
.postbox .cf-complex__tabs &,
body[class*="taxonomy-"] .cf-complex__tabs & {
font-weight: 600;
color: $wp-color-dark-gray;
}
}
.cf-complex__tabs-item {
.postbox &,
body[class*="taxonomy"] & {
font-weight: 600;
color: $wp-color-dark-gray;
}
}

View file

@ -0,0 +1,12 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
addFilter( 'carbon-fields.date_time.metabox', 'carbon-fields/metaboxes', ( OriginalDatetimeField ) => ( props ) => (
<OriginalDatetimeField
buttonText={ __( 'Select Date', 'carbon-fields-ui' ) }
{ ...props }
/>
) );

View file

@ -0,0 +1,16 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
addFilter( 'carbon-fields.file.metabox', 'carbon-fields/metaboxes', ( OriginalFileField ) => ( props ) => {
return (
<OriginalFileField
buttonLabel={ __( 'Select File', 'carbon-fields-ui' ) }
mediaLibraryButtonLabel={ __( 'Use File', 'carbon-fields-ui' ) }
mediaLibraryTitle={ __( 'Select File', 'carbon-fields-ui' ) }
{ ...props }
/>
);
} );

View file

@ -0,0 +1,16 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
addFilter( 'carbon-fields.image.metabox', 'carbon-fields/metaboxes', ( OriginalImageField ) => ( props ) => {
return (
<OriginalImageField
buttonLabel={ __( 'Select Image', 'carbon-fields-ui' ) }
mediaLibraryButtonLabel={ __( 'Use Image', 'carbon-fields-ui' ) }
mediaLibraryTitle={ __( 'Select Image', 'carbon-fields-ui' ) }
{ ...props }
/>
);
} );

View file

@ -0,0 +1,54 @@
/**
* External dependencies.
*/
import { compose } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
import { withDispatch } from '@wordpress/data';
/**
* Carbon Fields dependencies.
*/
import { withValidation } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import withField from '../hocs/with-field';
import withConditionalLogic from '../hocs/with-conditional-logic';
import isGutenberg from '../utils/is-gutenberg';
/**
* Connects every field to the store.
*/
addFilter( 'carbon-fields.field-edit.metabox', 'carbon-fields/metaboxes', compose(
withField,
withConditionalLogic,
withDispatch( ( dispatch ) => {
if ( isGutenberg() ) {
const { lockPostSaving, unlockPostSaving } = dispatch( 'core/editor' );
return {
lockSaving: lockPostSaving,
unlockSaving: unlockPostSaving
};
}
const { lockSaving, unlockSaving } = dispatch( 'carbon-fields/metaboxes' );
return {
lockSaving,
unlockSaving
};
} ),
withValidation
) );
import './association';
import './complex';
import './datetime';
import './file';
import './image';
import './multiselect';
import './media-gallery';
import './radio';
import './sidebar';

View file

@ -0,0 +1,21 @@
/**
* External dependencies.
*/
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
/**
* The internal dependencies.
*/
import './style.scss';
addFilter( 'carbon-fields.media_gallery.metabox', 'carbon-fields/metaboxes', ( OriginalMediaGalleryField ) => ( props ) => {
return (
<OriginalMediaGalleryField
buttonLabel={ __( 'Select Attachments', 'carbon-fields-ui' ) }
mediaLibraryButtonLabel={ __( 'Use Attachments', 'carbon-fields-ui' ) }
mediaLibraryTitle={ __( 'Select Attachments', 'carbon-fields-ui' ) }
{ ...props }
/>
);
} );

View file

@ -0,0 +1,54 @@
/* ==========================================================================
Media Gallery
========================================================================== */
.cf-media-gallery__inner {
.postbox & {
border: 1px solid $wp-color-gray-light-500;
}
}
.cf-media-gallery__actions {
.postbox & {
border-top: 1px solid $wp-color-gray-light-500;
}
.cf-container-term-meta & {
padding-left: 0;
}
}
.cf-media-gallery__list {
.cf-container-term-meta & {
margin: 0 -8px;
max-height: 285px;
}
}
.cf-media-gallery__item {
.cf-container-term-meta & {
flex-basis: 20%;
}
}
.cf-media-gallery__item-inner {
.postbox &,
.cf-container-term-meta & {
border: 1px solid $wp-color-gray-light-500;
}
}
.cf-media-gallery__item-preview {
.postbox &,
.cf-container-term-meta & {
background-color: $wp-color-gray-light-200;
}
}
.cf-media-gallery__item-name {
.postbox &,
.cf-container-term-meta & {
border-top: 1px solid $wp-color-gray-light-500;
background-color: $wp-color-gray-light-200;
}
}

View file

@ -0,0 +1,4 @@
/**
* Internal dependencies.
*/
import './style.scss';

View file

@ -0,0 +1,15 @@
/* ==========================================================================
Multiselect
========================================================================== */
.cf-multiselect__control {
.postbox & {
border-color: #ddd;
box-shadow: 0 1px 2px transparentize($color-black, 0.93) inset;
}
.postbox &--is-focused,
.postbox &--is-focused:hover {
box-shadow: 0 0 2px transparentize($wp-color-blue, 0.2);
}
}

View file

@ -0,0 +1,4 @@
/**
* The internal dependencies.
*/
import './style.scss';

View file

@ -0,0 +1,21 @@
/* ==========================================================================
Radio
========================================================================== */
.cf-radio__label {
.inside .cf-radio-image .cf-radio__input:focus ~ & {
box-shadow: 0 0 2px transparentize($wp-color-medium-blue, 0.2);
}
}
.cf-radio-image__image {
.inside & {
border: 1px solid $wp-color-gray-light-800;
}
.inside .cf-radio__input:focus ~ .cf-radio__label &,
.inside .cf-radio__input:checked ~ .cf-radio__label & {
outline: 4px solid $wp-color-medium-blue;
}
}

View file

@ -0,0 +1,13 @@
/**
* External dependencies.
*/
import { withDispatch } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
addFilter( 'carbon-fields.sidebar.metabox', 'carbon-fields/metaboxes', withDispatch( ( dispatch ) => {
const { receiveSidebar } = dispatch( 'carbon-fields/metaboxes' );
return {
onAdded: receiveSidebar
};
} ) );

View file

@ -0,0 +1,175 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import takeUntil from 'callbag-take-until';
import distinctUntilChanged from 'callbag-distinct-until-changed';
import { pipe, merge } from 'callbag-basics';
import { select } from '@wordpress/data';
import {
get,
map,
find,
some,
pick,
keyBy,
repeat,
isEqual,
fromPairs,
difference,
startsWith
} from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { fromSelector, withConditionalLogic } from '@carbon-fields/core';
/**
* Returns all root fields from the given holder
* while excluding some of them.
*
* @param {Object} fieldsHolder
* @param {Object} allFields
* @param {string[]} excludedIds
* @return {Object[]}
*/
function getFieldsFromFieldsHolder( fieldsHolder, allFields, excludedIds = [] ) {
if ( typeof fieldsHolder === 'undefined' ) {
return [];
}
return pick( allFields, difference( map( fieldsHolder.fields, 'id' ), excludedIds ) );
}
/**
* Adds the `parent.` parent prefix to field's name.
*
* @param {Object[]} fields
* @param {number} depth
* @return {Array[]}
*/
function mapParentPrefix( fields, depth = 0 ) {
return map( fields, ( field ) => ( [
field.id,
`${ repeat( 'parent.', depth ) }${ field.base_name }`
] ) );
}
/**
* The function used to track dependencies required
* by conditional logic.
*
* @param {Object} props
* @param {Object} component
* @return {Object}
*/
function input( props, component ) {
const { getFieldsByContainerId } = select( 'carbon-fields/metaboxes' );
return pipe(
merge(
of( getFieldsByContainerId( props.containerId ) ),
fromSelector( getFieldsByContainerId, props.containerId )
),
takeUntil( component.unmount ),
distinctUntilChanged( isEqual )
);
}
/**
* The function that provides the data that needs to be
* evaluated by conditional logic.
*
* @param {Object} props
* @param {Object} fields
* @return {Object}
*/
function output( props, fields ) {
fields = keyBy( fields, 'id' );
const container = select( 'carbon-fields/metaboxes' ).getContainerById( props.containerId );
const isTopLevelField = some( container.fields, [ 'id', props.id ] );
let siblingFields = [];
if ( isTopLevelField ) {
siblingFields = getFieldsFromFieldsHolder( container, fields, [ props.id ] );
siblingFields = mapParentPrefix( siblingFields );
} else {
const fieldName = props.name.replace( new RegExp( `^${ window.cf.config.compactInputKey }\\[(.+?)\\]` ), '$1' );
// Get the root field.
const rootField = find( fields, ( field ) => {
return field.container_id === props.containerId
&& startsWith( fieldName, field.name );
} );
// Get the hierarchy.
let path = fieldName.split( /\[|\]/g );
// Remove the chunk with name of root field
// because we already have it.
path.shift();
// Remove any chunks that don't have a value.
path = path.filter( ( chunk ) => chunk !== '' );
// Remove the chunk with name of field
// because we don't needed it.
path.pop();
// Keep reference to the depth
// so we can add the `parent.` prefix.
let depth = path.reduce( ( accumulator, chunk ) => {
return isNaN( chunk )
? accumulator + 1
: accumulator;
}, 0 );
// Collect fields that are siblings of root field.
siblingFields = getFieldsFromFieldsHolder( container, fields, [ rootField.id ] );
siblingFields = mapParentPrefix( siblingFields, depth + 1 );
// Keep reference to the full path of the field.
let pathPrefix = `${ rootField.id }.value`;
while ( path.length > 0 ) {
const chunk = path.shift();
const isGroup = ! isNaN( chunk );
const isNestedComplex = ! isGroup;
if ( isGroup ) {
pathPrefix = `${ pathPrefix }[${ chunk }]`;
const group = get( fields, pathPrefix );
const groupFields = getFieldsFromFieldsHolder( group, fields, [ props.id ] );
siblingFields = siblingFields.concat( mapParentPrefix( groupFields, depth ) );
pathPrefix = `${ pathPrefix }.fields`;
}
if ( isNestedComplex ) {
const groupField = find( get( fields, pathPrefix ), [ 'name', chunk ] );
if ( groupField ) {
pathPrefix = `${ groupField.id }.value`;
}
depth--;
}
}
}
siblingFields = siblingFields.map( ( [ id, name ] ) => ( [
name,
get( fields, `${ id }.value` )
] ) );
return fromPairs( siblingFields );
}
export default withConditionalLogic( input, output );

View file

@ -0,0 +1,24 @@
/**
* External dependencies.
*/
import { withSelect } from '@wordpress/data';
import { createHigherOrderComponent } from '@wordpress/compose';
/**
* Creates a high-order component which adds connection
* to the store.
*
* @param {Function} Component
* @return {Function}
*/
export default createHigherOrderComponent( ( Component ) => {
const applyWithSelect = withSelect( ( select, { id } ) => {
const container = select( 'carbon-fields/metaboxes' ).getContainerById( id );
return {
container
};
} );
return applyWithSelect( Component );
}, 'withContainer' );

View file

@ -0,0 +1,48 @@
/**
* External dependencies.
*/
import { withSelect, withDispatch } from '@wordpress/data';
import { compose, createHigherOrderComponent } from '@wordpress/compose';
/**
* Creates a high-order component which adds connection
* to the store.
*
* @param {Function} Component
* @return {Function}
*/
export default createHigherOrderComponent( ( Component ) => {
const applyWithSelect = withSelect( ( select, props ) => {
const { compactInput, compactInputKey } = window.cf.config;
const field = select( 'carbon-fields/metaboxes' ).getFieldById( props.id );
const value = field && field.value;
let name = props.name || field.name;
/**
* Wrap top-level field names in compact input key.
*
* The fields in widgets don't need this prefix because
* their input is already compacted by the `widget` namespace.
*/
if ( compactInput && ! props.name && name.indexOf( 'widget-carbon_fields' ) === -1 ) {
name = `${ compactInputKey }[${ name }]`;
}
return {
field,
name,
value
};
} );
const applyWithDispatch = withDispatch( ( dispatch ) => {
const { updateFieldValue } = dispatch( 'carbon-fields/metaboxes' );
return {
onChange: updateFieldValue
};
} );
return compose( applyWithSelect, applyWithDispatch )( Component );
}, 'withField' );

View file

@ -0,0 +1,39 @@
/**
* External dependencies.
*/
import { setLocaleData } from '@wordpress/i18n';
import { addAction } from '@wordpress/hooks';
/**
* Internal dependencies.
*/
import './store';
import './fields';
import initializeMonitors from './monitors';
import initializeContainers from './containers';
import isGutenberg from './utils/is-gutenberg';
/**
* Public API.
*/
export { registerContainerType, getContainerType } from './containers/registry';
/**
* Sets the locale data for the package type
*/
setLocaleData( window.cf.config.locale, 'carbon-fields-ui' );
/**
* Determines the rendering context.
*
* @type {string}
*/
const context = isGutenberg() ? 'gutenberg' : 'classic';
/**
* Abracadabra! Poof! Containers everywhere ...
*/
addAction( 'carbon-fields.init', 'carbon-fields/metaboxes', () => {
initializeContainers( context );
initializeMonitors( context );
} );

View file

@ -0,0 +1,5 @@
export const PAGE_NOW_WIDGETS = 'widgets.php';
export const PAGE_NOW_CUSTOMIZE = 'customize.php';
export const CARBON_FIELDS_CONTAINER_ID_PREFIX = 'carbon_fields_container_';
export const CARBON_FIELDS_CONTAINER_WIDGET_ID_PREFIX = 'carbon_fields_';

View file

@ -0,0 +1,52 @@
/**
* External dependencies.
*/
import produce from 'immer';
import { applyFilters } from '@wordpress/hooks';
import { assign } from 'lodash';
import {
pipe,
merge,
scan
} from 'callbag-basics';
/**
* Internal dependencies.
*/
import './post-parent';
import './post-format';
import './post-template';
import './post-term';
import './term-parent';
import './user-role';
/**
* The function that controls the stream of side effects.
*
* @param {Object} props
* @param {string} props.context
* @return {Object}
*/
// eslint-disable-next-line no-unused-vars
export default function aperture( component, { context } ) {
const postParent$ = applyFilters( `carbon-fields.conditional-display-post-parent.${ context }` );
const postFormat$ = applyFilters( `carbon-fields.conditional-display-post-format.${ context }` );
const postTemplate$ = applyFilters( `carbon-fields.conditional-display-post-template.${ context }` );
const postTerm$ = applyFilters( `carbon-fields.conditional-display-post-term.${ context }` );
const termParent$ = applyFilters( `carbon-fields.conditional-display-term-parent.${ context }` );
const userRole$ = applyFilters( `carbon-fields.conditional-display-user-role.${ context }` );
return pipe(
merge(
postParent$,
postFormat$,
postTemplate$,
postTerm$,
termParent$,
userRole$
),
scan( ( previous, current ) => produce( previous, ( draft ) => {
assign( draft, current );
} ) )
);
}

View file

@ -0,0 +1,79 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import startWith from 'callbag-start-with';
import fromDelegatedEvent from 'callbag-from-delegated-event';
import distinctUntilChanged from 'callbag-distinct-until-changed';
import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import {
pipe,
map,
filter
} from 'callbag-basics';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* The default state.
*
* @type {Object}
*/
const INITIAL_STATE = {
post_format: 'standard'
};
/**
* Extracts `post_format` from the input.
*
* @param {Object} input
* @return {Object}
*/
function getPostFormatFromRadioInput( input ) {
let value = input.value;
// Normalize the value of "Standard" input.
if ( value === '0' ) {
value = 'standard';
}
return {
post_format: value
};
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-post-format.classic', 'carbon-fields/metaboxes', () => {
const node = document.querySelector( 'div#post-formats-select' );
if ( ! node ) {
return of( INITIAL_STATE );
}
return pipe(
fromDelegatedEvent( node, 'input.post-format', 'change' ),
map( ( { target } ) => getPostFormatFromRadioInput( target ) ),
startWith( getPostFormatFromRadioInput( node.querySelector( 'input.post-format:checked' ) ) )
);
} );
/**
* Defines the side effects for Gutenberg.
*/
addFilter( 'carbon-fields.conditional-display-post-format.gutenberg', 'carbon-fields/metaboxes', ( ) => {
return pipe(
fromSelector( select( 'core/editor' ).getEditedPostAttribute, 'format' ),
distinctUntilChanged(),
filter( Boolean ),
map( ( postFormat ) => ( {
post_format: postFormat
} ) ),
startWith( INITIAL_STATE )
);
} );

View file

@ -0,0 +1,150 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import startWith from 'callbag-start-with';
import distinctUntilChanged from 'callbag-distinct-until-changed';
import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import {
get,
find,
isEqual
} from 'lodash';
import {
pipe,
map,
combine,
fromEvent
} from 'callbag-basics';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* Internal dependencies.
*/
import getParentIdFromOption from '../utils/get-parent-id-from-option';
import getLevelFromOption from '../utils/get-level-from-option';
import getAncestorsFromOption from '../utils/get-ancestors-from-option';
/**
* The default state.
*
* @type {Object}
*/
const INITIAL_STATE = {
post_ancestors: [],
post_parent_id: 0,
post_level: 1
};
/**
* Extracts the `post_ancestors`, `post_parent_id` & `post_level` from the select.
*
* @param {Object} node
* @return {Object}
*/
function getParentIdAncestorsAndLevelFromSelect( node ) {
const option = node.options[ node.selectedIndex ];
const ancestors = getAncestorsFromOption( option );
const parentId = getParentIdFromOption( option );
const level = getLevelFromOption( option ) + 1;
return {
post_ancestors: ancestors,
post_parent_id: parentId,
post_level: level
};
}
/**
* Extracts `post_ancestors` from the list.
*
* @param {number} parentId
* @param {Object[]} posts
* @param {Array} ancestors
* @return {number[]}
*/
function getAncestorsFromPostsList( parentId, posts, ancestors = [] ) {
const parent = find( posts, [ 'id', parentId ] );
if ( ! parent ) {
return ancestors;
}
ancestors.unshift( parent.id );
if ( parent.parent ) {
return getAncestorsFromPostsList( parent.parent, posts, ancestors );
}
return ancestors;
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-post-parent.classic', 'carbon-fields/metaboxes', ( ) => {
const node = document.querySelector( 'select#parent_id' );
if ( ! node ) {
return of( INITIAL_STATE );
}
return pipe(
fromEvent.default( node, 'change' ),
map( ( { target } ) => getParentIdAncestorsAndLevelFromSelect( target ) ),
startWith( getParentIdAncestorsAndLevelFromSelect( node ) )
);
} );
/**
* Defines the side effects for Gutenberg.
*/
addFilter( 'carbon-fields.conditional-display-post-parent.gutenberg', 'carbon-fields/metaboxes', ( ) => {
const { getPostType, getEntityRecords } = select( 'core' );
return pipe(
combine(
fromSelector( select( 'core/editor' ).getCurrentPostId ),
fromSelector( select( 'core/editor' ).getEditedPostAttribute, 'type' ),
fromSelector( select( 'core/editor' ).getEditedPostAttribute, 'parent' )
),
distinctUntilChanged( isEqual ),
map( ( [ postId, postTypeSlug, parentId ] ) => {
parentId = parseInt( parentId, 10 );
if ( isNaN( parentId ) ) {
return INITIAL_STATE;
}
const postType = getPostType( postTypeSlug );
const isHierarchical = get( postType, [ 'hierarchical' ], false );
if ( ! isHierarchical ) {
return INITIAL_STATE;
}
// Borrowed from https://github.com/WordPress/gutenberg/blob/master/packages/editor/src/components/page-attributes/parent.js
const items = getEntityRecords( 'postType', postTypeSlug, {
per_page: -1,
exclude: postId,
parent_exclude: postId,
orderby: 'menu_order',
order: 'asc'
} ) || [];
const ancestors = getAncestorsFromPostsList( parentId, items );
const level = ancestors.length + 1;
return {
post_ancestors: ancestors,
post_parent_id: parentId,
post_level: level
};
} )
);
} );

View file

@ -0,0 +1,80 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import startWith from 'callbag-start-with';
import distinctUntilChanged from 'callbag-distinct-until-changed';
import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import { isString } from 'lodash';
import {
pipe,
map,
filter,
fromEvent
} from 'callbag-basics';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* The default state.
*
* @type {Object}
*/
const INITIAL_STATE = {
post_template: ''
};
/**
* Extracts `post_template` from the select.
*
* @param {Object} node
* @return {Object}
*/
function getPostTemplateFromSelect( node ) {
let { value } = node;
// In Gutenberg for the "Default" template is used an empty string.
if ( value === 'default' ) {
value = '';
}
return {
post_template: value
};
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-post-template.classic', 'carbon-fields/metaboxes', () => {
const node = document.querySelector( 'select#page_template' );
if ( ! node ) {
return of( INITIAL_STATE );
}
return pipe(
fromEvent.default( node, 'change' ),
map( ( { target } ) => getPostTemplateFromSelect( target ) ),
startWith( getPostTemplateFromSelect( node ) )
);
} );
/**
* Defines the side effects for Gutenberg.
*/
addFilter( 'carbon-fields.conditional-display-post-template.gutenberg', 'carbon-fields/metaboxes', () => {
return pipe(
fromSelector( select( 'core/editor' ).getEditedPostAttribute, 'template' ),
distinctUntilChanged(),
filter( isString ),
map( ( postTemplate ) => ( {
post_template: postTemplate
} ) ),
startWith( INITIAL_STATE )
);
} );

View file

@ -0,0 +1,192 @@
/**
* External dependencies.
*/
import produce from 'immer';
import startWith from 'callbag-start-with';
import fromDelegatedEvent from 'callbag-from-delegated-event';
import {
merge,
pipe,
scan,
map,
filter,
fromEvent
} from 'callbag-basics';
import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import { pull, fromPairs } from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
const TAGS_DELIMITER = ',';
/**
* Applies a monkey patch to the specified method of `window.tagBox` API
* so we can detect changes of the non-hierarchical taxonomies.
*
* @param {Object} tagBox
* @param {string} method
* @return {void}
*/
function patchWordPressTagBoxAPI( tagBox, method ) {
tagBox[ `original_${ method }` ] = tagBox[ method ];
tagBox[ method ] = function( ...args ) {
const event = new Event( 'change' );
const textarea = window.jQuery( args[ 0 ] )
.closest( '.postbox' )
.find( 'textarea.the-tags' )
.get( 0 );
const result = tagBox[ `original_${ method }` ]( ...args );
textarea.dispatchEvent( event );
return result;
};
}
if ( window.tagBox ) {
patchWordPressTagBoxAPI( window.tagBox, 'parseTags' );
patchWordPressTagBoxAPI( window.tagBox, 'flushTags' );
}
/**
* Extracts the terms of a hierarchical taxonomy.
*
* @param {string} taxonomy
* @return {Object}
*/
function getTermsFromChecklist( taxonomy ) {
const inputs = document.querySelectorAll( `#${ taxonomy }checklist input[type="checkbox"]:checked` );
return [ ...inputs ].reduce( ( memo, input ) => {
const value = parseInt( input.value, 10 );
memo[ taxonomy ].push( value );
return memo;
}, {
[ taxonomy ]: []
} );
}
/**
* Extracts the terms of a non-hierarchical taxonomy.
*
* @param {string} taxonomy
* @return {Object}
*/
function getTermsFromText( taxonomy ) {
const node = document.querySelector( `#tagsdiv-${ taxonomy } textarea.the-tags` );
const terms = node.value
? node.value.split( TAGS_DELIMITER )
: [];
return {
[ taxonomy ]: terms
};
}
/**
* Keeps track of the hierarchical taxonomies like `categories`.
*
* @return {Function}
*/
function trackHierarchicalTaxonomies() {
const nodes = document.querySelectorAll( 'div[id^="taxonomy-"]' );
return [ ...nodes ].map( ( node ) => {
const taxonomy = node.id.replace( 'taxonomy-', '' );
return pipe(
fromDelegatedEvent( node.querySelector( `#${ taxonomy }checklist` ), 'input[type="checkbox"]', 'change' ),
scan( ( stack, { target } ) => {
return produce( stack, ( draft ) => {
const value = parseInt( target.value, 10 );
if ( target.checked ) {
draft[ taxonomy ].push( value );
} else {
pull( draft[ taxonomy ], value );
}
} );
}, {
[ taxonomy ]: []
} ),
startWith( getTermsFromChecklist( taxonomy ) )
);
} );
}
/**
* Keeps track of the non-hierarchical taxonomies like `tags`.
*
* @return {Function}
*/
function trackNonHierarchicalTaxonomies() {
const nodes = document.querySelectorAll( 'div[id^="tagsdiv-"]' );
return [ ...nodes ].map( ( node ) => {
const taxonomy = node.id.replace( 'tagsdiv-', '' );
return pipe(
fromEvent.default( node.querySelector( 'textarea.the-tags' ), 'change' ),
map( ( { target } ) => ( {
[ taxonomy ]: target.value
? target.value.split( TAGS_DELIMITER )
: []
} ) ),
startWith( getTermsFromText( taxonomy ) )
);
} );
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-post-term.classic', 'carbon-fields/metaboxes', () => {
return pipe(
merge(
...trackHierarchicalTaxonomies(),
...trackNonHierarchicalTaxonomies()
),
scan( ( previous, current ) => {
return {
post_term: {
...previous.post_term,
...current
}
};
}, {
post_term: {}
} )
);
} );
/**
* Defines the side effects for Gutenberg.
*/
addFilter( 'carbon-fields.conditional-display-post-term.gutenberg', 'carbon-fields/metaboxes', () => {
const { getTaxonomies } = select( 'core' );
const { getEditedPostAttribute } = select( 'core/editor' );
// Borrowed from https://github.com/WordPress/gutenberg/blob/master/packages/editor/src/components/post-taxonomies/index.js
return pipe(
fromSelector( getTaxonomies, { per_page: -1 } ),
filter( Boolean ),
map( ( taxonomies ) => {
const pairs = taxonomies.map( ( taxonomy ) => ( [
taxonomy.slug,
getEditedPostAttribute( taxonomy.rest_base ) || []
] ) );
return {
post_term: fromPairs( pairs )
};
} )
);
} );

View file

@ -0,0 +1,66 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import startWith from 'callbag-start-with';
import { addFilter } from '@wordpress/hooks';
import {
pipe,
map,
fromEvent
} from 'callbag-basics';
/**
* Internal dependencies.
*/
import getParentIdFromOption from '../utils/get-parent-id-from-option';
import getLevelFromOption from '../utils/get-level-from-option';
import getAncestorsFromOption from '../utils/get-ancestors-from-option';
/**
* The default state.
*
* @type {Object}
*/
const INITIAL_STATE = {
term_ancestors: [],
term_parent: 0,
term_level: 1
};
/**
* Extracts the `term_ancestors`, `term_parent` & `term_level` from the select.
*
* @param {Object} node
* @return {Object}
*/
function getParentIdAncestorsAndLevelFromSelect( node ) {
const option = node.options[ node.selectedIndex ];
const ancestors = getAncestorsFromOption( option );
const parentId = getParentIdFromOption( option );
const level = getLevelFromOption( option ) + 1;
return {
term_ancestors: ancestors,
term_parent: parentId,
term_level: level
};
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-term-parent.classic', 'carbon-fields/metaboxes', ( ) => {
const node = document.querySelector( 'select#parent' );
if ( ! node ) {
return of( INITIAL_STATE );
}
return pipe(
fromEvent.default( node, 'change' ),
map( ( { target } ) => getParentIdAncestorsAndLevelFromSelect( target ) ),
startWith( getParentIdAncestorsAndLevelFromSelect( node ) )
);
} );

View file

@ -0,0 +1,60 @@
/**
* External dependencies.
*/
import of from 'callbag-of';
import startWith from 'callbag-start-with';
import { addFilter } from '@wordpress/hooks';
import {
pipe,
map,
fromEvent
} from 'callbag-basics';
/**
* The default state.
*
* @type {Object}
*/
const INITIAL_STATE = {
user_role: ''
};
/**
* Extracts `user_role` from a select.
*
* @param {Object} node
* @return {Object}
*/
function getRoleFromSelect( node ) {
return {
user_role: node.value
};
}
/**
* Defines the side effects for Classic Editor.
*/
addFilter( 'carbon-fields.conditional-display-user-role.classic', 'carbon-fields/metaboxes', ( ) => {
const node = document.querySelector( 'select#role' );
if ( ! node ) {
const fieldset = document.querySelector( 'fieldset[data-profile-role]' );
// The selectbox doesn't exist on the "Profile" page.
// So we need to read the role from the container in DOM.
if ( fieldset ) {
return of( {
user_role: fieldset.dataset.profileRole
} );
}
return of( INITIAL_STATE );
}
return pipe(
fromEvent.default( node, 'change' ),
map( ( { target } ) => getRoleFromSelect( target ) ),
startWith( getRoleFromSelect( node ) )
);
} );

View file

@ -0,0 +1,32 @@
/**
* External dependencies.
*/
import { intersection } from 'lodash';
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
operators: [ 'IN', 'NOT IN' ],
/**
* @inheritdoc
*/
evaluate( a, operator, b ) {
switch ( operator ) {
case 'IN':
return intersection( a, b ).length > 0;
case 'NOT IN':
return intersection( a, b ).length === 0;
default:
return false;
}
}
};

View file

@ -0,0 +1,33 @@
/**
* External dependencies.
*/
import { includes } from 'lodash';
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
operators: [ '=', '!=' ],
/**
* @inheritdoc
*/
evaluate( a, operator, b ) {
switch ( operator ) {
case '=':
return includes( a, b );
case '!=':
return ! includes( a, b );
default:
return false;
}
}
};

View file

@ -0,0 +1,30 @@
export default {
/**
* The supported operators.
*
* @type {string[]}
*/
operators: [],
/**
* Checks if the operator is supported.
*
* @param {string} operator
* @return {boolean}
*/
isOperatorSupported( operator ) {
return this.operators.indexOf( operator ) > -1;
},
/**
* Performs the comparison.
*
* @param {mixed} a
* @param {string} operator
* @param {mixed} b
* @return {boolean}
*/
evaluate() {
return false;
}
};

View file

@ -0,0 +1,27 @@
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
operators: [ 'IN', 'NOT IN' ],
/**
* @inheritdoc
*/
evaluate( a, operator, b ) {
switch ( operator ) {
case 'IN':
return b.indexOf( a ) > -1;
case 'NOT IN':
return b.indexOf( a ) === -1;
default:
return false;
}
}
};

View file

@ -0,0 +1,30 @@
/* eslint eqeqeq: "off" */
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
operators: [ '=', '!=' ],
/**
* @inheritdoc
*/
evaluate( a, operator, b ) {
switch ( operator ) {
case '=':
return a == b;
case '!=':
return a != b;
default:
return false;
}
}
};

View file

@ -0,0 +1,31 @@
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
operators: [ '>', '>=', '<', '<=' ],
/**
* @inheritdoc
*/
evaluate( a, operator, b ) {
switch ( operator ) {
case '>':
return a > b;
case '>=':
return a >= b;
case '<':
return a < b;
case '<=':
return a <= b;
default:
return false;
}
}
};

View file

@ -0,0 +1,70 @@
/**
* External dependencies.
*/
import { __, sprintf } from '@wordpress/i18n';
import { find } from 'lodash';
/**
* Internal dependencies.
*/
import equality from '../comparers/equality';
import contain from '../comparers/contain';
import scalar from '../comparers/scalar';
export default {
/**
* The supported comparers.
*
* @type {Function[]}
*/
comparers: [
equality,
contain,
scalar
],
/**
* Checks if the condition is fulfiled.
*
* @param {Object} definition
* @param {Object} values
* @return {boolean}
*/
isFulfiled( definition, values ) {
const { compare, value } = definition;
return this.firstComparerIsCorrect( this.getEnvironmentValue( definition, values ), compare, value );
},
/**
* Checks if any comparer is correct for `a` and `b`.
*
* @param {mixed} a
* @param {string} operator
* @param {mixed} b
* @return {boolean}
*/
firstComparerIsCorrect( a, operator, b ) {
const comparer = find( this.comparers, ( item ) => item.isOperatorSupported( operator ) );
if ( ! comparer ) {
// eslint-disable-next-line no-console
console.error( sprintf( __( 'Unsupported container condition comparison operator used - "%1$s".', 'carbon-fields-ui' ), operator ) );
return false;
}
return comparer.evaluate( a, operator, b );
},
/**
* Returns the value from the environment.
*
* @param {Object} definition
* @param {Object} values
* @return {Object}
*/
getEnvironmentValue( definition, values ) {
return values[ definition.type ];
}
};

View file

@ -0,0 +1,15 @@
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
getEnvironmentValue() {
return true;
}
};

View file

@ -0,0 +1,30 @@
/**
* External dependencies.
*/
import { get } from 'lodash';
/**
* Internal dependencies.
*/
import anyEquality from '../comparers/any-equality';
import anyContain from '../comparers/any-contain';
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
comparers: [
anyEquality,
anyContain
],
/**
* @inheritdoc
*/
getEnvironmentValue( definition, values ) {
return get( values, 'post_ancestors', [] );
}
};

View file

@ -0,0 +1,33 @@
/**
* External dependencies.
*/
import { isArray } from 'lodash';
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
isFulfiled( definition, values ) {
definition = { ...definition };
// In Gutenberg for the "Default" template is used an empty string.
if ( definition.value === 'default' ) {
definition.value = '';
} else if ( isArray( definition.value ) ) {
const defaultIndex = definition.value.indexOf( 'default' );
if ( defaultIndex !== -1 ) {
definition.value[ defaultIndex ] = '';
}
}
return base.isFulfiled( definition, values );
}
};

View file

@ -0,0 +1,71 @@
/**
* External dependencies.
*/
import { get, isArray } from 'lodash';
/**
* Internal dependencies.
*/
import anyEquality from '../comparers/any-equality';
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
comparers: [
anyEquality
],
/**
* @inheritdoc
*/
isFulfiled( definition, values ) {
let { compare, value } = definition;
if ( isArray( value ) ) {
let method;
switch ( compare ) {
case 'IN':
compare = '=';
method = 'some';
break;
case 'NOT IN':
compare = '!=';
method = 'every';
break;
default:
return false;
}
const results = value.map( ( descriptor ) => {
return this.isFulfiled( {
...definition,
compare,
value: descriptor
}, values );
} );
return results[ method ]( Boolean );
}
// TODO: Improve value resolution in context of Gutenberg
value = value.taxonomy_object.hierarchical
? value.term_object.term_id
: value.term_object.name;
return this.firstComparerIsCorrect( this.getEnvironmentValue( definition, values ), compare, value );
},
/**
* @inheritdoc
*/
getEnvironmentValue( definition, values ) {
return get( values, `post_term.${ definition.value.taxonomy }`, [] );
}
};

View file

@ -0,0 +1,50 @@
/**
* External dependencies.
*/
import {
get,
isArray,
isPlainObject
} from 'lodash';
/**
* Internal dependencies.
*/
import anyEquality from '../comparers/any-equality';
import anyContain from '../comparers/any-contain';
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
comparers: [
anyEquality,
anyContain
],
/**
* @inheritdoc
*/
isFulfiled( definition, values ) {
const { compare } = definition;
let { value } = definition;
if ( isArray( value ) ) {
value = value.map( ( item ) => item.term_object.term_id );
} else if ( isPlainObject( value ) ) {
value = value.term_object.term_id;
}
return this.firstComparerIsCorrect( this.getEnvironmentValue( definition, values ), compare, value );
},
/**
* @inheritdoc
*/
getEnvironmentValue( definition, values ) {
return get( values, 'term_ancestors', [] );
}
};

View file

@ -0,0 +1,29 @@
/**
* External dependencies.
*/
import { isArray, isPlainObject } from 'lodash';
/**
* Internal dependencies.
*/
import base from './base';
export default {
...base,
/**
* @inheritdoc
*/
isFulfiled( definition, values ) {
const { compare } = definition;
let { value } = definition;
if ( isArray( value ) ) {
value = value.map( ( item ) => item.term_object.term_id );
} else if ( isPlainObject( value ) ) {
value = value.term_object.term_id;
}
return this.firstComparerIsCorrect( this.getEnvironmentValue( definition, values ), compare, value );
}
};

View file

@ -0,0 +1,136 @@
/**
* External dependencies.
*/
import { __, sprintf } from '@wordpress/i18n';
import { get, map } from 'lodash';
import { createRoot } from '@wordpress/element';
/**
* Internal dependencies.
*/
import { renderContainer } from '../../../containers';
import base from '../conditions/base';
import boolean from '../conditions/boolean';
import postTerm from '../conditions/post-term';
import postTemplate from '../conditions/post-template';
import postAncestorId from '../conditions/post-ancestor-id';
import termParentId from '../conditions/term-parent-id';
import termAncestorId from '../conditions/term-ancestor-id';
import { getContainerRoot } from '../../../containers/root-registry';
/**
* Keeps track of supported conditions.
*
* @type {Object}
*/
const conditions = {
boolean,
post_term: postTerm,
post_ancestor_id: postAncestorId,
post_parent_id: base,
post_level: base,
post_format: base,
post_template: postTemplate,
term_level: base,
term_parent: termParentId,
term_ancestor: termAncestorId,
user_role: base
};
/**
* Walks through the definitions and evaluates the conditions.
*
* @param {Object[]} definitions
* @param {Object} values
* @param {string} relation
* @return {boolean}
*/
function evaluate( definitions, values, relation ) {
const results = definitions.map( ( definition ) => {
if ( ! definition.relation ) {
const condition = get( conditions, definition.type );
if ( condition ) {
return condition.isFulfiled( definition, values );
} else { // eslint-disable-line no-else-return
// eslint-disable-next-line no-console
console.error( sprintf( __( 'Unsupported container condition - "%1$s".', 'carbon-fields-ui' ), definition.type ) );
return false;
}
} else { // eslint-disable-line no-else-return
return evaluate( definition.conditions, values, definition.relation );
}
} );
switch ( relation ) {
case 'AND':
return results.every( Boolean );
case 'OR':
return results.some( Boolean );
default:
// eslint-disable-next-line no-console
console.error( sprintf( __( 'Unsupported container condition relation used - "%1$s".', 'carbon-fields-ui' ), relation ) );
return false;
}
}
/**
* The function that causes the side effects.
*
* @param {Object} props
* @param {Object} props.containers
* @param {string} props.context
* @return {Function}
*/
export default function handler( { containers, context } ) {
return function( effect ) {
const results = map( containers, ( container, id ) => {
return [
id,
evaluate( container.conditions.conditions, effect, container.conditions.relation )
];
} );
results.forEach( ( [ id, result ] ) => {
const postboxNode = document.getElementById( id );
const containerNode = document.querySelector( `.container-${ id }` );
const isMounted = !! containerNode?.dataset?.mounted;
if ( postboxNode ) {
postboxNode.hidden = ! result;
}
if ( containerNode ) {
if ( createRoot ) {
const containerRoot = getContainerRoot( id );
if ( result && ! containerRoot ) {
renderContainer( containers[ id ], context );
}
if ( ! result && containerRoot ) {
containerRoot.unmount();
}
} else {
if ( result && ! isMounted ) {
renderContainer( containers[ id ], context );
}
if ( ! result && isMounted ) {
delete containerNode?.dataset?.mounted;
// Rely on React's internals instead of `unmountComponentAtNode`
// due to https://github.com/facebook/react/issues/13690.
// TODO: Conditionally render the fields in the container, this way
// we can move away from mount/unmount cycles.
containerNode?._reactRootContainer?.unmount();
}
}
}
} );
};
}

View file

@ -0,0 +1,36 @@
/**
* External dependencies.
*/
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { withEffects } from 'refract-callbag';
/**
* Internal dependencies.
*/
import aperture from './aperture';
import handler from './handler';
/**
* Performs the evaluation of conditions.
*
* @return {null}
*/
function ConditionalDisplay() {
return null;
}
const applyWithSelect = withSelect( ( select ) => {
const containers = select( 'carbon-fields/metaboxes' ).getContainers();
return {
containers
};
} );
const applyWithEffects = withEffects( aperture, { handler } );
export default compose(
applyWithSelect,
applyWithEffects
)( ConditionalDisplay );

View file

@ -0,0 +1,38 @@
/**
* Internal dependencies.
*/
import getLevelFromOption from './get-level-from-option';
/**
* Extracts the ancestors of the post/term from an option.
*
* @param {Object} option
* @return {number[]}
*/
export default function getAncestorsFromOption( option ) {
const ancestors = [];
let previousOption = option;
let level = getLevelFromOption( option );
while ( level > 0 && previousOption ) {
if ( getLevelFromOption( previousOption ) !== level ) {
previousOption = previousOption.previousSibling;
// Skip this iteration because the option isn't an ancestor.
continue;
}
const id = parseInt( previousOption.value, 10 );
if ( id > 0 ) {
ancestors.unshift( id );
}
previousOption = previousOption.previousSibling;
level--;
}
return ancestors;
}

View file

@ -0,0 +1,19 @@
/**
* Extracts the level from an option.
*
* @param {Object} option
* @return {number}
*/
export default function getLevelFromOption( option ) {
let level = 0;
if ( option.className ) {
const matches = option.className.match( /^level-(\d+)/ );
if ( matches ) {
level = parseInt( matches[ 1 ], 10 ) + 1;
}
}
return level;
}

View file

@ -0,0 +1,11 @@
/**
* Extracts the id of the post/term parent from an option.
*
* @param {Object} option
* @return {number}
*/
export default function getParentIdFromOption( option ) {
const value = parseInt( option.value, 10 );
return ( ! isNaN( value ) && value >= 0 ) ? value : 0;
}

View file

@ -0,0 +1,61 @@
/**
* External dependencies.
*/
import { Fragment, render, createRoot } from '@wordpress/element';
/**
* Internal dependencies.
*/
import SaveLock from './save-lock';
import ConditionalDisplay from './conditional-display';
import WidgetHandler from './widget-handler';
import RevisionsFlag from './revisions-flag';
import isGutenberg from '../utils/is-gutenberg';
import { PAGE_NOW_WIDGETS, PAGE_NOW_CUSTOMIZE } from '../lib/constants';
/**
* Initializes the monitors.
*
* @param {string} context
* @return {void}
*/
export default function initializeMonitors( context ) {
const { pagenow } = window.cf.config;
const MonitorElement = document.createElement( 'div' );
const MonitorComponent = <Fragment>
{ ! isGutenberg() && (
<SaveLock />
) }
{ ( pagenow === PAGE_NOW_WIDGETS || pagenow === PAGE_NOW_CUSTOMIZE ) && (
<WidgetHandler />
) }
<ConditionalDisplay context={ context } />
</Fragment>;
if ( createRoot ) {
createRoot( MonitorElement ).render( MonitorComponent );
} else {
render(
MonitorComponent,
MonitorElement,
);
}
const postStuffNode = document.querySelector( '#poststuff' );
if ( postStuffNode ) {
const postStuffElement = document.createElement( 'div' );
const postStuffComponenet = <RevisionsFlag />;
const postStuffChildElement = postStuffNode.appendChild( postStuffElement );
if ( createRoot ) {
createRoot( postStuffChildElement ).render( postStuffComponenet );
} else {
render( postStuffComponenet, postStuffChildElement );
}
}
}

View file

@ -0,0 +1,26 @@
/**
* External dependencies.
*/
import { withSelect } from '@wordpress/data';
/**
* Renders the input used to notify the backend about the changes.
*
* @param {Object} props
* @param {boolean} props.isDirty
* @return {mixed}
*/
function RevisionsFlag( props ) {
return (
<input
type="hidden"
name={ window.cf.config.revisionsInputKey }
disabled={ ! props.isDirty }
value="1"
/>
);
}
export default withSelect( ( select ) => ( {
isDirty: select( 'carbon-fields/metaboxes' ).isDirty()
} ) )( RevisionsFlag );

View file

@ -0,0 +1,51 @@
/**
* External dependencies.
*/
import { withEffects } from 'refract-callbag';
import { select } from '@wordpress/data';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* Toggles the ability to save the page.
*
* @return {null}
*/
function SaveLock() {
return null;
}
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return fromSelector( select( 'carbon-fields/metaboxes' ).isSavingLocked );
}
/**
* The function that causes the side effects.
*
* @return {Function}
*/
function handler() {
return function( isLocked ) {
const nodes = document.querySelectorAll( `
#publishing-action input#publish,
#publishing-action input#save,
#addtag input#submit,
#edittag input[type="submit"],
#your-profile input#submit
` );
nodes.forEach( ( node ) => {
node.disabled = isLocked;
} );
};
}
export default withEffects( aperture, { handler } )( SaveLock );

View file

@ -0,0 +1,47 @@
/**
* External dependencies.
*/
import { select } from '@wordpress/data';
import { withEffects } from 'refract-callbag';
/**
* Carbon Fields dependencies.
*/
import { fromSelector } from '@carbon-fields/core';
/**
* Toggles the alert if the page has not been saved before reload.
*
* @return {null}
*/
function UnsavedChanges() {
return null;
}
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return fromSelector( select( 'carbon-fields/metaboxes' ).isDirty );
}
/**
* The function that causes the side effects.
*
* @return {Function}
*/
function handler() {
return function( isDirty ) {
if ( ! isDirty ) {
return;
}
// Support for custom message has been removed
// Ref : https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload#Browser_compatibility
window.onbeforeunload = ( e ) => e;
};
}
export default withEffects( aperture, { handler } )( UnsavedChanges );

View file

@ -0,0 +1,183 @@
/**
* External dependencies.
*/
import { dispatch, select } from '@wordpress/data';
import { unmountComponentAtNode } from '@wordpress/element';
import { startsWith, flow } from 'lodash';
import { withEffects } from 'refract-callbag';
import {
map,
merge,
pipe,
filter
} from 'callbag-basics';
/**
* Internal dependencies.
*/
import urldecode from '../../utils/urldecode';
import flattenField from '../../utils/flatten-field';
import fromEventPattern from '../../utils/from-event-pattern';
import { renderContainer } from '../../containers';
import {
CARBON_FIELDS_CONTAINER_ID_PREFIX,
CARBON_FIELDS_CONTAINER_WIDGET_ID_PREFIX,
PAGE_NOW_CUSTOMIZE
} from '../../lib/constants';
/**
* Performs the re-initialization of widgets.
*
* @return {null}
*/
function WidgetHandler() {
return null;
}
/**
* Returns whether the widget is created by Carbon Fields.
*
* @param {string} identifier
* @return {boolean}
*/
function isCarbonFieldsWidget( identifier ) {
return identifier.indexOf( CARBON_FIELDS_CONTAINER_WIDGET_ID_PREFIX ) > -1;
}
/**
* The function that controls the stream of side effects.
*
* @return {Object}
*/
function aperture() {
return merge(
pipe(
fromEventPattern(
( handler ) => window.jQuery( document ).on( 'widget-added widget-updated', handler ),
( handler ) => window.jQuery( document ).off( 'widget-added widget-updated', handler ),
( event, $widget ) => ( {
event,
$widget
} )
),
filter( ( { $widget } ) => {
return isCarbonFieldsWidget( $widget[ 0 ].id );
} ),
map( ( payload ) => ( {
type: 'WIDGET_CREATED_OR_UPDATED',
payload
} ) )
),
pipe(
fromEventPattern(
( handler ) => window.jQuery( document ).on( 'ajaxSend', handler ),
( handler ) => window.jQuery( document ).off( 'ajaxSend', handler ),
( event, xhr, options, data ) => ( {
event,
xhr,
options,
data
} )
),
filter( ( { options } ) => {
return startsWith( options.data, CARBON_FIELDS_CONTAINER_ID_PREFIX );
} ),
map( ( payload ) => ( {
type: 'WIDGET_BEIGN_UPDATED_OR_DELETED',
payload
} ) )
)
);
}
/**
* The function that causes the side effects.
*
* @return {Function}
*/
function handler() {
return function( effect ) {
const { getContainerById } = select( 'carbon-fields/metaboxes' );
const {
addContainer,
removeContainer,
addFields,
removeFields
} = dispatch( 'carbon-fields/metaboxes' );
switch ( effect.type ) {
case 'WIDGET_CREATED_OR_UPDATED': {
const { event, $widget } = effect.payload;
const container = flow(
urldecode,
JSON.parse
)(
$widget
.find( '[data-json]' )
.data( 'json' )
);
const fields = [];
container.fields = container.fields.map( ( field ) => flattenField( field, container, fields ) );
addFields( fields );
addContainer( container );
renderContainer( container, 'classic' );
// WARNING: This piece of code manipulates the core behavior of WordPress Widgets.
// Some day this code will stop to work and we'll need to find another workaround.
//
// * Disable the submit { handler } since it breaks our validation logic.
// * Disable live preview mode because we can't detect when the widget is updated/synced.
// * Show the "Apply" button because it's hidden by the live mode.
if ( window.cf.config.pagenow === PAGE_NOW_CUSTOMIZE && event.type === 'widget-added' ) {
const widgetId = $widget
.find( '[name="widget-id"]' )
.val();
$widget
.find( '[name="savewidget"]' )
.show()
.end()
.find( '.widget-content:first' )
.off( 'keydown', 'input' )
.off( 'change input propertychange', ':input' );
const instance = wp.customize.Widgets.getWidgetFormControlForWidget( widgetId );
// Change the flag for 'live mode' so we can receive proper `widget-updated` events.
instance.liveUpdateMode = false;
}
break;
}
case 'WIDGET_BEIGN_UPDATED_OR_DELETED': {
const [ , widgetId ] = effect.payload.options.data.match( /widget-id=(.+?)&/ );
const containerId = `${ CARBON_FIELDS_CONTAINER_ID_PREFIX }${ widgetId }`;
// Get the container from the store.
const container = getContainerById( containerId );
// Remove the current instance from DOM.
unmountComponentAtNode( document.querySelector( `.container-${ containerId }` ) );
// Get the fields that belongs to the container.
const fieldsIds = _.map( container.fields, 'id' );
// Remove everything from the store.
removeContainer( containerId );
removeFields( fieldsIds );
break;
}
}
};
}
export default withEffects( aperture, { handler } )( WidgetHandler );

View file

@ -0,0 +1,169 @@
/**
* Returns an action object used to setup the state when first opening an editor.
*
* @param {Object[]} containers
* @param {Object} fields
* @return {Object}
*/
export function setupState( containers, fields ) {
return {
type: 'SETUP_STATE',
payload: {
containers,
fields
}
};
}
/**
* Returns an action object used to update the state.
*
* @param {Object[]} containers
* @param {Object} fields
* @return {Object}
*/
export function updateState( containers, fields ) {
return {
type: 'UPDATE_STATE',
payload: {
containers,
fields
}
};
}
/**
* Returns an action object used to update the field's value.
*
* @param {string} fieldId
* @param {mixed} value
* @param {string[]} fieldsToRemove It's used by the complex fields to remove the nested
* fields within a single action.
* @return {Object}
*/
export function updateFieldValue( fieldId, value, fieldsToRemove = [] ) {
return {
type: 'UPDATE_FIELD_VALUE',
payload: {
fieldId,
value,
fieldsToRemove
}
};
}
/**
* Returns an action object used to add the fields.
*
* @param {Object[]} fields
* @return {Object}
*/
export function addFields( fields ) {
return {
type: 'ADD_FIELDS',
payload: {
fields
}
};
}
/**
* Returns an action object used to clone the fields.
*
* @param {string[]} originFieldIds
* @param {string[]} cloneFieldIds
* @return {Object}
*/
export function cloneFields( originFieldIds, cloneFieldIds ) {
return {
type: 'CLONE_FIELDS',
payload: {
originFieldIds,
cloneFieldIds
}
};
}
/**
* Returns an action object used to remove the fields.
*
* @param {string[]} fieldIds
* @return {Object}
*/
export function removeFields( fieldIds ) {
return {
type: 'REMOVE_FIELDS',
payload: {
fieldIds
}
};
}
/**
* Returns an action object used to add a container to all containers.
*
* @param {Object} container
* @return {Object}
*/
export function addContainer( container ) {
return {
type: 'ADD_CONTAINER',
payload: container
};
}
/**
* Returns an action object used to remove a container from all containers.
*
* @param {Object} container
* @return {Object}
*/
export function removeContainer( container ) {
return {
type: 'REMOVE_CONTAINER',
payload: container
};
}
/**
* Returns an action object used to add the created sidebar to all fields.
*
* @param {Object} sidebar
* @return {Object}
*/
export function receiveSidebar( sidebar ) {
return {
type: 'RECEIVE_SIDEBAR',
payload: sidebar
};
}
/**
* Returns an action object used to signal that saving is locked.
*
* @param {string} lockName
* @return {Object}
*/
export function lockSaving( lockName ) {
return {
type: 'LOCK_SAVING',
payload: {
lockName
}
};
}
/**
* Returns an action object used to signal that saving is unlocked.
*
* @param {string} lockName
* @return {Object}
*/
export function unlockSaving( lockName ) {
return {
type: 'UNLOCK_SAVING',
payload: {
lockName
}
};
}

View file

@ -0,0 +1,28 @@
/**
* External dependencies.
*/
import { assign, endsWith } from 'lodash';
/**
* Internal dependencies.
*/
import flattenField from '../utils/flatten-field';
/**
* Transform the shape of the given state to be more Redux friendly.
*
* @param {Object} state
* @return {Object}
*/
export function normalizePreloadedState( state ) {
const fields = [];
const containers = state
.filter( ( { id } ) => ! endsWith( id, '__i__' ) )
.map( ( container ) => {
return assign( {}, container, {
fields: container.fields.map( ( field ) => flattenField( field, container.id, fields ) )
} );
} );
return { containers, fields };
}

View file

@ -0,0 +1,35 @@
/**
* External dependencies.
*/
import { registerStore, dispatch } from '@wordpress/data';
import {
get,
keyBy
} from 'lodash';
/**
* Internal dependencies.
*/
import reducer from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';
import { normalizePreloadedState } from './helpers';
/**
* Register the store.
*/
registerStore( 'carbon-fields/metaboxes', {
reducer,
actions,
selectors
} );
/**
* Hydrate the store's state.
*/
const { containers, fields } = normalizePreloadedState( get( window.cf, 'preloaded.containers', [] ) );
dispatch( 'carbon-fields/metaboxes' ).setupState(
keyBy( containers, 'id' ),
keyBy( fields, 'id' )
);

View file

@ -0,0 +1,247 @@
/**
* External dependencies.
*/
import produce from 'immer';
import { combineReducers } from '@wordpress/data';
import {
omit,
keyBy,
assign,
forEach,
cloneDeep,
values,
unset
} from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { uniqueId } from '@carbon-fields/core';
/**
* The reducer that keeps track of the containers.
*
* @param {Object} state
* @param {Object} action
* @return {Object}
*/
export function containers( state = {}, action ) {
switch ( action.type ) {
case 'SETUP_STATE':
return action.payload.containers;
case 'UPDATE_STATE':
return produce( state, ( draft ) => {
values( action.payload.containers ).forEach( ( container ) => {
draft[ container.id ] = container;
} );
} );
case 'ADD_CONTAINER':
return produce( state, ( draft ) => {
draft[ action.payload.id ] = action.payload;
} );
case 'REMOVE_CONTAINER':
return omit( state, action.payload );
default:
return state;
}
}
/**
* Clones a field.
*
* @param {string} originId
* @param {string} cloneId
* @param {Object} fields
* @param {Object[]} accumulator
* @return {Object[]}
*/
function cloneField( originId, cloneId, fields, accumulator ) {
const field = cloneDeep( fields[ originId ] );
field.id = cloneId;
if ( field.type === 'complex' ) {
field.value.forEach( ( group ) => {
group.id = uniqueId();
accumulator = group.fields.reduce( ( groupAccumulator, groupField ) => {
const originGroupFieldId = groupField.id;
const cloneGroupFieldId = uniqueId();
groupField.id = cloneGroupFieldId;
return cloneField( originGroupFieldId, cloneGroupFieldId, fields, groupAccumulator );
}, accumulator );
} );
}
return accumulator.concat( field );
}
/**
* Returns a list of field ids by a given root id.
*
* @param {string} fieldId
* @param {Object} fields
* @param {string[]} accumulator
* @return {string[]}
*/
function getFieldIdsByRootId( fieldId, fields, accumulator ) {
const field = fields[ fieldId ];
if ( field.type === 'complex' ) {
field.value.forEach( ( group ) => {
accumulator = group.fields.reduce( ( groupAccumulator, groupField ) => {
return getFieldIdsByRootId( groupField.id, fields, groupAccumulator );
}, accumulator );
} );
}
return accumulator.concat( fieldId );
}
/**
* The reducer that keeps track of the fields.
*
* @param {Object} state
* @param {Object} action
* @return {Object}
*/
export function fields( state = {}, action ) {
switch ( action.type ) {
case 'SETUP_STATE':
return action.payload.fields;
case 'UPDATE_STATE':
return produce( state, ( draft ) => {
values( action.payload.fields ).forEach( ( field ) => {
draft[ field.id ] = field;
} );
} );
case 'UPDATE_FIELD_VALUE':
return produce( state, ( draft ) => {
const {
fieldId,
value,
fieldsToRemove
} = action.payload;
draft[ fieldId ].value = value;
const fieldIdsToRemove = fieldsToRemove.reduce( ( accumulator, fieldIdToRemove ) => {
return getFieldIdsByRootId( fieldIdToRemove, state, accumulator );
}, [] );
fieldIdsToRemove.forEach( ( fieldIdToRemove ) => {
unset( draft, fieldIdToRemove );
} );
} );
case 'ADD_FIELDS':
return produce( state, ( draft ) => {
action.payload.fields.forEach( ( field ) => {
draft[ field.id ] = field;
} );
} );
case 'CLONE_FIELDS':
return produce( state, ( draft ) => {
const { originFieldIds, cloneFieldIds } = action.payload;
const clonedFields = originFieldIds.reduce( ( accumulator, originFieldId, index ) => {
return cloneField( originFieldId, cloneFieldIds[ index ], draft, accumulator );
}, [] );
assign( draft, keyBy( clonedFields, 'id' ) );
} );
case 'REMOVE_FIELDS':
const fieldIds = action.payload.fieldIds.reduce( ( accumulator, fieldId ) => {
return getFieldIdsByRootId( fieldId, state, accumulator );
}, [] );
return omit( state, fieldIds );
case 'RECEIVE_SIDEBAR':
return produce( state, ( draft ) => {
forEach( draft, ( field ) => {
if ( field.type === 'sidebar' ) {
field.options.unshift( action.payload );
}
} );
} );
default:
return state;
}
}
/**
* The reducer that keeps track of the save locks.
*
* @param {Object} state
* @param {Object} action
* @return {Object}
*/
export function savingLock( state = {}, action ) {
switch ( action.type ) {
case 'LOCK_SAVING':
return {
...state,
[ action.payload.lockName ]: true
};
case 'UNLOCK_SAVING':
return omit( state, [ action.payload.lockName ] );
default:
return state;
}
}
/**
* The reducer that keeps track if there is dirty fields.
*
* @param {boolean} state
* @param {Object} action
* @return {Object}
*/
export function isDirty( state = false, action ) {
switch ( action.type ) {
case 'UPDATE_FIELD_VALUE':
return true;
default:
return state;
}
}
/**
* The reducer that keeps track if an update is being made.
*
* @param {boolean} state
* @param {Object} action
* @return {Object}
*/
export function isFieldUpdated( state, action ) {
switch ( action.type ) {
case 'UPDATE_FIELD_VALUE':
return { action };
default:
return false;
}
}
export default combineReducers( {
containers,
fields,
savingLock,
isDirty,
isFieldUpdated
} );

View file

@ -0,0 +1,108 @@
/**
* External dependencies.
*/
import {
filter,
pick,
mapValues,
mapKeys
} from 'lodash';
/**
* Returns the containers.
*
* @param {Object} state
* @return {Object[]}
*/
export function getContainers( state ) {
return state.containers;
}
/**
* Returns a container by an id.
*
* @param {Object} state
* @param {string} containerId
* @return {?Object}
*/
export function getContainerById( state, containerId ) {
return state.containers[ containerId ];
}
/**
* Returns the fields.
*
* @param {Object} state
* @return {Object}
*/
export function getFields( state ) {
return state.fields;
}
/**
* Returns the fields that belong to the specified container.
*
* @param {Object} state
* @param {string} containerId
* @return {Object[]}
*/
export function getFieldsByContainerId( state, containerId ) {
return filter( state.fields, [ 'container_id', containerId ] );
}
/**
* Returns a field by an id.
*
* @param {Object} state
* @param {string} fieldId
* @return {?Object}
*/
export function getFieldById( state, fieldId ) {
return state.fields[ fieldId ];
}
/**
* Returns whether saving is locked.
*
* @param {Object} state
* @return {boolean}
*/
export function isSavingLocked( state ) {
return Object.keys( state.savingLock ).length > 0;
}
/**
* Returns whether the metaboxes fields contain unsaved changed.
*
* @param {Object} state
* @return {boolean}
*/
export function isDirty( state ) {
return state.isDirty;
}
/**
* Returns whether the metaboxes fields contain unsaved changed.
*
* @param {Object} state
* @return {boolean}
*/
export function isFieldUpdated( state ) {
return state.isFieldUpdated;
}
/**
* Returns a map of field values for a given group.
*
* @param {Object} state
* @param {string[]} fieldIds
* @return {Object}
*/
export function getComplexGroupValues( state, fieldIds ) {
let fields = pick( getFields( state ), fieldIds );
fields = mapKeys( fields, ( field ) => field.base_name.replace( /\-/g, '_' ) );
fields = mapValues( fields, ( field ) => field.value );
return fields;
}

View file

@ -0,0 +1,46 @@
/**
* External dependencies.
*/
import { pick, cloneDeep } from 'lodash';
/**
* Carbon Fields dependencies.
*/
import { uniqueId } from '@carbon-fields/core';
/**
* Flattens a field.
*
* @param {Object} field
* @param {string} containerId
* @param {Object[]} accumulator
* @return {Object}
*/
export default function flattenField( field, containerId, accumulator ) {
field = cloneDeep( field );
// Replace the id of the field.
field.id = uniqueId();
// Keep reference to the container.
field.container_id = containerId;
// The complex fields represent a nested structure of fields.
// So we need to flat them as well.
if ( field.type === 'complex' ) {
field.value.forEach( ( group ) => {
group.id = uniqueId();
group.container_id = containerId;
group.fields = group.fields.map( ( groupField ) => flattenField( groupField, containerId, accumulator ) );
} );
}
accumulator.push( field );
return pick( field, [
'id',
'type',
'name',
'base_name'
] );
}

View file

@ -0,0 +1,23 @@
/**
* External dependencies.
*/
import create from 'callbag-create';
/**
* Callbag source factory from `addHandler` and `removeHandler` pair.
*
* @see https://github.com/Andarist/callbag-from-event-pattern
* @param {Function} addHandler
* @param {Function} removeHandler
* @param {Function} argsTransformer
* @return {Function}
*/
export default function fromEventPattern( addHandler, removeHandler, argsTransformer = ( ...args ) => args ) {
return create( ( sink ) => {
const handler = ( ...args ) => sink( 1, argsTransformer( ...args ) );
addHandler( handler );
return () => removeHandler( handler );
} );
}

View file

@ -0,0 +1,13 @@
/**
* External dependencies.
*/
import { isUndefined } from 'lodash';
/**
* Returns true if Gutenberg is presented.
*
* @return {boolean}
*/
export default function isGutenberg() {
return ! isUndefined( window._wpLoadBlockEditor );
}

View file

@ -0,0 +1,15 @@
/**
* Removes the prefix used to compact the input of Carbon Fields.
*
* @param {string} str
* @return {string}
*/
export default function stripCompactInputPrefix( str ) {
const { compactInput, compactInputKey } = window.cf.config;
if ( ! compactInput || str.indexOf( compactInputKey ) !== 0 ) {
return str;
}
return str.replace( new RegExp( `^${ compactInputKey }\\[(.+?)\\]` ), '$1' );
}

View file

@ -0,0 +1,16 @@
/**
* Source: https://github.com/kvz/locutus/blob/master/src/php/url/urldecode.js
*
* @param {string} str
* @return {string}
*/
export default function urldecode( str ) {
return decodeURIComponent( ( str + '' )
.replace( /%(?![\da-f]{2})/gi, function() {
// PHP tolerates poorly formed escape sequences
return '%25';
} )
.replace( /\+/g, '%20' )
);
}