1: <?php
2:
3: 4: 5:
6: class Papi_Property_Repeater extends Papi_Property {
7:
8: 9: 10: 11: 12:
13: public $convert_type = 'array';
14:
15: 16: 17: 18: 19:
20: protected $counter = 0;
21:
22: 23: 24: 25: 26:
27: public $default_value = [];
28:
29: 30: 31: 32: 33:
34: protected $exclude_properties = ['flexible', 'repeater'];
35:
36: 37: 38: 39: 40: 41: 42: 43: 44:
45: public function delete_value( $slug, $post_id, $type ) {
46: $rows = intval( papi_get_property_meta_value( $post_id, $slug ) );
47: $value = $this->load_value( $rows, $slug, $post_id );
48: $value = papi_to_property_array_slugs( $value, $slug );
49: $result = true;
50:
51: foreach ( $value as $key => $value ) {
52: $out = papi_delete_property_meta_value( $post_id, $key, $type );
53: $result = $out ? $result : $out;
54: }
55:
56: return $result;
57: }
58:
59: 60: 61: 62: 63: 64: 65: 66: 67: 68:
69: public function format_value( $values, $repeater_slug, $post_id ) {
70: if ( ! is_array( $values ) ) {
71: return [];
72: }
73:
74: $values = papi_to_property_array_slugs( $values, $repeater_slug );
75:
76: foreach ( $values as $slug => $value ) {
77: if ( papi_is_property_type_key( $slug ) ) {
78: continue;
79: }
80:
81: $property_type_slug = papi_get_property_type_key_f( $slug );
82:
83: if ( ! isset( $values[$property_type_slug] ) ) {
84: continue;
85: }
86:
87:
88: $property_type_value = $values[$property_type_slug];
89: $property_type = papi_get_property_type( $property_type_value );
90:
91: if ( ! is_object( $property_type ) ) {
92: continue;
93: }
94:
95:
96: $child_slug = $this->get_child_slug( $repeater_slug, $slug );
97:
98:
99: $values[$slug] = $property_type->load_value( $value, $child_slug, $post_id );
100: $values[$slug] = papi_filter_load_value( $property_type->type, $values[$slug], $child_slug, $post_id );
101:
102:
103: $values[$slug] = $property_type->format_value( $values[$slug], $child_slug, $post_id );
104:
105: if ( ! is_admin() ) {
106: $values[$slug] = papi_filter_format_value( $property_type->type, $values[$slug], $child_slug, $post_id );
107: }
108:
109: $values[$property_type_slug] = $property_type_value;
110: }
111:
112: if ( ! is_admin() ) {
113: foreach ( $values as $slug => $value ) {
114: if ( papi_is_property_type_key( $slug ) ) {
115: unset( $values[$slug] );
116: }
117: }
118: }
119:
120: return papi_from_property_array_slugs( $values, $repeater_slug );
121: }
122:
123: 124: 125: 126: 127: 128: 129: 130:
131: protected function get_child_slug( $repeater_slug, $child_slug ) {
132: return preg_replace( '/^\_/', '', preg_replace( '/\_\d+\_/', '', str_replace( $repeater_slug, '', $child_slug ) ) );
133: }
134:
135: 136: 137: 138: 139:
140: public function get_default_settings() {
141: return [
142: 'add_new_label' => __( 'Add new row', 'papi' ),
143: 'closed_rows' => false,
144: 'items' => [],
145: 'layout' => 'table',
146: 'limit' => -1
147: ];
148: }
149:
150: 151: 152: 153: 154:
155: public function get_import_settings() {
156: return [
157: 'property_array_slugs' => true
158: ];
159: }
160:
161: 162: 163: 164: 165: 166: 167: 168: 169:
170: protected function get_results( $value, $repeater_slug, $post_id ) {
171: global $wpdb;
172:
173: $option_page = $this->is_option_page();
174:
175: if ( $option_page ) {
176: $table = $wpdb->prefix . 'options';
177: $query = $wpdb->prepare(
178: "SELECT * FROM `$table` WHERE `option_name` LIKE '%s' ORDER BY `option_id` ASC",
179: $repeater_slug . '_%'
180: );
181: } else {
182: $table = $wpdb->prefix . 'postmeta';
183: $query = $wpdb->prepare(
184: "SELECT * FROM `$table` WHERE `meta_key` LIKE '%s' AND `post_id` = %s ORDER BY `meta_id` ASC", $repeater_slug . '_%',
185: $post_id
186: );
187: }
188:
189: $dbresults = $wpdb->get_results( $query );
190: $value = intval( $value );
191:
192:
193: if ( empty( $value ) ) {
194: return [[], []];
195: }
196:
197: $values = [];
198: $results = [];
199: $trash = [];
200:
201:
202: $rows = array_values( $this->get_row_results( $dbresults ) );
203:
204:
205: $values[$repeater_slug] = $value;
206:
207: for ( $i = 0; $i < $value; $i++ ) {
208: $no_trash = [];
209:
210: if ( ! isset( $no_trash[$i] ) ) {
211: $no_trash[$i] = [];
212: }
213:
214: if ( ! isset( $rows[$i] ) ) {
215: continue;
216: }
217:
218: foreach ( $rows[$i] as $slug => $meta ) {
219: if ( ! is_string( $slug ) || ! isset( $rows[$i][$slug] ) ) {
220: continue;
221: }
222:
223:
224:
225: $no_trash[$slug] = $meta;
226:
227:
228: $values[$meta->meta_key] = papi_maybe_json_decode(
229: maybe_unserialize( $meta->meta_value )
230: );
231: }
232:
233:
234: $trash_diff = array_diff( array_keys( $rows[$i] ), array_keys( $no_trash[$i] ) );
235:
236: if ( ! empty( $trash_diff ) ) {
237:
238: foreach ( $trash_diff as $slug ) {
239: if ( ! isset( $results[$i] ) || ! isset( $rows[$i][$slug] ) ) {
240: continue;
241: }
242:
243: $trash[$results[$i][$slug]->meta_key] = $rows[$i][$slug];
244: }
245: }
246: }
247:
248: $properties = $this->get_settings_properties();
249:
250:
251: for ( $i = 0; $i < $value; $i++ ) {
252: foreach ( $properties as $prop ) {
253: $slug = sprintf( '%s_%d_%s', $repeater_slug, $i, papi_remove_papi( $prop->slug ) );
254:
255: if ( ! isset( $values[$slug] ) ) {
256: $values[$slug] = null;
257: }
258: }
259: }
260:
261: return [$values, $trash];
262: }
263:
264: 265: 266: 267: 268: 269: 270:
271: protected function get_row_results( $dbresults ) {
272: $results = [];
273: $option_page = $this->is_option_page();
274:
275: foreach ( $dbresults as $key => $meta ) {
276:
277: if ( $option_page ) {
278: preg_match( '/^[^\d]*(\d+)/', $meta->option_name, $matches );
279: } else {
280: preg_match( '/^[^\d]*(\d+)/', $meta->meta_key, $matches );
281: }
282:
283: if ( count( $matches ) < 2 ) {
284: continue;
285: }
286: $i = intval( $matches[1] );
287:
288: if ( ! isset( $results[$i] ) ) {
289: $results[$i] = [];
290: }
291:
292: if ( $option_page ) {
293: $results[$i][$meta->option_name] = (object) [
294: 'meta_key' => $meta->option_name,
295: 'meta_value' => $meta->option_value
296: ];
297: } else {
298: $results[$i][$meta->meta_key] = $meta;
299: }
300: }
301:
302: return $results;
303: }
304:
305: 306: 307: 308: 309:
310: protected function get_settings_properties() {
311: $settings = $this->get_settings();
312:
313: if ( is_null( $settings ) ) {
314: return [];
315: }
316:
317: return $this->prepare_properties( papi_to_array( $settings->items ) );
318: }
319:
320: 321: 322:
323: public function html() {
324: $options = $this->get_options();
325:
326:
327: $this->counter = 0;
328:
329:
330: $this->render_repeater( $options );
331:
332:
333: $this->render_json_template( $options->slug );
334: }
335:
336: 337: 338: 339: 340: 341: 342: 343: 344:
345: public function import_value( $value, $slug, $post_id ) {
346: if ( ! is_array( $value ) ) {
347: return [];
348: }
349:
350:
351: $extras = array_filter( $value, function ( $value ) {
352: return ! is_array( $value );
353: } );
354:
355: if ( ! empty( $extras ) ) {
356: $extra = [];
357:
358: foreach ( $extras as $key => $val ) {
359: if ( isset( $value[$key] ) ) {
360: unset( $value[$key] );
361: }
362:
363: $extra[$key] = $val;
364: }
365:
366: $value[] = $extra;
367: }
368:
369: return $this->update_value( $value, $slug, $post_id );
370: }
371:
372: 373: 374: 375: 376: 377: 378:
379: protected function layout( $layout ) {
380: return $this->get_setting( 'layout' ) === $layout;
381: }
382:
383: 384: 385: 386: 387: 388: 389: 390: 391: 392:
393: public function load_value( $value, $repeater_slug, $post_id ) {
394: if ( is_array( $value ) ) {
395: return $value;
396: }
397:
398: list( $results, $trash ) = $this->get_results( $value, $repeater_slug, $post_id );
399:
400:
401: unset( $trash );
402:
403: $page = $this->get_page();
404: $types = [];
405: $results = papi_from_property_array_slugs(
406: $results,
407: papi_remove_papi( $repeater_slug )
408: );
409:
410: if ( empty( $page ) || empty( $results ) ) {
411: return $this->default_value;
412: }
413:
414: foreach ( $results[0] as $slug => $value ) {
415: if ( $property = $page->get_property( $repeater_slug, $slug ) ) {
416: $types[$slug] = $property;
417: }
418: }
419:
420: foreach ( $results as $index => $row ) {
421: foreach ( $row as $slug => $value ) {
422: if ( ! isset( $types[$slug] ) ) {
423: continue;
424: }
425:
426: $type_key = papi_get_property_type_key_f( $slug );
427: $results[$index][$type_key] = $types[$slug];
428: }
429: }
430:
431: return $results;
432: }
433:
434: 435: 436: 437: 438: 439: 440: 441:
442: protected function prepare_properties( $items ) {
443: $key = isset( $this->layout_key ) &&
444: $this->layout_key === '_layout' ? 'flexible' : 'repeater';
445: $items = array_map( 'papi_get_property_options', $items );
446:
447: $exclude_properties = $this->exclude_properties;
448: $exclude_properties = array_merge(
449: $exclude_properties,
450: apply_filters( 'papi/property/' . $key . '/exclude', [] )
451: );
452:
453: return array_filter( $items, function ( $item ) use ( $exclude_properties ) {
454: if ( ! is_object( $item ) ) {
455: return false;
456: }
457:
458: if ( empty( $item->type ) ) {
459: return false;
460: }
461:
462: return ! in_array( $item->type, $exclude_properties );
463: } );
464: }
465:
466: 467: 468: 469: 470: 471:
472: protected function remove_repeater_rows( $post_id, $repeater_slug ) {
473: global $wpdb;
474:
475: $option_page = $this->is_option_page();
476: $repeater_slug = $repeater_slug . '_%';
477:
478: if ( $option_page ) {
479: $table = $wpdb->prefix . 'options';
480: $sql = "SELECT * FROM $table WHERE (`option_name` LIKE %s OR `option_name` LIKE %s AND NOT `option_name` = %s)";
481: $query = $wpdb->prepare(
482: $sql,
483: $repeater_slug,
484: papi_f( $repeater_slug ),
485: papi_get_property_type_key_f( $repeater_slug )
486: );
487: } else {
488: $table = $wpdb->prefix . 'postmeta';
489: $sql = "SELECT * FROM $table WHERE `post_id` = %d AND (`meta_key` LIKE %s OR `meta_key` LIKE %s AND NOT `meta_key` = %s)";
490: $query = $wpdb->prepare(
491: $sql,
492: $post_id,
493: $repeater_slug,
494: papi_f( $repeater_slug ),
495: papi_get_property_type_key_f( $repeater_slug )
496: );
497: }
498:
499: $results = $wpdb->get_results( $query );
500:
501: foreach ( $results as $res ) {
502: if ( $option_page ) {
503: $key = $res->option_name;
504: } else {
505: $key = $res->meta_key;
506: }
507:
508: papi_delete_property_meta_value( $post_id, $key );
509: }
510: }
511:
512: 513: 514:
515: public function render_ajax_request() {
516: $items = $this->get_settings_properties();
517:
518: if ( papi_doing_ajax() ) {
519: $counter = papi_get_qs( 'counter' );
520: $this->counter = intval( $counter );
521: }
522:
523: $this->render_properties( $items, false );
524: }
525:
526: 527: 528: 529: 530:
531: protected function render_json_template( $slug ) {
532: $options = $this->get_options();
533:
534: foreach ( $options->settings->items as $key => $value ) {
535: if ( ! papi_is_property( $value ) ) {
536: unset( $options->settings->items[$key] );
537: continue;
538: }
539:
540: $options->settings->items[$key] = clone $value->get_options();
541: }
542:
543: papi_render_html_tag( 'script', [
544: 'data-papi-json' => sprintf( '%s_repeater_json', $slug ),
545: 'type' => 'application/json',
546: json_encode( [$options] )
547: ] );
548: }
549:
550: 551: 552: 553: 554: 555:
556: protected function render_properties( $row, $value ) {
557: $layout = $this->get_setting( 'layout' );
558:
559: if ( $layout === 'row' ): ?>
560: <td class="repeater-layout-row">
561: <div class="repeater-content-open">
562: <table class="papi-table">
563: <tbody>
564: <?php endif;
565:
566: $has_value = $value !== false;
567:
568: foreach ( $row as $property ) {
569:
570: if ( $property->disabled() ) {
571: continue;
572: }
573:
574: $render_property = clone $property->get_options();
575: $value_slug = $property->get_slug( true );
576:
577: if ( $has_value ) {
578: if ( array_key_exists( $value_slug, $value ) ) {
579: $render_property->value = $value[$value_slug];
580: } else {
581: if ( array_key_exists( $property->get_slug(), $value ) ) {
582: $render_property->value = $property->default_value;
583: } else {
584: continue;
585: }
586: }
587: }
588:
589: $render_property->slug = $this->html_name( $property, $this->counter );
590: $render_property->raw = $layout === 'table';
591:
592: if ( $layout === 'table' ) {
593: echo '<td class="repeater-column ' . ( $property->display() ? '' : 'papi-hide' ) . '">';
594: echo '<div class="repeater-content-open">';
595: echo sprintf(
596: '<label for="%s" class="papi-visually-hidden">%s</label>',
597: $this->html_id( $property, $this->counter ),
598: $property->title
599: );
600: }
601:
602: papi_render_property( $render_property );
603:
604: if ( $layout === 'table' ) {
605: echo '</div>';
606: echo '</td>';
607: }
608: }
609:
610: if ( $layout === 'row' ): ?>
611: </tbody>
612: </table>
613: </div>
614: </td>
615: <?php endif;
616: }
617:
618: 619: 620: 621: 622:
623: protected function render_repeater( $options ) {
624: ?>
625: <div class="papi-property-repeater papi-property-repeater-top" data-limit="<?php echo $this->get_setting( 'limit' ); ?>">
626: <table class="papi-table">
627: <?php $this->render_repeater_head(); ?>
628:
629: <tbody class="repeater-tbody">
630: <?php $this->render_repeater_rows(); ?>
631: </tbody>
632: </table>
633:
634: <div class="bottom">
635: <?php
636: papi_render_html_tag( 'button', [
637: 'class' => 'button button-primary',
638: 'data-papi-json' => sprintf( '%s_repeater_json', $options->slug ),
639: 'type' => 'button',
640: esc_html( $this->get_setting( 'add_new_label' ) )
641: ] );
642: ?>
643: </div>
644:
645: <?php ?>
646:
647: <input type="hidden" data-papi-rule="<?php echo $options->slug; ?>" name="<?php echo $options->slug; ?>[]" />
648: </div>
649: <?php
650: }
651:
652: 653: 654:
655: protected function render_repeater_head() {
656: $properties = $this->get_settings_properties();
657: ?>
658: <thead>
659: <?php if ( ! $this->layout( 'row' ) ): ?>
660: <tr>
661: <th></th>
662: <?php
663: foreach ( $properties as $property ):
664:
665: if ( $property->disabled() ) {
666: continue;
667: }
668: ?>
669: <th class="repeater-column <?php echo $property->display() ? '' : 'papi-hide'; ?>">
670: <?php echo $property->title; ?>
671: </th>
672: <?php endforeach; ?>
673: <th class="last"></th>
674: </tr>
675: <?php endif; ?>
676: </thead>
677: <?php
678: }
679:
680: 681: 682:
683: protected function render_repeater_rows() {
684: $items = $this->get_settings_properties();
685: $values = $this->get_value();
686: $slugs = wp_list_pluck( $items, 'slug' );
687:
688:
689: foreach ( $values as $index => $value ) {
690: $keys = array_keys( $value );
691:
692: foreach ( $slugs as $slug ) {
693: if ( in_array( $slug, $keys ) ) {
694: continue;
695: }
696:
697: $values[$index][$slug] = '';
698: }
699: }
700:
701: $values = array_filter( $values );
702: $closed_rows = $this->get_setting( 'closed_rows', true );
703:
704: foreach ( $values as $row ):
705: ?>
706: <tr <?php echo $closed_rows ? 'class="closed"' : ''; ?>>
707: <td class="handle">
708: <span class="toggle"></span>
709: <span class="count"><?php echo $this->counter + 1; ?></span>
710: </td>
711: <?php
712: $this->render_properties( $items, $row );
713: $this->counter++;
714: ?>
715: <td class="last">
716: <span>
717: <a title="<?php _e( 'Remove', 'papi' ); ?>" href="#" class="repeater-remove-item">x</a>
718: </span>
719: </td>
720: </tr>
721: <?php
722: endforeach;
723: }
724:
725: 726: 727:
728: public function render_repeater_rows_template() {
729: ?>
730: <script type="text/template" id="tmpl-papi-property-repeater-row">
731: <tr>
732: <td class="handle">
733: <span class="toggle"></span>
734: <span class="count"><%= counter + 1 %></span>
735: </td>
736: <%= columns %>
737: <td class="last">
738: <span>
739: <a title="<?php _e( 'Remove', 'papi' ); ?>" href="#" class="repeater-remove-item">x</a>
740: </span>
741: </td>
742: </tr>
743: </script>
744: <?php
745: }
746:
747: 748: 749:
750: protected function setup_actions() {
751: add_action( 'admin_head', [$this, 'render_repeater_rows_template'] );
752: }
753:
754: 755: 756: 757: 758: 759: 760: 761: 762:
763: public function update_value( $values, $repeater_slug, $post_id ) {
764: $rows = intval( papi_get_property_meta_value(
765: $post_id,
766: $repeater_slug
767: ) );
768:
769: if ( ! is_array( $values ) ) {
770: $values = [];
771: }
772:
773: list( $results, $trash ) = $this->get_results( $rows, $repeater_slug, $post_id );
774:
775:
776: foreach ( $trash as $index => $meta ) {
777: papi_delete_property_meta_value( $post_id, $meta->meta_key );
778: }
779:
780: $values = papi_to_property_array_slugs( $values, $repeater_slug );
781:
782: foreach ( $values as $slug => $value ) {
783: if ( papi_is_property_type_key( $slug ) ) {
784: continue;
785: }
786:
787: $property_type_slug = papi_get_property_type_key_f( $slug );
788:
789: if ( ! isset( $values[$property_type_slug] ) ) {
790: continue;
791: }
792:
793:
794: $property_slug = $this->get_child_slug( $repeater_slug, $slug );
795:
796:
797: $property_type_value = $values[$property_type_slug]->type;
798: $property_type = papi_get_property_type( $property_type_value );
799:
800:
801: $value = papi_maybe_json_decode(
802: maybe_unserialize( $value )
803: );
804:
805:
806: $value = $property_type->update_value(
807: $value,
808: $property_slug,
809: $post_id
810: );
811:
812:
813: $values[$slug] = papi_filter_update_value(
814: $property_type_value,
815: $value,
816: $property_slug,
817: $post_id
818: );
819:
820: if ( is_array( $values[$slug] ) ) {
821: foreach ( $values[$slug] as $key => $val ) {
822: if ( ! is_string( $key ) ) {
823: continue;
824: }
825:
826: unset( $values[$slug][$key] );
827: $key = preg_replace( '/^\_/', '', $key );
828: $values[$slug][$key] = $val;
829: }
830: }
831:
832: if ( isset( $values[$property_type_slug] ) ) {
833: unset( $values[$property_type_slug] );
834: }
835: }
836:
837: $trash = array_diff(
838: array_keys( papi_to_array( $results ) ),
839: array_keys( papi_to_array( $values ) )
840: );
841:
842:
843: foreach ( $trash as $trash_key => $trash_value ) {
844: papi_delete_property_meta_value( $post_id, $trash_key );
845: }
846:
847:
848:
849: $this->remove_repeater_rows( $post_id, $repeater_slug );
850:
851: return $values;
852: }
853: }
854: