diff --git a/kirki-packages/control-repeater/src/control.js b/kirki-packages/control-repeater/src/control.js index 48334de82..9d75653a0 100644 --- a/kirki-packages/control-repeater/src/control.js +++ b/kirki-packages/control-repeater/src/control.js @@ -670,6 +670,8 @@ wp.customize.controlConstructor.repeater = wp.customize.Control.extend({ this.setting.set(encodeURI(JSON.stringify(filteredValue))); if (refresh) { + // Check acive_callback in every change + KirkiRepeaterDependencies.init(); // Trigger the change event on the hidden field so // previewer refresh the website on Customizer this.settingField.trigger("change"); diff --git a/kirki-packages/module-field-dependencies/src/Field_Dependencies.php b/kirki-packages/module-field-dependencies/src/Field_Dependencies.php index 7af72cc66..f67d767de 100644 --- a/kirki-packages/module-field-dependencies/src/Field_Dependencies.php +++ b/kirki-packages/module-field-dependencies/src/Field_Dependencies.php @@ -27,6 +27,16 @@ class Field_Dependencies { */ private $dependencies = []; + /** + * An array of all repeater controls available. + * Regardless if it has [active_callback] or not. + * + * @access private + * @since 4.1.1 + * @var array + */ + private $repeater_controls = []; + /** * Constructor. * @@ -40,6 +50,28 @@ public function __construct() { } + /** + * Collect all Repeater Controls in a new dependencies array + * other than [kirkiControlDependencies] + * to fix the issue that we can't find the repeater control + * in the array [kirkiControlDependencies] because + * it doesn't have [active_callback] array + * + * Now, We can use [active_callback] array in the repeater's childern + * + * @access private + * @since 4.1.1 + * @param array $args The field arguments. + * @return void + */ + private function field_add_repeater_controls( $args ) { + + if ('repeater' === $args['type'] || 'kirki-repeater' === $args['type'] ) { + $this->repeater_controls[ $args['settings'] ] = '__return_true'; + } + + } + /** * Filter control arguments. * @@ -50,6 +82,9 @@ public function __construct() { */ public function field_add_control_args( $args ) { + // Collect a list of all Repeater Controls available + $this->field_add_repeater_controls( $args ); + if ( isset( $args['active_callback'] ) ) { if ( is_array( $args['active_callback'] ) ) { if ( ! is_callable( $args['active_callback'] ) ) { @@ -104,6 +139,7 @@ public function field_dependencies() { wp_enqueue_script( 'kirki_field_dependencies', URL::get_from_path( dirname( __DIR__ ) . '/dist/control.js' ), [ 'jquery', 'customize-base', 'customize-controls' ], '4.0', true ); wp_localize_script( 'kirki_field_dependencies', 'kirkiControlDependencies', $this->dependencies ); + wp_localize_script( 'kirki_field_dependencies', 'kirkiRepeaterControlsAvailable', $this->repeater_controls ); } } diff --git a/kirki-packages/module-field-dependencies/src/control.js b/kirki-packages/module-field-dependencies/src/control.js index bfa22a090..dfabcf322 100644 --- a/kirki-packages/module-field-dependencies/src/control.js +++ b/kirki-packages/module-field-dependencies/src/control.js @@ -318,6 +318,315 @@ var kirkiDependencies = { }, }; + +/** + * Enable [active_callback] for repeater's controls + * This function MUST be here to use [kirkiControlDependencies] passed from [Field_Dependencies.php] + * + * @author: Kirki + * @since 4.1.1 + */ +var KirkiRepeaterDependencies = { + + repeatersControls: {}, + + repeatersActiveCallbackFields: {}, + listenTo: {}, + + init: function() { + var self = this; + + /* 1. Collect All Repeaters */ + _.each( window.kirkiRepeaterControlsAvailable, function (repDetails, repeaterID) { + + var control = wp.customize.control(repeaterID); + + if( control && control.params && control.params.type && control.params.type === 'repeater' ) { + + self.repeatersControls[ control.id ] = self.repeatersControls[ control.id ] || []; + + self.repeatersControls[ control.id ] = { + 'user_entries': JSON.parse( decodeURI( control.setting.get() ) ) /* @see function [getValue] in [wp.customize.controlConstructor.repeater] located in [controls/js/script.js] */ + }; + + } + + } ); + + /* 2. Collect Active Callbacks Arrays for each Available Repeater */ + _.each( self.repeatersControls, function( repDetails, repID ) { + + var repControl = wp.customize.control( repID ), + repUserEntries = (! _.isUndefined( repDetails ) && !_.isUndefined( repDetails['user_entries'] ) && ! _.isEmpty( repDetails['user_entries'] ) ) ? repDetails['user_entries'] : null; + + if( ! _.isUndefined( repControl ) && ! _.isNull( repUserEntries ) ) { + + _.each(repUserEntries, function(rowValue, rowIndex) { + + _.each(rowValue, function(eleValue, eleID) { + + if( !_.isUndefined( repControl.params.fields[ eleID ] ) ) { + + var eleDetails = repControl.params.fields[ eleID ]; + + self.showRepeaterControl( repControl.id, eleDetails, rowValue ); + + } + + }); + + }); + + self.repeatersActiveCallbackFields[ repControl.id ] = self.repeatersActiveCallbackFields[ repControl.id ] || []; + self.repeatersActiveCallbackFields[ repControl.id ] = self.listenTo; + /* Destroy it */ + self.listenTo = {}; + + } + + } ); + + + /* 3. Iterate inside every user entry and Apply [showRepeaterControl] on each slave element */ + _.each( self.repeatersActiveCallbackFields, function( required_fields, repID ) { + + objRepeaterControl = wp.customize.control( repID ); + + if( ! _.isUndefined( objRepeaterControl ) ) { + + var repUserEntries = JSON.parse( decodeURI( objRepeaterControl.setting.get() ) ), /* @see function [getValue] in [wp.customize.controlConstructor.repeater] located in [controls/js/script.js] */ + repFields = objRepeaterControl.params.fields; + + _.each(repUserEntries, function(rowValue, rowIndex) { + + _.each( required_fields, function( slaves, master ) { + + _.each( slaves, function( slave ) { + + var objSlave = repFields[ slave ], + setActiveState, + isDisplayed; + + isDisplayed = function() { + return self.showRepeaterControl( repID, objSlave, rowValue ); + }; + + setActiveState = function() { + if( isDisplayed() ) { + jQuery(objRepeaterControl.selector).find( '[data-row="' + rowIndex + '"] .repeater-field-' + slave ).removeClass('inactive').addClass('active').slideDown('fast'); + } + else { + jQuery(objRepeaterControl.selector).find( '[data-row="' + rowIndex + '"] .repeater-field-' + slave ).removeClass('active').addClass('inactive').slideUp('fast'); + } + }; + + setActiveState(); + + + } ); + + }); + + }); + + } + + } ); + + }, + + + /** + * Should we show the control? + * + * @since 4.0.22 + * + * @param {string} repeaterID - The repeater ID + * @param {string|object} control - The control-id or the control object. + * @param {object} rowEntries - The user entry for a repeater block + * @returns {bool} + */ + showRepeaterControl: function( repeaterID, control, rowEntries ) { + + var self = this, + show = true, + + isOption = ( + ! _.isUndefined( control ) && /* Fix: Multiple Repeaters with no active_callback */ + ! _.isUndefined( control.id ) && /* Fix: Multiple Repeaters with no active_callback */ + control.id && // Check if id exists. + control.type && // Check if tpe exists. + ! _.isEmpty( control.type ) // Check if control's type is not empty. + ), + i; + + // Exit early if control not found or if "required" argument is not defined. + if ( 'undefined' === typeof control || 'undefined' === typeof control.active_callback || ( control.active_callback && _.isEmpty( control.active_callback ) ) ) { + return true; + } + + // Loop control requirements. + for ( i = 0; i < control.active_callback.length; i++ ) { + if ( ! self.checkCondition( repeaterID, control.active_callback[ i ], control, rowEntries, isOption, 'AND' ) ) { + show = false; + } + } + return show; + }, + + /** + * Check a condition. + * + * @param {string} repeaterID - The repeater ID + * @param {Object} requirement - The requirement, inherited from showRepeaterControl - Represents the Active Callack Array. + * @param {Object} control - The repeater's control object. + * @param {object} rowEntries - The user entry for a repeater block + * @param {bool} isOption - Whether it's an option or not. + * @param {string} relation - Can be one of 'AND' or 'OR'. + */ + checkCondition: function( repeaterID, requirement, control, rowEntries, isOption, relation ) { + var self = this, + childRelation = ( 'AND' === relation ) ? 'OR' : 'AND', + nestedItems, + requirementSettingValue, + i; + + + + // If an array of other requirements nested, we need to process them separately. + if ( 'undefined' !== typeof requirement[0] && 'undefined' === typeof requirement.setting ) { + + nestedItems = []; + + // Loop sub-requirements. + for ( i = 0; i < requirement.length; i++ ) { + nestedItems.push( self.checkCondition( repeaterID, requirement[ i ], control, rowEntries, isOption, childRelation ) ); + } + + + // OR relation. Check that true is part of the array. + if ( 'OR' === childRelation ) { + return ( -1 !== nestedItems.indexOf( true ) ); + } + + // AND relation. Check that false is not part of the array. + return ( -1 === nestedItems.indexOf( false ) ); + } + + + // Early exit if setting is not defined. + if ( ! requirement.setting in rowEntries ) { + return true; + } + + /* Requirement Setting User Value */ + requirementSettingValue = rowEntries[ requirement.setting ]; + + // console.log( requirementSettingValue ); + + /** + * Output: listenTo + * + * Master_#1 => array( + * 0: Slave #1, + * 1: Slave #2 + * ) + * + */ + self.listenTo[ requirement.setting ] = self.listenTo[ requirement.setting ] || []; + + if ( -1 === self.listenTo[ requirement.setting ].indexOf( control.id ) ) { + + self.listenTo[ requirement.setting ].push( control.id ); + + } + + return self.evaluate( + requirement.value, + requirementSettingValue, + requirement.operator + ); + + }, + + /** + * Figure out if the 2 values have the relation we want. + * + * @since 4.0.22 + * @param {mixed} value1 - The 1st value. + * @param {mixed} value2 - The 2nd value. + * @param {string} operator - The comparison to use. + * @returns {bool} + */ + evaluate: function( value1, value2, operator ) { + var found = false; + + if ( '===' === operator ) { + return value1 === value2; + } + if ( '==' === operator || '=' === operator || 'equals' === operator || 'equal' === operator ) { + return value1 == value2; + } + if ( '!==' === operator ) { + return value1 !== value2; + } + if ( '!=' === operator || 'not equal' === operator ) { + return value1 != value2; + } + if ( '>=' === operator || 'greater or equal' === operator || 'equal or greater' === operator ) { + return value2 >= value1; + } + if ( '<=' === operator || 'smaller or equal' === operator || 'equal or smaller' === operator ) { + return value2 <= value1; + } + if ( '>' === operator || 'greater' === operator ) { + return value2 > value1; + } + if ( '<' === operator || 'smaller' === operator ) { + return value2 < value1; + } + if ( 'contains' === operator || 'in' === operator ) { + if ( _.isArray( value1 ) && _.isArray( value2 ) ) { + _.each( value2, function( value ) { + if ( value1.includes( value ) ) { + found = true; + return false; + } + } ); + return found; + } + if ( _.isArray( value2 ) ) { + _.each( value2, function( value ) { + if ( value == value1 ) { // jshint ignore:line + found = true; + } + } ); + return found; + } + if ( _.isObject( value2 ) ) { + if ( ! _.isUndefined( value2[ value1 ] ) ) { + found = true; + } + _.each( value2, function( subValue ) { + if ( value1 === subValue ) { + found = true; + } + } ); + return found; + } + if ( _.isString( value2 ) ) { + if ( _.isString( value1 ) ) { + return ( -1 < value1.indexOf( value2 ) && -1 < value2.indexOf( value1 ) ); + } + return -1 < value1.indexOf( value2 ); + } + } + return value1 == value2; + } + +}; + jQuery(document).ready(function () { kirkiDependencies.init(); + KirkiRepeaterDependencies.init(); });