10 || ! is_array( $r['fields'] ) || ! count( $r['fields'] ) || count( $r['fields'] ) > 6 || isset( $raw['condition'] ) ) { return self::error( __( 'A repeatable group needs 1–6 simple questions and 0–10 rows, with a maximum of at least one. Group conditions are unsupported.', 'killerconversions' ) ); } foreach ( $r['fields'] as $child ) { if ( ! is_array( $child ) || ! isset( $child['type'] ) || ! in_array( $child['type'], array( 'text', 'email', 'number', 'date', 'select' ), true ) || array_intersect( array_keys( $child ), array( 'repeater', 'condition', 'calculation', 'consent' ) ) ) { return self::error( __( 'Repeatable questions support text, email, number, date and dropdown only. Nested groups, consent, files and conditions are unsupported.', 'killerconversions' ) ); } } $children = KCONV_Core::sanitize_schema( array( 'fields' => $r['fields'] ) ); if ( is_wp_error( $children ) ) { return $children; } $clean['required'] = false; $clean['width'] = 'full'; $clean['repeater'] = array( 'min' => $r['min'], 'max' => $r['max'], 'fields' => $children['fields'] ); // Dropdown consent is deliberately unsupported in groups; these are ordinary answers. foreach ( $clean['repeater']['fields'] as &$child ) { unset( $child['consent'] ); } unset( $child ); return $clean; } private static function references( $value, $ids ) { if ( ! is_array( $value ) ) { return false; } foreach ( $value as $key => $part ) { if ( in_array( $key, array( 'source', 'field' ), true ) && is_string( $part ) && in_array( $part, $ids, true ) ) { return true; } if ( is_array( $part ) && self::references( $part, $ids ) ) { return true; } } return false; } public static function schema( $schema, $raw ) { if ( is_wp_error( $schema ) ) { return $schema; } $ids = array(); foreach ( $schema['fields'] as $field ) { if ( 'repeater' === $field['type'] ) { $ids[] = $field['id']; } } if ( count( $ids ) > 3 ) { return self::error( __( 'Use at most three repeatable groups per form.', 'killerconversions' ) ); } foreach ( $schema['fields'] as $field ) { if ( ! empty( $field['condition'] ) && self::references( $field['condition'], $ids ) ) { return self::error( __( 'Repeatable groups cannot control conditional questions.', 'killerconversions' ) ); } if ( ! empty( $field['calculation'] ) ) { foreach ( $ids as $id ) { if ( preg_match( '/\b' . preg_quote( $id, '/' ) . '\b/', wp_json_encode( $field['calculation'] ) ) ) { return self::error( __( 'Repeatable groups cannot be used in calculations.', 'killerconversions' ) ); } } } } if ( $ids && ! empty( $raw['logic']['branches'] ) ) { return self::error( __( 'Repeatable groups currently require a linear step flow without branching.', 'killerconversions' ) ); } foreach ( array( 'estimate', 'logic' ) as $key ) { if ( ! empty( $raw[ $key ] ) && self::references( $raw[ $key ], $ids ) ) { return self::error( __( 'Repeatable groups cannot be pricing or advanced-logic dependencies.', 'killerconversions' ) ); } } return $schema; } public static function is_value( $value ) { if ( ! is_array( $value ) || ! isset( $value['count'], $value['rows'], $value['labels'] ) || ! is_int( $value['count'] ) || $value['count'] < 0 || $value['count'] > 10 || ! is_array( $value['rows'] ) || ! is_array( $value['labels'] ) || count( $value['rows'] ) !== $value['count'] || ! count( $value['labels'] ) || count( $value['labels'] ) > 6 || ( $value['count'] && array_keys( $value['rows'] ) !== range( 0, $value['count'] - 1 ) ) ) { return false; } foreach ( $value['labels'] as $id => $label ) { if ( ! is_string( $id ) || ! preg_match( '/^[a-z][a-z0-9_]{0,39}$/D', $id ) || ! is_string( $label ) || ! strlen( $label ) || KCONV_Field_Rules::length( $label ) > 200 ) { return false; } } foreach ( $value['rows'] as $row ) { if ( ! is_array( $row ) || count( $row ) !== count( $value['labels'] ) || array_diff( array_keys( $row ), array_keys( $value['labels'] ) ) ) { return false; } foreach ( $row as $answer ) { if ( ! is_string( $answer ) || KCONV_Field_Rules::length( $answer ) > 1000 ) { return false; } } } return true; } public static function validate( $previous, $field, $value ) { if ( 'repeater' !== $field['type'] ) { return $previous; } $r = $field['repeater']; $partial = ! empty( $field['_kconv_partial'] ); if ( ! is_array( $value ) || array_diff( array_keys( $value ), array( 'count', 'rows', 'labels' ) ) || ! isset( $value['count'], $value['rows'] ) || ! is_int( $value['count'] ) || ! preg_match( '/^(?:0|[1-9][0-9]?)$/D', (string) $value['count'] ) || ! is_array( $value['rows'] ) ) { return self::error( __( 'The repeatable group is incomplete. Reload and enter its answers again.', 'killerconversions' ) ); } $count = (int) $value['count']; $rows = $value['rows']; if ( $count > $r['max'] || ( ! $partial && $count < $r['min'] ) || count( $rows ) !== $count || ( $count && array_keys( $rows ) !== range( 0, $count - 1 ) ) ) { /* translators: 1: minimum rows, 2: maximum rows. */ return self::error( sprintf( __( 'Use %1$d–%2$d complete rows in this group.', 'killerconversions' ), $partial ? 0 : $r['min'], $r['max'] ) ); } $clean = array(); $keys = array_column( $r['fields'], 'id' ); $labels = array_column( $r['fields'], 'label', 'id' ); foreach ( $rows as $i => $row ) { if ( ! is_array( $row ) || count( $row ) !== count( $keys ) || array_diff( array_keys( $row ), $keys ) || array_diff( $keys, array_keys( $row ) ) ) { return self::error( __( 'A repeatable row is incomplete or contains unknown questions. Reload before submitting.', 'killerconversions' ) ); } $children = $r['fields']; if ( $partial ) { foreach ( $children as &$child ) { $child['required'] = false; } unset( $child ); } list( $answers, $errors ) = KCONV_Core::validate_values( $children, $row ); if ( $errors ) { $key = key( $errors ); /* translators: 1: row number, 2: question label, 3: validation error. */ return self::error( sprintf( __( 'Row %1$d, %2$s: %3$s', 'killerconversions' ), $i + 1, $labels[ $key ], $errors[ $key ] ) ); } $clean[] = $answers; } return array( 'value' => array( 'count' => $count, 'rows' => $clean, 'labels' => $labels ) ); } public static function text( $value ) { if ( ! self::is_value( $value ) ) { return ''; } $lines = array(); foreach ( $value['rows'] as $i => $row ) { /* translators: Sequential row number in a repeatable group. */ $lines[] = sprintf( __( 'Row %d', 'killerconversions' ), $i + 1 ); foreach ( $row as $key => $answer ) { $label = isset( $value['labels'][ $key ] ) && is_string( $value['labels'][ $key ] ) ? $value['labels'][ $key ] : $key; $lines[] = $label . ': ' . ( is_scalar( $answer ) ? (string) $answer : '' ); } } return implode( "\n", $lines ); } public static function emails( $values, $fields ) { $emails = array(); foreach ( $fields as $field ) { if ( 'repeater' !== $field['type'] || ! isset( $values[ $field['id'] ] ) || ! self::is_value( $values[ $field['id'] ] ) ) { continue; } foreach ( $field['repeater']['fields'] as $child ) { if ( 'email' !== $child['type'] ) { continue; } foreach ( $values[ $field['id'] ]['rows'] as $row ) { if ( ! empty( $row[ $child['id'] ] ) && is_email( $row[ $child['id'] ] ) ) { $emails[] = strtolower( $row[ $child['id'] ] ); } } } } return array_unique( $emails ); } public static function render( $html, $field, $form, $instance ) { if ( 'repeater' !== $field['type'] ) { return $html; } $id = $instance . '-' . $field['id']; return '
' . esc_html( $field['label'] ) . '

' . esc_html( $field['help'] ) . '

'; } public static function assets( $form ) { if ( self::has( $form ) ) { wp_enqueue_script( 'kconv-repeaters', KCONV_URL . 'assets/repeaters.js', array( 'kconv-forms', 'wp-i18n' ), KCONV_VERSION, true ); wp_set_script_translations( 'kconv-repeaters', 'killerconversions', KCONV_DIR . 'languages' ); wp_enqueue_style( 'kconv-repeaters', KCONV_URL . 'assets/repeaters.css', array( 'kconv-forms' ), KCONV_VERSION ); } } public static function admin_assets() { if ( KCONV_Admin_Scope::plugin( array( 'killerconversions' ) ) ) { wp_enqueue_script( 'kconv-repeaters-admin', KCONV_URL . 'assets/repeaters-admin.js', array( 'kconv-admin', 'wp-i18n' ), KCONV_VERSION, true ); wp_set_script_translations( 'kconv-repeaters-admin', 'killerconversions', KCONV_DIR . 'languages' ); wp_enqueue_style( 'kconv-repeaters-admin', KCONV_URL . 'assets/repeaters.css', array( 'kconv-workspace' ), KCONV_VERSION ); } } }