From ecbb1d1d1145403eb813f6c3ab77a9225758002f Mon Sep 17 00:00:00 2001 From: Hady Shaltout Date: Wed, 8 Feb 2023 18:43:11 +0200 Subject: [PATCH 1/3] Enable Using [active_callback] Inside Repeater Controls Version 4.1.1 Here's the solution of using [active_callback] array inside repeater controls to allow you show/hide other controls inside repeater's rows. We've added the code in the unminified JS files until Kirki team decide to accept it and merge it to be able to minify via their minification tool. Because Kirki is using the minified js files you must minify the new code and (Only Append) to the [dist/control.js] files, DO NOT Replace the old ones. File-1: .\kirki\kirki-packages\module-field-dependencies\src\control.js Minify the new code before [jQuery(document).ready] and add it in .\kirki\kirki-packages\module-field-dependencies\dist\control.js Then append the line: jQuery(document).ready((function(){KirkiRepeaterDependencies.init()})); File-2: .\kirki\kirki-packages\control-repeater\src\control.js We need to add the line KirkiRepeaterDependencies.init(); in the minified version in [dist] directory: .\kirki\kirki-packages\control-repeater\dist\control.js Replace the old line this.settingField.trigger("change") With (KirkiRepeaterDependencies.init(),this.settingField.trigger("change")) --- .../control-repeater/src/control.js | 2 + .../module-field-dependencies/src/control.js | 309 ++++++++++++++++++ 2 files changed, 311 insertions(+) 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/control.js b/kirki-packages/module-field-dependencies/src/control.js index bfa22a090..0b77d3ff4 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.kirkiControlDependencies, function (requirements, dependantID) { + + var control = wp.customize.control(dependantID); + + 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(); }); From 8744846e3624a990648bd76bda916e0a9d480e2c Mon Sep 17 00:00:00 2001 From: Hady Shaltout Date: Wed, 8 Feb 2023 22:30:55 +0200 Subject: [PATCH 2/3] Fix Repeaters' items active_callback Collect all repeater controls regardless it has [active_callback] or not to fix the issue appears while the parent repeater doesn't have an [active_callback] array --- .../src/Field_Dependencies.php | 39 +++++++++++++++++++ .../module-field-dependencies/src/control.js | 4 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/kirki-packages/module-field-dependencies/src/Field_Dependencies.php b/kirki-packages/module-field-dependencies/src/Field_Dependencies.php index dbd5c596c..299f36e3c 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,30 @@ 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 +84,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'] ) ) { @@ -86,6 +123,7 @@ public function field_add_control_args( $args ) { if ( ! empty( $args['required'] ) ) { $this->dependencies[ $args['settings'] ] = $args['required']; } + } return $args; @@ -104,6 +142,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 0b77d3ff4..dfabcf322 100644 --- a/kirki-packages/module-field-dependencies/src/control.js +++ b/kirki-packages/module-field-dependencies/src/control.js @@ -337,9 +337,9 @@ var KirkiRepeaterDependencies = { var self = this; /* 1. Collect All Repeaters */ - _.each( window.kirkiControlDependencies, function (requirements, dependantID) { + _.each( window.kirkiRepeaterControlsAvailable, function (repDetails, repeaterID) { - var control = wp.customize.control(dependantID); + var control = wp.customize.control(repeaterID); if( control && control.params && control.params.type && control.params.type === 'repeater' ) { From bd087f607cd7c38338e52399ea8deabb1882a847 Mon Sep 17 00:00:00 2001 From: Hady Shaltout Date: Wed, 8 Feb 2023 22:34:17 +0200 Subject: [PATCH 3/3] Clean Some Lines --- .../module-field-dependencies/src/Field_Dependencies.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/kirki-packages/module-field-dependencies/src/Field_Dependencies.php b/kirki-packages/module-field-dependencies/src/Field_Dependencies.php index 299f36e3c..69289d3be 100644 --- a/kirki-packages/module-field-dependencies/src/Field_Dependencies.php +++ b/kirki-packages/module-field-dependencies/src/Field_Dependencies.php @@ -67,9 +67,7 @@ public function __construct() { private function field_add_repeater_controls( $args ) { if ('repeater' === $args['type'] || 'kirki-repeater' === $args['type'] ) { - $this->repeater_controls[ $args['settings'] ] = '__return_true'; - } } @@ -123,7 +121,6 @@ public function field_add_control_args( $args ) { if ( ! empty( $args['required'] ) ) { $this->dependencies[ $args['settings'] ] = $args['required']; } - } return $args;