Update to Kirki 4.0.22

This commit is contained in:
AlxMedia 2022-03-15 14:29:17 +01:00
parent 2b6ac38550
commit 78edeb1b25
492 changed files with 29668 additions and 39884 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 kirki-framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,98 @@
# control-repeater
## Installation
First, install the package using composer:
```bash
composer require kirki-framework/control-repeater
```
Make sure you include the autoloader:
```php
require_once get_parent_theme_file_path( 'vendor/autoload.php' );
```
To add a control using the customizer API:
```php
/**
* Registers the control and whitelists it for JS templating.
*
* @since 1.0
* @param WP_Customize_Manager $wp_customize The WP_Customize_Manager object.
* @return void
*/
add_action( 'customize_register', function( $wp_customize ) {
$wp_customize->register_control_type( '\Kirki\Control\Repeater' );
} );
/**
* Add Customizer settings & controls.
*
* @since 1.0
* @param WP_Customize_Manager $wp_customize The WP_Customize_Manager object.
* @return void
*/
add_action( 'customize_register', function( $wp_customize ) {
// Add settings.
$wp_customize->add_setting( new \Kirki\Settings\Repeater( $wp_customize, 'my_repeater_setting', [
'default' => [
[
'link_text' => esc_html__( 'Kirki Site', 'theme_textdomain' ),
'link_url' => 'https://aristath.github.io/kirki/',
'link_target' => '_self',
],
[
'link_text' => esc_html__( 'Kirki Repository', 'theme_textdomain' ),
'link_url' => 'https://github.com/aristath/kirki',
'link_target' => '_self',
],
],
'type' => 'theme_mod',
'capability' => 'edit_theme_options',
'transport' => 'refresh',
'sanitize_callback' => function( $value ) { // Custom sanitization callback.
$value = ( is_array( $value ) ) ? $value : json_decode( urldecode( $value ), true );
$value = ( empty( $value ) || ! is_array( $value ) ) ? [] : $value;
foreach ( $value as $row_index => $row_data ) {
$value[ $row_index ]['link_text'] = isset( $row_data['link_text'] ) ? sanitize_text_field( $row_data['link_text'] ) : '';
$value[ $row_index ]['link_url'] = isset( $row_data['link_url'] ) ? esc_url( $row_data['link_url'] ) : '';
$value[ $row_index ]['link_target'] = isset( $row_data['link_target'] ) && in_array( $row_data['link_target'], [ '_self', '_blank', '_parent', '_top' ], true ) ? $row_data['link_target'] : '_self';
}
},
] ) );
// Add controls.
$wp_customize->add_control( new \Kirki\Control\Repeater( $wp_customize, 'my_repeater_setting', [
'label' => esc_html__( 'My Control', 'theme_textdomain' ),
'section' => 'colors',
'fields' => [
'link_text' => [
'type' => 'text',
'label' => esc_html__( 'Link Text', 'theme_textdomain' ),
'description' => esc_html__( 'This will be the label for your link', 'theme_textdomain' ),
'default' => '',
],
'link_url' => [
'type' => 'text',
'label' => esc_html__( 'Link URL', 'theme_textdomain' ),
'description' => esc_html__( 'This will be the link URL', 'theme_textdomain' ),
'default' => '',
],
'link_target' => [
'type' => 'radio',
'label' => esc_html__( 'Link Target', 'theme_textdomain' ),
'description' => esc_html__( 'This will be the link target', 'theme_textdomain' ),
'default' => '_self',
'choices' => [
'_blank' => esc_html__( 'New Window', 'theme_textdomain' ),
'_self' => esc_html__( 'Same Frame', 'theme_textdomain' ),
],
],
],
] ) );
} );
```

View file

