Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak: Enable [active_callback] For Repeater's Children #2498

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kirki-packages/control-repeater/src/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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'] ) ) {
Expand Down Expand Up @@ -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 );

}
}
309 changes: 309 additions & 0 deletions kirki-packages/module-field-dependencies/src/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});