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