@ -0,0 +1,2 @@
.customize-control-repeater{position:relative}.customize-control-repeater .repeater-fields .repeater-row{background:#eee;border:1px solid #e5e5e5;margin-top:.5rem;position:relative}.customize-control-repeater .repeater-fields .repeater-row.minimized{border:1px solid #dfdfdf;padding:0}.customize-control-repeater .repeater-fields .repeater-row.minimized:hover{border:1px solid #e5e5e5}.customize-control-repeater .repeater-fields .repeater-row.minimized .repeater-row-content{display:none}.customize-control-repeater .repeater-fields .repeater-row label{clear:both;margin-bottom:12px}.customize-control-repeater .repeater-fields .repeater-row .repeater-field.repeater-field-,.customize-control-repeater .repeater-fields .repeater-row .repeater-field.repeater-field-radio-image input{display:none}.customize-control-repeater .repeater-fields .repeater-row .repeater-field.repeater-field-radio-image input img{border:1px solid transparent}.customize-control-repeater .repeater-fields .repeater-row .repeater-field.repeater-field-radio-image input:checked+label img{border:1px solid #3498db;-webkit-box-shadow:0 0 5px 2px rgba(0,0,0,.25);box-shadow:0 0 5px 2px rgba(0,0,0,.25)}.customize-control-repeater .repeater-fields .repeater-row .repeater-field:last-child{border-bottom:none;padding-bottom:0}.customize-control-repeater button.repeater-add{margin-top:1rem}.customize-control-repeater .repeater-row-content{background:#fff;padding:10px 15px}.customize-control-repeater .repeater-field{border-bottom:1px dotted #ccc;clear:both;margin-bottom:12px;padding-bottom:12px;width:100%}.customize-control-repeater .repeater-field .customize-control-description,.customize-control-repeater .repeater-field .customize-control-title{font-size:13px;line-height:normal}.customize-control-repeater .repeater-field.repeater-field-hidden{border:0;margin:0;padding:0}.customize-control-repeater .repeater-field-select select{margin-left:0}.customize-control-repeater .repeater-field-checkbox label{line-height:28px}.customize-control-repeater .repeater-field-checkbox input{line-height:28px;margin-right:5px}.customize-control-repeater .repeater-field-textarea textarea{resize:vertical;width:100%}.customize-control-repeater .repeater-row-header{word-wrap:break-word;background:#fff;border-bottom:1px solid #dfdfdf;height:auto;line-height:30px;min-height:20px;overflow:hidden;padding:10px 15px;position:relative}.customize-control-repeater .repeater-row-header:hover{cursor:move}.customize-control-repeater .repeater-row-header .dashicons{color:#a0a5aa;font-size:18px;position:absolute;right:12px;top:2px}.customize-control-repeater .repeater-row-label{display:block;font-size:13px;font-weight:600;height:18px;line-height:20px;overflow:hidden;width:90%}.customize-control-repeater .repeater-row-remove{color:#a00}.customize-control-repeater .repeater-row-remove:hover{color:red}.customize-control-repeater .repeater-minimize{line-height:36px}.customize-control-repeater .remove-button,.customize-control-repeater .upload-button{width:48%}.kirki-image-attachment{margin:0 0 10px;text-align:center}.kirki-image-attachment img{display:inline-block}.kirki-file-attachment{margin:0 0 10px;text-align:center}.kirki-file-attachment .file{background:#f9f9f9;border:1px dotted #c3c3c3;display:block;padding:10px 5px}.limit{border-radius:3px;padding:3px}.limit.highlight{background:#d32f2f;color:#fff}
/*# sourceMappingURL=control.css.map */

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,534 @@
<?php
/**
* Customizer Control: repeater.
*
* @package kirki-framework/control-repeater
* @copyright Copyright (c) 2019, Ari Stathopoulos (@aristath)
* @license https://opensource.org/licenses/MIT
* @since 1.0
*/
namespace Kirki\Control;
use Kirki\Control\Base;
use Kirki\URL;
/**
* Repeater control
*
* @since 1.0
*/
class Repeater extends Base {
/**
* The control type.
*
* @access public
* @since 1.0
* @var string
*/
public $type = 'repeater';
/**
* The fields that each container row will contain.
*
* @access public
* @since 1.0
* @var array
*/
public $fields = [];
/**
* Will store a filtered version of value for advenced fields (like images).
*
* @access protected
* @since 1.0
* @var array
*/
protected $filtered_value = [];
/**
* The row label
*
* @access public
* @since 1.0
* @var array
*/
public $row_label = [];
/**
* The button label
*
* @access public
* @since 1.0
* @var string
*/
public $button_label = '';
/**
* The version. Used in scripts & styles for cache-busting.
*
* @static
* @access public
* @since 1.0
* @var string
*/
public static $control_ver = '1.0';
/**
* Constructor.
* Supplied `$args` override class property defaults.
* If `$args['settings']` is not defined, use the $id as the setting ID.
*
* @access public
* @since 1.0
* @param WP_Customize_Manager $manager Customizer bootstrap instance.
* @param string $id Control ID.
* @param array $args {@see WP_Customize_Control::__construct}.
*/
public function __construct( $manager, $id, $args = [] ) {
parent::__construct( $manager, $id, $args );
// Set up defaults for row labels.
$this->row_label = [
'type' => 'text',
'value' => esc_attr__( 'row', 'kirki' ),
'field' => false,
];
// Validate row-labels.
$this->row_label( $args );
if ( empty( $this->button_label ) ) {
/* translators: %s represents the label of the row. */
$this->button_label = sprintf( esc_html__( 'Add new %s', 'kirki' ), $this->row_label['value'] );
}
if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
$args['fields'] = [];
}
// An array to store keys of fields that need to be filtered.
$media_fields_to_filter = [];
foreach ( $args['fields'] as $key => $value ) {
if ( ! isset( $value['default'] ) ) {
$args['fields'][ $key ]['default'] = '';
}
if ( ! isset( $value['label'] ) ) {
$args['fields'][ $key ]['label'] = '';
}
$args['fields'][ $key ]['id'] = $key;
// We check if the filed is an uploaded media ( image , file, video, etc.. ).
if ( isset( $value['type'] ) ) {
switch ( $value['type'] ) {
case 'image':
case 'cropped_image':
case 'upload':
// We add it to the list of fields that need some extra filtering/processing.
$media_fields_to_filter[ $key ] = true;
break;
case 'dropdown-pages':
// If the field is a dropdown-pages field then add it to args.
$dropdown = wp_dropdown_pages(
[
'name' => '',
'echo' => 0,
'show_option_none' => esc_html__( 'Select a Page', 'kirki' ),
'option_none_value' => '0',
'selected' => '',
]
);
// Hackily add in the data link parameter.
$dropdown = str_replace( '<select', '<select data-field="' . esc_attr( $args['fields'][ $key ]['id'] ) . '"' . $this->get_link(), $dropdown ); // phpcs:ignore Generic.Formatting.MultipleStatementAlignment
$args['fields'][ $key ]['dropdown'] = $dropdown;
break;
}
}
}
$this->fields = $args['fields'];
// Now we are going to filter the fields.
// First we create a copy of the value that would be used otherwise.
$this->filtered_value = $this->value();
if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
// We iterate over the list of fields.
foreach ( $this->filtered_value as &$filtered_value_field ) {
if ( is_array( $filtered_value_field ) && ! empty( $filtered_value_field ) ) {
// We iterate over the list of properties for this field.
foreach ( $filtered_value_field as $key => &$value ) {
// We check if this field was marked as requiring extra filtering (in this case image, cropped_images, upload).
if ( array_key_exists( $key, $media_fields_to_filter ) ) {
// What follows was made this way to preserve backward compatibility.
// The repeater control use to store the URL for images instead of the attachment ID.
// We check if the value look like an ID (otherwise it's probably a URL so don't filter it).
if ( is_numeric( $value ) ) {
// "sanitize" the value.
$attachment_id = (int) $value;
// Try to get the attachment_url.
$url = wp_get_attachment_url( $attachment_id );
$filename = basename( get_attached_file( $attachment_id ) );
// If we got a URL.
if ( $url ) {
// 'id' is needed for form hidden value, URL is needed to display the image.
$value = [
'id' => $attachment_id,
'url' => $url,
'filename' => $filename,
];
}
}
}
}
}
}
}
}
/**
* Enqueue control related scripts/styles.
*
* @access public
* @since 1.0
* @return void
*/
public function enqueue() {
parent::enqueue();
// Enqueue the script.
wp_enqueue_script( 'kirki-control-repeater', URL::get_from_path( dirname( dirname( __DIR__ ) ) . '/dist/control.js' ), [ 'jquery', 'customize-base', 'wp-color-picker' ], self::$control_ver, false );
// Enqueue the style.
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'kirki-control-repeater-style', URL::get_from_path( dirname( dirname( __DIR__ ) ) . '/dist/control.css' ), [], self::$control_ver );
}
/**
* Refresh the parameters passed to the JavaScript via JSON.
*
* @access public
* @since 1.0
* @return void
*/
public function to_json() {
parent::to_json();
$fields = $this->fields;
$this->json['fields'] = $fields;
$this->json['row_label'] = $this->row_label;
// If filtered_value has been set and is not empty we use it instead of the actual value.
if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
$this->json['value'] = $this->filtered_value;
}
$this->json['value'] = apply_filters( "kirki_controls_repeater_value_{$this->id}", $this->json['value'] );
}
/**
* Render the control's content.
* Allows the content to be overriden without having to rewrite the wrapper in $this->render().
*
* @access protected
* @since 1.0
* @return void
*/
protected function render_content() {
?>
<label>
<?php if ( ! empty( $this->label ) ) : ?>
<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
<?php endif; ?>
<?php if ( ! empty( $this->description ) ) : ?>
<span class="description customize-control-description"><?php echo wp_kses_post( $this->description ); ?></span>
<?php endif; ?>
<input type="hidden" {{{ data.inputAttrs }}} value="" <?php echo wp_kses_post( $this->get_link() ); ?> />
</label>
<ul class="repeater-fields"></ul>
<?php if ( isset( $this->choices['limit'] ) ) : ?>
<?php /* translators: %s represents the number of rows we're limiting the repeater to allow. */ ?>
<p class="limit"><?php printf( esc_html__( 'Limit: %s rows', 'kirki' ), esc_html( $this->choices['limit'] ) ); ?></p>
<?php endif; ?>
<button class="button-secondary repeater-add"><?php echo esc_html( $this->button_label ); ?></button>
<?php
$this->repeater_js_template();
}
/**
* An Underscore (JS) template for this control's content (but not its container).
* Class variables for this control class are available in the `data` JS object.
*
* @access public
* @since 1.0
* @return void
*/
public function repeater_js_template() {
?>
<script type="text/html" class="customize-control-repeater-content">
<# var field; var index = data.index; #>
<li class="repeater-row minimized" data-row="{{{ index }}}">
<div class="repeater-row-header">
<span class="repeater-row-label"></span>
<i class="dashicons dashicons-arrow-down repeater-minimize"></i>
</div>
<div class="repeater-row-content">
<# _.each( data, function( field, i ) { #>
<div class="repeater-field repeater-field-{{{ field.type }}} repeater-field-{{ field.id }}">
<# if ( 'text' === field.type || 'url' === field.type || 'link' === field.type || 'email' === field.type || 'tel' === field.type || 'date' === field.type || 'number' === field.type ) { #>
<# var fieldExtras = ''; #>
<# if ( 'link' === field.type ) { #>
<# field.type = 'url' #>
<# } #>
<# if ( 'number' === field.type ) { #>
<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.min ) ) { #>
<# fieldExtras += ' min="' + field.choices.min + '"'; #>
<# } #>
<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.max ) ) { #>
<# fieldExtras += ' max="' + field.choices.max + '"'; #>
<# } #>
<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.step ) ) { #>
<# fieldExtras += ' step="' + field.choices.step + '"'; #>
<# } #>
<# } #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<input type="{{field.type}}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ fieldExtras }}>
</label>
<# } else if ( 'number' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<input type="{{ field.type }}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ numberFieldExtras }}>
</label>
<# } else if ( 'hidden' === field.type ) { #>
<input type="hidden" data-field="{{{ field.id }}}" <# if ( field.default ) { #> value="{{{ field.default }}}" <# } #> />
<# } else if ( 'checkbox' === field.type ) { #>
<label>
<input type="checkbox" value="{{{ field.default }}}" data-field="{{{ field.id }}}" <# if ( field.default ) { #> checked="checked" <# } #> /> {{{ field.label }}}
<# if ( field.description ) { #>{{{ field.description }}}<# } #>
</label>
<# } else if ( 'select' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<select data-field="{{{ field.id }}}"<# if ( ! _.isUndefined( field.multiple ) && false !== field.multiple ) { #> multiple="multiple" data-multiple="{{ field.multiple }}"<# } #>>
<# _.each( field.choices, function( choice, i ) { #>
<option value="{{{ i }}}" <# if ( -1 !== jQuery.inArray( i, field.default ) || field.default == i ) { #> selected="selected" <# } #>>{{ choice }}</option>
<# }); #>
</select>
</label>
<# } else if ( 'dropdown-pages' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ data.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<div class="customize-control-content repeater-dropdown-pages">{{{ field.dropdown }}}</div>
</label>
<# } else if ( 'radio' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<# _.each( field.choices, function( choice, i ) { #>
<label><input type="radio" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>> {{ choice }} <br/></label>
<# }); #>
</label>
<# } else if ( 'radio-image' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<# _.each( field.choices, function( choice, i ) { #>
<input type="radio" id="{{{ field.id }}}_{{ index }}_{{{ i }}}" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>>
<label for="{{{ field.id }}}_{{ index }}_{{{ i }}}"><img src="{{ choice }}"></label>
</input>
<# }); #>
</label>
<# } else if ( 'color' === field.type ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
</label>
<# var defaultValue = '';
if ( field.default ) {
if ( -1 === field.default.indexOf( 'rgba' ) ) {
defaultValue = ( '#' !== field.default.substring( 0, 1 ) ) ? '#' + field.default : field.default;
defaultValue = ' data-default-color=' + defaultValue; // Quotes added automatically.
} else {
defaultValue = ' data-default-color="' + defaultValue + '" data-alpha="true"';
}
} #>
<input class="color-picker-hex" type="text" maxlength="7" value="{{{ field.default }}}" data-field="{{{ field.id }}}" {{ defaultValue }} />
<# } else if ( 'textarea' === field.type ) { #>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<textarea rows="5" data-field="{{{ field.id }}}">{{ field.default }}</textarea>
<# } else if ( field.type === 'image' || field.type === 'cropped_image' ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
</label>
<figure class="kirki-image-attachment" data-placeholder="<?php esc_attr_e( 'No Image Selected', 'kirki' ); ?>" >
<# if ( field.default ) { #>
<# var defaultImageURL = ( field.default.url ) ? field.default.url : field.default; #>
<img src="{{{ defaultImageURL }}}">
<# } else { #>
<?php esc_html_e( 'No Image Selected', 'kirki' ); ?>
<# } #>
</figure>
<div class="actions">
<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
<button type="button" class="button upload-button" data-label=" <?php esc_attr_e( 'Add Image', 'kirki' ); ?>" data-alt-label="<?php echo esc_attr_e( 'Change Image', 'kirki' ); ?>" >
<# if ( field.default ) { #>
<?php esc_html_e( 'Change Image', 'kirki' ); ?>
<# } else { #>
<?php esc_html_e( 'Add Image', 'kirki' ); ?>
<# } #>
</button>
<# if ( field.default.id ) { #>
<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
<# } else { #>
<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
<# } #>
</div>
<# } else if ( field.type === 'upload' ) { #>
<label>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
</label>
<figure class="kirki-file-attachment" data-placeholder="<?php esc_attr_e( 'No File Selected', 'kirki' ); ?>" >
<# if ( field.default ) { #>
<# var defaultFilename = ( field.default.filename ) ? field.default.filename : field.default; #>
<span class="file"><span class="dashicons dashicons-media-default"></span> {{ defaultFilename }}</span>
<# } else { #>
<?php esc_html_e( 'No File Selected', 'kirki' ); ?>
<# } #>
</figure>
<div class="actions">
<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
<button type="button" class="button upload-button" data-label="<?php esc_attr_e( 'Add File', 'kirki' ); ?>" data-alt-label="<?php esc_attr_e( 'Change File', 'kirki' ); ?>">
<# if ( field.default ) { #>
<?php esc_html_e( 'Change File', 'kirki' ); ?>
<# } else { #>
<?php esc_html_e( 'Add File', 'kirki' ); ?>
<# } #>
</button>
<# if ( field.default.id ) { #>
<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
<# } else { #>
<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
<# } #>
</div>
<# } else if ( 'custom' === field.type ) { #>
<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
<div data-field="{{{ field.id }}}">{{{ field.default }}}</div>
<# } #>
</div>
<# }); #>
<button type="button" class="button-link repeater-row-remove"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
</div>
</li>
</script>
<?php
}
/**
* Validate row-labels.
*
* @access protected
* @since 1.0
* @param array $args {@see WP_Customize_Control::__construct}.
* @return void
*/
protected function row_label( $args ) {
// Validating args for row labels.
if ( isset( $args['row_label'] ) && is_array( $args['row_label'] ) && ! empty( $args['row_label'] ) ) {
// Validating row label type.
if ( isset( $args['row_label']['type'] ) && ( 'text' === $args['row_label']['type'] || 'field' === $args['row_label']['type'] ) ) {
$this->row_label['type'] = $args['row_label']['type'];
}
// Validating row label type.
if ( isset( $args['row_label']['value'] ) && ! empty( $args['row_label']['value'] ) ) {
$this->row_label['value'] = esc_html( $args['row_label']['value'] );
}
// Validating row label field.
if ( isset( $args['row_label']['field'] ) && ! empty( $args['row_label']['field'] ) && isset( $args['fields'][ sanitize_key( $args['row_label']['field'] ) ] ) ) {
$this->row_label['field'] = esc_html( $args['row_label']['field'] );
} else {
// If from field is not set correctly, making sure standard is set as the type.
$this->row_label['type'] = 'text';
}
}
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* Override field methods
*
* @package kirki-framework/control-repeater
* @copyright Copyright (c) 2019, Ari Stathopoulos (@aristath)
* @license https://opensource.org/licenses/MIT
* @since 1.0
*/
namespace Kirki\Field;
use Kirki\Compatibility\Field;
use Kirki\Field\Upload;
/**
* Field overrides.
*
* @since 1.0
*/
class Repeater extends Field {
/**
* The field type.
*
* @access public
* @since 1.0
* @var string
*/
public $type = 'kirki-repeater';
/**
* Used only on repeaters.
* Contains an array of the fields.
*
* @access protected
* @since 1.0
* @var array
*/
protected $fields = [];
/**
* Sets the control type.
*
* @access protected
* @since 1.0
* @return void
*/
protected function set_type() {
$this->type = 'repeater';
}
/**
* Sets the $transport
*
* @access protected
* @since 1.0
* @return void
*/
protected function set_transport() {
// Force using refresh mode.
// Currently the repeater control does not support postMessage.
$this->transport = 'refresh';
}
/**
* Sets the $sanitize_callback
*
* @access protected
* @since 1.0
* @return void
*/
protected function set_sanitize_callback() {
if ( empty( $this->sanitize_callback ) ) {
$this->sanitize_callback = [ $this, 'sanitize' ];
}
}
/**
* The sanitize method that will be used as a falback
*
* @access public
* @since 1.0
* @param string|array $value The control's value.
*/
public function sanitize( $value ) {
// is the value formatted as a string?
if ( is_string( $value ) ) {
$value = rawurldecode( $value );
$value = json_decode( $value, true );
}
// Nothing to sanitize if we don't have fields.
if ( empty( $this->fields ) ) {
return $value;
}
foreach ( $value as $row_id => $row_value ) {
// Make sure the row is formatted as an array.
if ( ! is_array( $row_value ) ) {
$value[ $row_id ] = [];
continue;
}
// Start parsing sub-fields in rows.
foreach ( $row_value as $subfield_id => $subfield_value ) {
// Make sure this is a valid subfield.
// If it's not, then unset it.
if ( ! isset( $this->fields[ $subfield_id ] ) ) {
unset( $value[ $row_id ][ $subfield_id ] );
}
// Get the subfield-type.
if ( ! isset( $this->fields[ $subfield_id ]['type'] ) ) {
continue;
}
$subfield = $this->fields[ $subfield_id ];
$subfield_type = $subfield['type'];
// Allow using a sanitize-callback on a per-field basis.
if ( isset( $this->fields[ $subfield_id ]['sanitize_callback'] ) ) {
$subfield_value = call_user_func( $this->fields[ $subfield_id ]['sanitize_callback'], $subfield_value );
} else {
switch ( $subfield_type ) {
case 'image':
case 'cropped_image':
case 'upload':
$save_as = isset( $subfield['choices'] ) && isset( $subfield['choices']['save_as'] ) ? $subfield['choices']['save_as'] : 'url';
$subfield_value = Upload::sanitize( $subfield_value, $save_as );
break;
case 'dropdown-pages':
$subfield_value = (int) $subfield_value;
break;
case 'color':
if ( $subfield_value ) {
$subfield_value = \Kirki\Field\ReactColorful::sanitize( $subfield_value );
}
break;
case 'text':
$subfield_value = sanitize_text_field( $subfield_value );
break;
case 'url':
case 'link':
$subfield_value = esc_url_raw( $subfield_value );
break;
case 'email':
$subfield_value = filter_var( $subfield_value, FILTER_SANITIZE_EMAIL );
break;
case 'tel':
$subfield_value = sanitize_text_field( $subfield_value );
break;
case 'checkbox':
$subfield_value = (bool) $subfield_value;
break;
case 'select':
if ( isset( $this->fields[ $subfield_id ]['multiple'] ) ) {
if ( true === $this->fields[ $subfield_id ]['multiple'] ) {
$multiple = 2;
}
$multiple = (int) $this->fields[ $subfield_id ]['multiple'];
if ( 1 < $multiple ) {
$subfield_value = (array) $subfield_value;
foreach ( $subfield_value as $sub_subfield_key => $sub_subfield_value ) {
$subfield_value[ $sub_subfield_key ] = sanitize_text_field( $sub_subfield_value );
}
} else {
$subfield_value = sanitize_text_field( $subfield_value );
}
}
break;
case 'radio':
case 'radio-image':
$subfield_value = sanitize_text_field( $subfield_value );
break;
case 'textarea':
$subfield_value = html_entity_decode( wp_kses_post( $subfield_value ) );
}
}
$value[ $row_id ][ $subfield_id ] = $subfield_value;
}
}
return $value;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* Repeater Customizer Setting.
*
* @package kirki-framework/control-repeater
* @copyright Copyright (c) 2019, Ari Stathopoulos (@aristath)
* @license https://opensource.org/licenses/MIT
* @since 1.0
*/
namespace Kirki\Settings;
/**
* Repeater Settings.
*/
class Repeater extends \WP_Customize_Setting {
/**
* Constructor.
*
* Any supplied $args override class property defaults.
*
* @access public
* @since 1.0
* @param WP_Customize_Manager $manager The WordPress WP_Customize_Manager object.
* @param string $id A specific ID of the setting. Can be a theme mod or option name.
* @param array $args Setting arguments.
*/
public function __construct( $manager, $id, $args = [] ) {
parent::__construct( $manager, $id, $args );
// Will convert the setting from JSON to array. Must be triggered very soon.
add_filter( "customize_sanitize_{$this->id}", [ $this, 'sanitize_repeater_setting' ], 10, 1 );
}
/**
* Fetch the value of the setting.
*
* @access public
* @since 1.0
* @return mixed The value.
*/
public function value() {
return (array) parent::value();
}
/**
* Convert the JSON encoded setting coming from Customizer to an Array.
*
* @access public
* @since 1.0
* @param string $value URL Encoded JSON Value.
* @return array
*/
public function sanitize_repeater_setting( $value ) {
if ( ! is_array( $value ) ) {
$value = json_decode( urldecode( $value ) );
}
if ( empty( $value ) || ! is_array( $value ) ) {
$value = [];
}
// Make sure that every row is an array, not an object.
foreach ( $value as $key => $val ) {
$value[ $key ] = (array) $val;
if ( empty( $val ) ) {
unset( $value[ $key ] );
}
}
// Reindex array.
if ( is_array( $value ) ) {
$value = array_values( $value );
}
return $value;
}
}

View file

@ -0,0 +1,824 @@
import "./control.scss";
/* global kirkiControlLoader */
/* eslint max-depth: 0 */
/* eslint no-useless-escape: 0 */
var RepeaterRow = function( rowIndex, container, label, control ) {
var self = this;
this.rowIndex = rowIndex;
this.container = container;
this.label = label;
this.header = this.container.find( '.repeater-row-header' );
this.header.on( 'click', function() {
self.toggleMinimize();
} );
this.container.on( 'click', '.repeater-row-remove', function() {
self.remove();
} );
this.header.on( 'mousedown', function() {
self.container.trigger( 'row:start-dragging' );
} );
this.container.on( 'keyup change', 'input, select, textarea', function( e ) {
self.container.trigger( 'row:update', [ self.rowIndex, jQuery( e.target ).data( 'field' ), e.target ] );
} );
this.setRowIndex = function( rowNum ) {
this.rowIndex = rowNum;
this.container.attr( 'data-row', rowNum );
this.container.data( 'row', rowNum );
this.updateLabel();
};
this.toggleMinimize = function() {
// Store the previous state.
this.container.toggleClass( 'minimized' );
this.header.find( '.dashicons' ).toggleClass( 'dashicons-arrow-up' ).toggleClass( 'dashicons-arrow-down' );
};
this.remove = function() {
this.container.slideUp( 300, function() {
jQuery( this ).detach();
} );
this.container.trigger( 'row:remove', [ this.rowIndex ] );
};
this.updateLabel = function() {
var rowLabelField,
rowLabel,
rowLabelSelector;
if ( 'field' === this.label.type ) {
rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' );
if ( _.isFunction( rowLabelField.val ) ) {
rowLabel = rowLabelField.val();
if ( '' !== rowLabel ) {
if ( ! _.isUndefined( control.params.fields[ this.label.field ] ) ) {
if ( ! _.isUndefined( control.params.fields[ this.label.field ].type ) ) {
if ( 'select' === control.params.fields[ this.label.field ].type ) {
if ( ! _.isUndefined( control.params.fields[ this.label.field ].choices ) && ! _.isUndefined( control.params.fields[ this.label.field ].choices[ rowLabelField.val() ] ) ) {
rowLabel = control.params.fields[ this.label.field ].choices[ rowLabelField.val() ];
}
} else if ( 'radio' === control.params.fields[ this.label.field ].type || 'radio-image' === control.params.fields[ this.label.field ].type ) {
rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked';
rowLabel = jQuery( rowLabelSelector ).val();
}
}
}
this.header.find( '.repeater-row-label' ).text( rowLabel );
return;
}
}
}
this.header.find( '.repeater-row-label' ).text( this.label.value + ' ' + ( this.rowIndex + 1 ) );
};
this.updateLabel();
};
wp.customize.controlConstructor.repeater = wp.customize.Control.extend( {
// When we're finished loading continue processing
ready: function() {
var control = this;
// Init the control.
if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
kirkiControlLoader( control );
} else {
control.initKirkiControl();
}
},
initKirkiControl: function( control ) {
var limit, theNewRow, settingValue;
control = control || this;
// The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
settingValue = control.params.value;
// The hidden field that keeps the data saved (though we never update it)
control.settingField = control.container.find( '[data-customize-setting-link]' ).first();
// Set the field value for the first time, we'll fill it up later
control.setValue( [], false );
// The DIV that holds all the rows
control.repeaterFieldsContainer = control.container.find( '.repeater-fields' ).first();
// Set number of rows to 0
control.currentIndex = 0;
// Save the rows objects
control.rows = [];
// Default limit choice
limit = false;
if ( ! _.isUndefined( control.params.choices.limit ) ) {
limit = ( 0 >= control.params.choices.limit ) ? false : parseInt( control.params.choices.limit, 10 );
}
control.container.on( 'click', 'button.repeater-add', function( e ) {
e.preventDefault();
if ( ! limit || control.currentIndex < limit ) {
theNewRow = control.addRow();
theNewRow.toggleMinimize();
control.initColorPicker();
control.initSelect( theNewRow );
} else {
jQuery( control.selector + ' .limit' ).addClass( 'highlight' );
}
} );
control.container.on( 'click', '.repeater-row-remove', function() {
control.currentIndex--;
if ( ! limit || control.currentIndex < limit ) {
jQuery( control.selector + ' .limit' ).removeClass( 'highlight' );
}
} );
control.container.on( 'click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function( e ) {
e.preventDefault();
control.$thisButton = jQuery( this );
control.openFrame( e );
} );
control.container.on( 'click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function( e ) {
e.preventDefault();
control.$thisButton = jQuery( this );
control.removeImage( e );
} );
control.container.on( 'click keypress', '.repeater-field-upload .remove-button', function( e ) {
e.preventDefault();
control.$thisButton = jQuery( this );
control.removeFile( e );
} );
/**
* Function that loads the Mustache template
*/
control.repeaterTemplate = _.memoize( function() {
var compiled,
/*
* Underscore's default ERB-style templates are incompatible with PHP
* when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
*
* @see trac ticket #22344.
*/
options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
escape: /\{\{([^\}]+?)\}\}(?!\})/g,
variable: 'data'
};
return function( data ) {
compiled = _.template( control.container.find( '.customize-control-repeater-content' ).first().html(), null, options );
return compiled( data );
};
} );
// When we load the control, the fields have not been filled up
// This is the first time that we create all the rows
if ( settingValue.length ) {
_.each( settingValue, function( subValue ) {
theNewRow = control.addRow( subValue );
control.initColorPicker();
control.initSelect( theNewRow, subValue );
} );
}
control.repeaterFieldsContainer.sortable( {
handle: '.repeater-row-header',
update: function() {
control.sort();
}
} );
},
/**
* Open the media modal.
*
* @param {Object} event - The JS event.
* @returns {void}
*/
openFrame: function( event ) {
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
return;
}
if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-cropped_image' ) ) {
this.initCropperFrame();
} else {
this.initFrame();
}
this.frame.open();
},
initFrame: function() {
var libMediaType = this.getMimeType();
this.frame = wp.media( {
states: [
new wp.media.controller.Library( {
library: wp.media.query( { type: libMediaType } ),
multiple: false,
date: false
} )
]
} );
// When a file is selected, run a callback.
this.frame.on( 'select', this.onSelect, this );
},
/**
* Create a media modal select frame, and store it so the instance can be reused when needed.
* This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
*
* @returns {void}
*/
initCropperFrame: function() {
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ),
attrs = [ 'width', 'height', 'flex_width', 'flex_height' ], // A list of attributes to look for
libMediaType = this.getMimeType();
// Make sure we got it
if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
// Make fields is defined and only do the hack for cropped_image
if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'cropped_image' === this.params.fields[ currentFieldId ].type ) {
//Iterate over the list of attributes
attrs.forEach( function( el ) {
// If the attribute exists in the field
if ( ! _.isUndefined( this.params.fields[ currentFieldId ][ el ] ) ) {
// Set the attribute in the main object
this.params[ el ] = this.params.fields[ currentFieldId ][ el ];
}
}.bind( this ) );
}
}
this.frame = wp.media( {
button: {
text: 'Select and Crop',
close: false
},
states: [
new wp.media.controller.Library( {
library: wp.media.query( { type: libMediaType } ),
multiple: false,
date: false,
suggestedWidth: this.params.width,
suggestedHeight: this.params.height
} ),
new wp.media.controller.CustomizeImageCropper( {
imgSelectOptions: this.calculateImageSelectOptions,
control: this
} )
]
} );
this.frame.on( 'select', this.onSelectForCrop, this );
this.frame.on( 'cropped', this.onCropped, this );
this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
},
onSelect: function() {
var attachment = this.frame.state().get( 'selection' ).first().toJSON();
if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-upload' ) ) {
this.setFileInRepeaterField( attachment );
} else {
this.setImageInRepeaterField( attachment );
}
},
/**
* After an image is selected in the media modal, switch to the cropper
* state if the image isn't the right size.
*/
onSelectForCrop: function() {
var attachment = this.frame.state().get( 'selection' ).first().toJSON();
if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
this.setImageInRepeaterField( attachment );
} else {
this.frame.setState( 'cropper' );
}
},
/**
* After the image has been cropped, apply the cropped image data to the setting.
*
* @param {object} croppedImage Cropped attachment data.
* @returns {void}
*/
onCropped: function( croppedImage ) {
this.setImageInRepeaterField( croppedImage );
},
/**
* Returns a set of options, computed from the attached image data and
* control-specific data, to be fed to the imgAreaSelect plugin in
* wp.media.view.Cropper.
*
* @param {wp.media.model.Attachment} attachment - The attachment from the WP API.
* @param {wp.media.controller.Cropper} controller - Media controller.
* @returns {Object} - Options.
*/
calculateImageSelectOptions: function( attachment, controller ) {
var control = controller.get( 'control' ),
flexWidth = !! parseInt( control.params.flex_width, 10 ),
flexHeight = !! parseInt( control.params.flex_height, 10 ),
realWidth = attachment.get( 'width' ),
realHeight = attachment.get( 'height' ),
xInit = parseInt( control.params.width, 10 ),
yInit = parseInt( control.params.height, 10 ),
ratio = xInit / yInit,
xImg = realWidth,
yImg = realHeight,
x1,
y1,
imgSelectOptions;
controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
if ( xImg / yImg > ratio ) {
yInit = yImg;
xInit = yInit * ratio;
} else {
xInit = xImg;
yInit = xInit / ratio;
}
x1 = ( xImg - xInit ) / 2;
y1 = ( yImg - yInit ) / 2;
imgSelectOptions = {
handles: true,
keys: true,
instance: true,
persistent: true,
imageWidth: realWidth,
imageHeight: realHeight,
x1: x1,
y1: y1,
x2: xInit + x1,
y2: yInit + y1
};
if ( false === flexHeight && false === flexWidth ) {
imgSelectOptions.aspectRatio = xInit + ':' + yInit;
}
if ( false === flexHeight ) {
imgSelectOptions.maxHeight = yInit;
}
if ( false === flexWidth ) {
imgSelectOptions.maxWidth = xInit;
}
return imgSelectOptions;
},
/**
* Return whether the image must be cropped, based on required dimensions.
*
* @param {bool} flexW - The flex-width.
* @param {bool} flexH - The flex-height.
* @param {int} dstW - Initial point distance in the X axis.
* @param {int} dstH - Initial point distance in the Y axis.
* @param {int} imgW - Width.
* @param {int} imgH - Height.
* @returns {bool} - Whether the image must be cropped or not based on required dimensions.
*/
mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
return ! ( ( true === flexW && true === flexH ) || ( true === flexW && dstH === imgH ) || ( true === flexH && dstW === imgW ) || ( dstW === imgW && dstH === imgH ) || ( imgW <= dstW ) );
},
/**
* If cropping was skipped, apply the image data directly to the setting.
*
* @returns {void}
*/
onSkippedCrop: function() {
var attachment = this.frame.state().get( 'selection' ).first().toJSON();
this.setImageInRepeaterField( attachment );
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment - The attachment object.
* @returns {void}
*/
setImageInRepeaterField: function( attachment ) {
var $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image' );
$targetDiv.find( '.kirki-image-attachment' ).html( '<img src="' + attachment.url + '">' ).hide().slideDown( 'slow' );
$targetDiv.find( '.hidden-field' ).val( attachment.id );
this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
$targetDiv.find( '.remove-button' ).show();
//This will activate the save button
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
this.frame.close();
},
/**
* Updates the setting and re-renders the control UI.
*
* @param {object} attachment - The attachment object.
* @returns {void}
*/
setFileInRepeaterField: function( attachment ) {
var $targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
$targetDiv.find( '.kirki-file-attachment' ).html( '<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>' ).hide().slideDown( 'slow' );
$targetDiv.find( '.hidden-field' ).val( attachment.id );
this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
$targetDiv.find( '.upload-button' ).show();
$targetDiv.find( '.remove-button' ).show();
//This will activate the save button
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
this.frame.close();
},
getMimeType: function() {
// We get the field id from which this was called
var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' );
// Make sure we got it
if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
// Make fields is defined and only do the hack for cropped_image
if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'upload' === this.params.fields[ currentFieldId ].type ) {
// If the attribute exists in the field
if ( ! _.isUndefined( this.params.fields[ currentFieldId ].mime_type ) ) {
// Set the attribute in the main object
return this.params.fields[ currentFieldId ].mime_type;
}
}
}
return 'image';
},
removeImage: function( event ) {
var $targetDiv,
$uploadButton;
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
return;
}
$targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload' );
$uploadButton = $targetDiv.find( '.upload-button' );
$targetDiv.find( '.kirki-image-attachment' ).slideUp( 'fast', function() {
jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
} );
$targetDiv.find( '.hidden-field' ).val( '' );
$uploadButton.text( $uploadButton.data( 'label' ) );
this.$thisButton.hide();
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
},
removeFile: function( event ) {
var $targetDiv,
$uploadButton;
if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
return;
}
$targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
$uploadButton = $targetDiv.find( '.upload-button' );
$targetDiv.find( '.kirki-file-attachment' ).slideUp( 'fast', function() {
jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
} );
$targetDiv.find( '.hidden-field' ).val( '' );
$uploadButton.text( $uploadButton.data( 'label' ) );
this.$thisButton.hide();
$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
},
/**
* Get the current value of the setting
*
* @returns {Object} - Returns the value.
*/
getValue: function() {
// The setting is saved in JSON
return JSON.parse( decodeURI( this.setting.get() ) );
},
/**
* Set a new value for the setting
*
* @param {Object} newValue - The new value.
* @param {bool} refresh - If we want to refresh the previewer or not
* @param {bool} filtering - If we want to filter or not.
* @returns {void}
*/
setValue: function( newValue, refresh, filtering ) {
// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
var filteredValue = newValue,
filter = [];
if ( filtering ) {
jQuery.each( this.params.fields, function( index, value ) {
if ( 'image' === value.type || 'cropped_image' === value.type || 'upload' === value.type ) {
filter.push( index );
}
} );
jQuery.each( newValue, function( index, value ) {
jQuery.each( filter, function( ind, field ) {
if ( ! _.isUndefined( value[ field ] ) && ! _.isUndefined( value[ field ].id ) ) {
filteredValue[index][ field ] = value[ field ].id;
}
} );
} );
}
this.setting.set( encodeURI( JSON.stringify( filteredValue ) ) );
if ( refresh ) {
// Trigger the change event on the hidden field so
// previewer refresh the website on Customizer
this.settingField.trigger( 'change' );
}
},
/**
* Add a new row to repeater settings based on the structure.
*
* @param {Object} data - (Optional) Object of field => value pairs (undefined if you want to get the default values)
* @returns {Object} - Returns the new row.
*/
addRow: function( data ) {
var control = this,
template = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
settingValue = this.getValue(), // Get the current setting value.
newRowSetting = {}, // Saves the new setting data.
templateData, // Data to pass to the template
newRow,
i;
if ( template ) {
// The control structure is going to define the new fields
// We need to clone control.params.fields. Assigning it
// ould result in a reference assignment.
templateData = jQuery.extend( true, {}, control.params.fields );
// But if we have passed data, we'll use the data values instead
if ( data ) {
for ( i in data ) {
if ( data.hasOwnProperty( i ) && templateData.hasOwnProperty( i ) ) {
templateData[ i ].default = data[ i ];
}
}
}
templateData.index = this.currentIndex;
// Append the template content
template = template( templateData );
// Create a new row object and append the element
newRow = new RepeaterRow(
control.currentIndex,
jQuery( template ).appendTo( control.repeaterFieldsContainer ),
control.params.row_label,
control
);
newRow.container.on( 'row:remove', function( e, rowIndex ) {
control.deleteRow( rowIndex );
} );
newRow.container.on( 'row:update', function( e, rowIndex, fieldName, element ) {
control.updateField.call( control, e, rowIndex, fieldName, element ); // eslint-disable-line no-useless-call
newRow.updateLabel();
} );
// Add the row to rows collection
this.rows[ this.currentIndex ] = newRow;
for ( i in templateData ) {
if ( templateData.hasOwnProperty( i ) ) {
newRowSetting[ i ] = templateData[ i ].default;
}
}
settingValue[ this.currentIndex ] = newRowSetting;
this.setValue( settingValue, true );
this.currentIndex++;
return newRow;
}
},
sort: function() {
var control = this,
$rows = this.repeaterFieldsContainer.find( '.repeater-row' ),
newOrder = [],
settings = control.getValue(),
newRows = [],
newSettings = [];
$rows.each( function( i, element ) {
newOrder.push( jQuery( element ).data( 'row' ) );
} );
jQuery.each( newOrder, function( newPosition, oldPosition ) {
newRows[ newPosition ] = control.rows[ oldPosition ];
newRows[ newPosition ].setRowIndex( newPosition );
newSettings[ newPosition ] = settings[ oldPosition ];
} );
control.rows = newRows;
control.setValue( newSettings );
},
/**
* Delete a row in the repeater setting
*
* @param {int} index - Position of the row in the complete Setting Array
* @returns {void}
*/
deleteRow: function( index ) {
var currentSettings = this.getValue(),
row,
prop;
if ( currentSettings[ index ] ) {
// Find the row
row = this.rows[ index ];
if ( row ) {
// Remove the row settings
delete currentSettings[ index ];
// Remove the row from the rows collection
delete this.rows[ index ];
// Update the new setting values
this.setValue( currentSettings, true );
}
}
// Remap the row numbers
for ( prop in this.rows ) {
if ( this.rows.hasOwnProperty( prop ) && this.rows[ prop ] ) {
this.rows[ prop ].updateLabel();
}
}
},
/**
* Update a single field inside a row.
* Triggered when a field has changed
*
* @param {Object} e - Event Object
* @param {int} rowIndex - The row's index as an integer.
* @param {string} fieldId - The field ID.
* @param {string|Object} element - The element's identifier, or jQuery Object of the element.
* @returns {void}
*/
updateField: function( e, rowIndex, fieldId, element ) {
var type,
row,
currentSettings;
if ( ! this.rows[ rowIndex ] ) {
return;
}
if ( ! this.params.fields[ fieldId ] ) {
return;
}
type = this.params.fields[ fieldId].type;
row = this.rows[ rowIndex ];
currentSettings = this.getValue();
element = jQuery( element );
if ( _.isUndefined( currentSettings[ row.rowIndex ][ fieldId ] ) ) {
return;
}
if ( 'checkbox' === type ) {
currentSettings[ row.rowIndex ][ fieldId ] = element.is( ':checked' );
} else {
// Update the settings
currentSettings[ row.rowIndex ][ fieldId ] = element.val();
}
this.setValue( currentSettings, true );
},
/**
* Init the color picker on color fields
* Called after AddRow
*
* @returns {void}
*/
initColorPicker: function() {
var control = this,
colorPicker = control.container.find( '.color-picker-hex' ),
options = {},
fieldId = colorPicker.data( 'field' );
// We check if the color palette parameter is defined.
if ( ! _.isUndefined( fieldId ) && ! _.isUndefined( control.params.fields[ fieldId ] ) && ! _.isUndefined( control.params.fields[ fieldId ].palettes ) && _.isObject( control.params.fields[ fieldId ].palettes ) ) {
options.palettes = control.params.fields[ fieldId ].palettes;
}
// When the color picker value is changed we update the value of the field
options.change = function( event, ui ) {
var currentPicker = jQuery( event.target ),
row = currentPicker.closest( '.repeater-row' ),
rowIndex = row.data( 'row' ),
currentSettings = control.getValue();
currentSettings[ rowIndex ][ currentPicker.data( 'field' ) ] = ui.color.toString();
control.setValue( currentSettings, true );
};
// Init the color picker
if ( colorPicker.length && 0 !== colorPicker.length ) {
colorPicker.wpColorPicker( options );
}
},
/**
* Init the dropdown-pages field.
* Called after AddRow
*
* @param {object} theNewRow the row that was added to the repeater
* @param {object} data the data for the row if we're initializing a pre-existing row
* @returns {void}
*/
initSelect: function( theNewRow, data ) {
var control = this,
dropdown = theNewRow.container.find( '.repeater-field select' ),
dataField;
if ( 0 === dropdown.length ) {
return;
}
dataField = dropdown.data( 'field' );
multiple = jQuery( dropdown ).data( 'multiple' );
data = data || {};
data[ dataField ] = data[ dataField ] || '';
jQuery( dropdown ).val( data[ dataField ] || jQuery( dropdown ).val() );
this.container.on( 'change', '.repeater-field select', function( event ) {
var currentDropdown = jQuery( event.target ),
row = currentDropdown.closest( '.repeater-row' ),
rowIndex = row.data( 'row' ),
currentSettings = control.getValue();
currentSettings[ rowIndex ][ currentDropdown.data( 'field' ) ] = jQuery( this ).val();
control.setValue( currentSettings );
} );
}
} );