Completed
Push — master ( 2cfa6f...8927a4 )
by Zack
10:00 queued 06:05
created

includes/class-template.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * GravityView templating engine class
4
 *
5
 * @package   GravityView
6
 * @license   GPL2+
7
 * @author    Katz Web Services, Inc.
8
 * @link      http://gravityview.co
9
 * @copyright Copyright 2014, Katz Web Services, Inc.
10
 *
11
 * @since 1.0.0
12
 */
13
14
/** If this file is called directly, abort. */
15
if ( ! defined( 'ABSPATH' ) ) {
16
	die;
17
}
18
19
if( ! class_exists( 'Gamajo_Template_Loader' ) ) {
20
	require( GRAVITYVIEW_DIR . 'includes/lib/class-gamajo-template-loader.php' );
21
}
22
23
class GravityView_View extends Gamajo_Template_Loader {
24
25
	/**
26
	 * Prefix for filter names.
27
	 * @var string
28
	 */
29
	protected $filter_prefix = 'gravityview';
30
31
	/**
32
	 * Directory name where custom templates for this plugin should be found in the theme.
33
	 * @var string
34
	 */
35
	protected $theme_template_directory = 'gravityview';
36
37
	/**
38
	 * Reference to the root directory path of this plugin.
39
	 * @var string
40
	 */
41
	protected $plugin_directory = GRAVITYVIEW_DIR;
42
43
	/**
44
	 * Store templates locations that have already been located
45
	 * @var array
46
	 */
47
	protected $located_templates = array();
48
49
	/**
50
	 * The name of the template, like "list", "table", or "datatables"
51
	 * @var string
52
	 */
53
	protected $template_part_slug = '';
54
55
	/**
56
	 * The name of the file part, like "body" or "single"
57
	 * @var string
58
	 */
59
	protected $template_part_name = '';
60
61
	/**
62
	 * @var int Gravity Forms form ID
63
	 */
64
	protected $form_id = NULL;
65
66
	/**
67
	 * @var int View ID
68
	 * @todo: this needs to be public until extensions support 1.7+
69
	 */
70
	public $view_id = NULL;
71
72
	/**
73
	 * @var array Fields for the form
74
	 */
75
	protected $fields = array();
76
77
	/**
78
	 * @var string Current screen. Defaults to "directory" or "single"
79
	 */
80
	protected $context = 'directory';
81
82
	/**
83
	 * @var int|null If in embedded post or page, the ID of it
84
	 */
85
	protected $post_id = NULL;
86
87
	/**
88
	 * @var array Gravity Forms form array at ID $form_id
89
	 */
90
	protected $form = NULL;
91
92
	/**
93
	 * @var array Configuration for the View
94
	 */
95
	protected $atts = array();
96
97
	/**
98
	 * @var array Entries for the current result. Single item in array for single entry View
99
	 */
100
	protected $entries = array();
101
102
	/**
103
	 * @var int Total entries count for the current result.
104
	 */
105
	protected $total_entries = 0;
106
107
	/**
108
	 * @var string The label to display back links
109
	 */
110
	protected $back_link_label = '';
111
112
	/**
113
	 * @var array Array with `offset` and `page_size` keys
114
	 */
115
	protected $paging = array();
116
117
	/**
118
	 * @var array Array with `sort_field` and `sort_direction` keys
119
	 */
120
	protected $sorting = array();
121
122
	/**
123
	 * @var bool Whether to hide the results until a search is performed
124
	 * @since 1.5.4
125
	 */
126
	protected $hide_until_searched = false;
127
128
	/**
129
	 * Current entry in the loop
130
	 * @var array
131
	 */
132
	protected $_current_entry = array();
133
134
	/**
135
	 * @var array
136
	 */
137
	protected $_current_field = array();
138
139
	/**
140
	 * @var GravityView_View
141
	 */
142
	static $instance = NULL;
143
144
	/**
145
	 * Construct the view object
146
	 * @param  array       $atts Associative array to set the data of
147
	 */
148
	function __construct( $atts = array() ) {
149
150
		$atts = wp_parse_args( $atts, array(
151
			'form_id' => NULL,
152
			'view_id' => NULL,
153
			'fields'  => NULL,
154
			'context' => NULL,
155
			'post_id' => NULL,
156
			'form'    => NULL,
157
			'atts'	  => NULL,
158
		) );
159
160
		foreach ($atts as $key => $value) {
161
			if( is_null( $value ) ) {
162
				continue;
163
			}
164
			$this->{$key} = $value;
165
		}
166
167
168
		// Add granular overrides
169
		add_filter( $this->filter_prefix . '_get_template_part', array( $this, 'add_id_specific_templates' ), 10, 3 );
170
171
172
		// widget logic
173
		add_action( 'gravityview_before', array( $this, 'render_widget_hooks' ) );
174
		add_action( 'gravityview_after', array( $this, 'render_widget_hooks' ) );
175
176
		/**
177
		 * Clear the current entry after the loop is done
178
		 * @since 1.7.3
179
		 */
180
		add_action( 'gravityview_footer', array( $this, 'clearCurrentEntry' ), 500 );
181
182
		self::$instance = &$this;
183
	}
184
185
	/**
186
	 * @param null $passed_post
187
	 *
188
	 * @return GravityView_View
189
	 */
190
	static function getInstance( $passed_post = NULL ) {
191
192
		if( empty( self::$instance ) ) {
193
			self::$instance = new self( $passed_post );
194
		}
195
196
		return self::$instance;
197
	}
198
199
	/**
200
	 * @param string|null $key The key to a specific attribute of the current field
201
	 * @return array|mixed|null If $key is set and attribute exists at $key, return that. If not set, return NULL. Otherwise, return current field array
202
	 */
203
	public function getCurrentField( $key = NULL ) {
204
205
		if( !empty( $key ) ) {
206
			if( isset( $this->_current_field[ $key ] ) ) {
207
				return $this->_current_field[ $key ];
208
			}
209
			return NULL;
210
		}
211
212
		return $this->_current_field;
213
	}
214
215
	public function setCurrentFieldSetting( $key, $value ) {
216
217
		if( !empty( $this->_current_field ) ) {
218
			$this->_current_field['field_settings'][ $key ] = $value;
219
		}
220
221
	}
222
223
	public function getCurrentFieldSetting( $key ) {
224
		$settings = $this->getCurrentField('field_settings');
225
226
		if( $settings && !empty( $settings[ $key ] ) ) {
227
			return $settings[ $key ];
228
		}
229
230
		return NULL;
231
	}
232
233
	/**
234
	 * @param array $passed_field
235
	 */
236
	public function setCurrentField( $passed_field ) {
237
238
		$existing_field = $this->getCurrentField();
239
240
		$set_field = wp_parse_args( $passed_field, $existing_field );
241
242
		$this->_current_field = $set_field;
243
244
		/**
245
		 * Backward compatibility
246
		 * @deprecated 1.6.2
247
		 */
248
		$this->field_data = $set_field;
249
	}
250
251
	/**
252
	 * @param string|null $key The key to a specific field in the fields array
253
	 * @return array|mixed|null If $key is set and field exists at $key, return that. If not set, return NULL. Otherwise, return array of fields.
254
	 */
255
	public function getAtts( $key = NULL ) {
256
257
		if( !empty( $key ) ) {
258
			if( isset( $this->atts[ $key ] ) ) {
259
				return $this->atts[ $key ];
260
			}
261
			return NULL;
262
		}
263
264
		return $this->atts;
265
	}
266
267
	/**
268
	 * @param array $atts
269
	 */
270
	public function setAtts( $atts ) {
271
		$this->atts = $atts;
272
	}
273
274
	/**
275
	 * @return array
276
	 */
277
	public function getForm() {
278
		return $this->form;
279
	}
280
281
	/**
282
	 * @param array $form
283
	 */
284
	public function setForm( $form ) {
285
		$this->form = $form;
286
	}
287
288
	/**
289
	 * @return int|null
290
	 */
291
	public function getPostId() {
292
		return $this->post_id;
293
	}
294
295
	/**
296
	 * @param int|null $post_id
297
	 */
298
	public function setPostId( $post_id ) {
299
		$this->post_id = $post_id;
300
	}
301
302
	/**
303
	 * @return string
304
	 */
305
	public function getContext() {
306
		return $this->context;
307
	}
308
309
	/**
310
	 * @param string $context
311
	 */
312
	public function setContext( $context ) {
313
		$this->context = $context;
314
	}
315
316
	/**
317
	 * @param string|null $key The key to a specific field in the fields array
318
	 * @return array|mixed|null If $key is set and field exists at $key, return that. If not set, return NULL. Otherwise, return array of fields.
319
	 */
320
	public function getFields( $key = null ) {
321
322
		$fields = empty( $this->fields ) ? NULL : $this->fields;
323
324
		if( $fields && !empty( $key ) ) {
325
			$fields = isset( $fields[ $key ] ) ? $fields[ $key ] : NULL;
326
		}
327
328
		return $fields;
329
	}
330
331
	/**
332
	 * @param array $fields
333
	 */
334
	public function setFields( $fields ) {
335
		$this->fields = $fields;
336
	}
337
338
	/**
339
	 * @param string $key The key to a specific field in the fields array
340
	 * @return array|mixed|null If $key is set and field exists at $key, return that. If not set, return NULL. Otherwise, return array of fields.
341
	 */
342
	public function getField( $key ) {
343
344
		if( !empty( $key ) ) {
345
			if( isset( $this->fields[ $key ] ) ) {
346
				return $this->fields[ $key ];
347
			}
348
		}
349
350
		return NULL;
351
	}
352
353
	/**
354
	 * @param string $key The key to a specific field in the fields array
355
	 * @param mixed $value The value to set for the field
356
	 */
357
	public function setField( $key, $value ) {
358
		$this->fields[ $key ] = $value;
359
	}
360
361
	/**
362
	 * @return int
363
	 */
364
	public function getViewId() {
365
		return absint( $this->view_id );
366
	}
367
368
	/**
369
	 * @param int $view_id
370
	 */
371
	public function setViewId( $view_id ) {
372
		$this->view_id = intval( $view_id );
373
	}
374
375
	/**
376
	 * @return int
377
	 */
378
	public function getFormId() {
379
		return $this->form_id;
380
	}
381
382
	/**
383
	 * @param int $form_id
384
	 */
385
	public function setFormId( $form_id ) {
386
		$this->form_id = $form_id;
387
	}
388
389
	/**
390
	 * @return array
391
	 */
392
	public function getEntries() {
393
		return $this->entries;
394
	}
395
396
	/**
397
	 * @param array $entries
398
	 */
399
	public function setEntries( $entries ) {
400
		$this->entries = $entries;
401
	}
402
403
	/**
404
	 * @return int
405
	 */
406
	public function getTotalEntries() {
407
		return (int)$this->total_entries;
408
	}
409
410
	/**
411
	 * @param int $total_entries
412
	 */
413
	public function setTotalEntries( $total_entries ) {
414
		$this->total_entries = intval( $total_entries );
415
	}
416
417
	/**
418
	 * @return array
419
	 */
420
	public function getPaging() {
421
422
	    $default_params = array(
423
            'offset' => 0,
424
            'page_size' => 20,
425
        );
426
427
		return wp_parse_args( $this->paging, $default_params );
428
	}
429
430
	/**
431
	 * @param array $paging
432
	 */
433
	public function setPaging( $paging ) {
434
		$this->paging = $paging;
435
	}
436
437
	/**
438
	 * Get an array with pagination information
439
	 *
440
	 * @since 1.13
441
	 * 
442
	 * @return array {
443
	 *  @type int $first The starting entry number (counter, not ID)
444
	 *  @type int $last The last displayed entry number (counter, not ID)
445
	 *  @type int $total The total number of entries
446
	 * }
447
	 */
448
	public function getPaginationCounts() {
449
450
		$paging = $this->getPaging();
451
		$offset = $paging['offset'];
452
		$page_size = $paging['page_size'];
453
		$total = $this->getTotalEntries();
454
455
		if ( empty( $total ) ) {
456
			do_action( 'gravityview_log_debug', __METHOD__ . ': No entries. Returning empty array.' );
457
458
			return array();
459
		}
460
461
		$first = empty( $offset ) ? 1 : $offset + 1;
462
463
		// If the page size + starting entry is larger than total, the total is the max.
464
		$last = ( $offset + $page_size > $total ) ? $total : $offset + $page_size;
465
466
		/**
467
		 * @filter `gravityview_pagination_counts` Modify the displayed pagination numbers
468
		 * @since 1.13
469
		 * @param array $counts Array with $first, $last, $total numbers in that order
470
		 */
471
		list( $first, $last, $total ) = apply_filters( 'gravityview_pagination_counts', array( $first, $last, $total ) );
472
473
		return array( 'first' => (int) $first, 'last' => (int) $last, 'total' => (int) $total );
474
	}
475
476
	/**
477
	 * @return array
478
	 */
479
	public function getSorting() {
480
481
		$defaults_params = array(
482
            'sort_field' => 'date_created',
483
            'sort_direction' => 'ASC',
484
            'is_numeric' => false,
485
        );
486
487
		return wp_parse_args( $this->sorting, $defaults_params );
488
	}
489
490
	/**
491
	 * @param array $sorting
492
	 */
493
	public function setSorting( $sorting ) {
494
		$this->sorting = $sorting;
495
	}
496
497
	/**
498
	 * @return string
499
	 */
500
	public function getBackLinkLabel() {
501
502
		$back_link_label = GravityView_API::replace_variables( $this->back_link_label, $this->getForm(), $this->getCurrentEntry() );
503
504
		$back_link_label = do_shortcode( $back_link_label );
505
506
		return $back_link_label;
507
	}
508
509
	/**
510
	 * @param string $back_link_label
511
	 */
512
	public function setBackLinkLabel( $back_link_label ) {
513
		$this->back_link_label = $back_link_label;
514
	}
515
516
	/**
517
	 * @return boolean
518
	 */
519
	public function isHideUntilSearched() {
520
		return $this->hide_until_searched;
521
	}
522
523
	/**
524
	 * @param boolean $hide_until_searched
525
	 */
526
	public function setHideUntilSearched( $hide_until_searched ) {
527
		$this->hide_until_searched = $hide_until_searched;
528
	}
529
530
	/**
531
	 * @return string
532
	 */
533
	public function getTemplatePartSlug() {
534
		return $this->template_part_slug;
535
	}
536
537
	/**
538
	 * @param string $template_part_slug
539
	 */
540
	public function setTemplatePartSlug( $template_part_slug ) {
541
		$this->template_part_slug = $template_part_slug;
542
	}
543
544
	/**
545
	 * @return string
546
	 */
547
	public function getTemplatePartName() {
548
		return $this->template_part_name;
549
	}
550
551
	/**
552
	 * @param string $template_part_name
553
	 */
554
	public function setTemplatePartName( $template_part_name ) {
555
		$this->template_part_name = $template_part_name;
556
	}
557
558
	/**
559
	 * Return the current entry. If in the loop, the current entry. If single entry, the currently viewed entry.
560
	 * @return array
561
	 */
562
	public function getCurrentEntry() {
563
564
		if( in_array( $this->getContext(), array( 'edit', 'single') ) ) {
565
			$entries = $this->getEntries();
566
			$entry = $entries[0];
567
		} else {
568
			$entry = $this->_current_entry;
569
		}
570
571
		/** @since 1.16 Fixes DataTables empty entry issue */
572
		if ( empty( $entry ) && ! empty( $this->_current_field['entry'] ) ) {
573
			$entry = $this->_current_field['entry'];
574
		}
575
576
		return $entry;
577
	}
578
579
	/**
580
	 * @param array $current_entry
581
	 * @return void
582
	 */
583
	public function setCurrentEntry( $current_entry ) {
584
		$this->_current_entry = $current_entry;
585
	}
586
587
	/**
588
	 * Clear the current entry after all entries in the loop have been displayed.
589
	 *
590
	 * @since 1.7.3
591
	 * @return void
592
	 */
593
	public function clearCurrentEntry() {
594
		$this->_current_entry = NULL;
595
	}
596
597
	/**
598
	 * Render an output zone, as configured in the Admin
599
	 *
600
	 * @since 1.16.4 Added $echo parameter
601
	 *
602
	 * @param string $zone The zone name, like 'footer-left'
603
	 * @param array $atts
604
	 * @param bool $echo Whether to print the output
605
	 *
606
	 * @return string|null
607
	 */
608
	public function renderZone( $zone = '', $atts = array(), $echo = true ) {
609
610
		if( empty( $zone ) ) {
611
			do_action('gravityview_log_error', 'GravityView_View[renderZone] No zone defined.');
612
			return NULL;
613
		}
614
615
		$defaults = array(
616
			'slug' => $this->getTemplatePartSlug(),
617
			'context' => $this->getContext(),
618
			'entry' => $this->getCurrentEntry(),
619
			'form' => $this->getForm(),
620
			'hide_empty' => $this->getAtts('hide_empty'),
621
		);
622
623
		$final_atts = wp_parse_args( $atts, $defaults );
624
625
		$output = '';
626
627
		$final_atts['zone_id'] = "{$final_atts['context']}_{$final_atts['slug']}-{$zone}";
628
629
		$fields = $this->getField( $final_atts['zone_id'] );
630
631
		// Backward compatibility
632
		if( 'table' === $this->getTemplatePartSlug() ) {
633
			/**
634
			 * @filter `gravityview_table_cells` Modify the fields displayed in a table
635
			 * @param array $fields
636
			 * @param GravityView_View $this
637
			 */
638
			$fields = apply_filters("gravityview_table_cells", $fields, $this );
639
		}
640
641
		if( empty( $fields ) ) {
642
			return NULL;
643
		}
644
645
		$field_output = '';
646
		foreach ( $fields as $field ) {
647
			$final_atts['field'] = $field;
648
649
			$field_output .= gravityview_field_output( $final_atts );
650
		}
651
652
		/**
653
		 * If a zone has no field output, choose whether to show wrapper
654
		 * False by default to keep backward compatibility
655
		 * @since 1.7.6
656
		 * @param boolean $hide_empty_zone Default: false
657
		 */
658
		if( empty( $field_output ) && apply_filters( 'gravityview/render/hide-empty-zone', false ) ) {
659
			return NULL;
0 ignored issues
show
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
660
		}
661
662
		if( !empty( $final_atts['wrapper_class'] ) ) {
663
			$output .= '<div class="'.gravityview_sanitize_html_class( $final_atts['wrapper_class'] ).'">';
664
		}
665
666
		$output .= $field_output;
667
668
		if( !empty( $final_atts['wrapper_class'] ) ) {
669
			$output .= '</div>';
670
		}
671
672
		if( $echo ) {
673
			echo $output;
674
		}
675
676
		return $output;
677
	}
678
679
	/**
680
	 * In order to improve lookup times, we store located templates in a local array.
681
	 *
682
	 * This improves performance by up to 1/2 second on a 250 entry View with 7 columns showing
683
	 *
684
	 * @inheritdoc
685
	 * @see Gamajo_Template_Loader::locate_template()
686
	 * @return null|string NULL: Template not found; String: path to template
687
	 */
688
	function locate_template( $template_names, $load = false, $require_once = true ) {
689
690
		if( is_string( $template_names ) && isset( $this->located_templates[ $template_names ] ) ) {
691
692
			$located = $this->located_templates[ $template_names ];
693
694
		} else {
695
696
			// Set $load to always false so we handle it here.
697
			$located = parent::locate_template( $template_names, false, $require_once );
698
699
			if( is_string( $template_names ) ) {
700
				$this->located_templates[ $template_names ] = $located;
701
			}
702
		}
703
704
		if ( $load && $located ) {
705
			load_template( $located, $require_once );
706
		}
707
708
		return $located;
709
	}
710
711
	/**
712
	 * Magic Method: Instead of throwing an error when a variable isn't set, return null.
713
	 * @param  string      $name Key for the data retrieval.
714
	 * @return mixed|null    The stored data.
715
	 */
716
	public function __get( $name ) {
717
		if( isset( $this->{$name} ) ) {
718
			return $this->{$name};
719
		} else {
720
			return NULL;
721
		}
722
	}
723
724
	/**
725
	 * Enable overrides of GravityView templates on a granular basis
726
	 *
727
	 * The loading order is:
728
	 *
729
	 * - view-[View ID]-table-footer.php
730
	 * - form-[Form ID]-table-footer.php
731
	 * - page-[ID of post or page where view is embedded]-table-footer.php
732
	 * - table-footer.php
733
	 *
734
	 * @see  Gamajo_Template_Loader::get_template_file_names() Where the filter is
735
	 * @param array $templates Existing list of templates.
736
	 * @param string $slug      Name of the template base, example: `table`, `list`, `datatables`, `map`
737
	 * @param string $name      Name of the template part, example: `body`, `footer`, `head`, `single`
738
	 *
739
	 * @return array $templates Modified template array, merged with existing $templates values
740
	 */
741
	function add_id_specific_templates( $templates, $slug, $name ) {
742
743
		$additional = array();
744
745
		// form-19-table-body.php
746
		$additional[] = sprintf( 'form-%d-%s-%s.php', $this->getFormId(), $slug, $name );
747
748
		// view-3-table-body.php
749
		$additional[] = sprintf( 'view-%d-%s-%s.php', $this->getViewId(), $slug, $name );
750
751
		if( $this->getPostId() ) {
752
753
			// page-19-table-body.php
754
			$additional[] = sprintf( 'page-%d-%s-%s.php', $this->getPostId(), $slug, $name );
755
		}
756
757
		// Combine with existing table-body.php and table.php
758
		$templates = array_merge( $additional, $templates );
759
760
		do_action( 'gravityview_log_debug', '[add_id_specific_templates] List of Template Files', $templates );
761
762
		return $templates;
763
	}
764
765
	// Load the template
766
	public function render( $slug, $name, $require_once = true ) {
767
768
		$this->setTemplatePartSlug( $slug );
769
770
		$this->setTemplatePartName( $name );
771
		
772
		$template_file = $this->get_template_part( $slug, $name, false );
773
774
		do_action( 'gravityview_log_debug', '[render] Rendering Template File', $template_file );
775
776
		if( !empty( $template_file) ) {
777
778
			if ( $require_once ) {
779
				require_once( $template_file );
780
			} else {
781
				require( $template_file );
782
			}
783
784
		}
785
	}
786
787
	/**
788
	 *
789
	 * @param $view_id
790
	 */
791
	public function render_widget_hooks( $view_id ) {
792
793
		if( empty( $view_id ) || 'single' == gravityview_get_context() ) {
794
			do_action( 'gravityview_log_debug', __METHOD__ . ' - Not rendering widgets; single entry' );
795
			return;
796
		}
797
798
		$view_data = gravityview_get_current_view_data( $view_id );
799
800
		// get View widget configuration
801
		$widgets = (array)$view_data['widgets'];
802
803
		switch( current_filter() ) {
804
			default:
805
			case 'gravityview_before':
806
				$zone = 'header';
807
				break;
808
			case 'gravityview_after':
809
				$zone = 'footer';
810
				break;
811
		}
812
813
		/**
814
		 * Filter widgets not in the current zone
815
		 * @since 1.16
816
		 */
817
		foreach( $widgets as $key => $widget ) {
818
			// The widget isn't in the current zone
819
			if( false === strpos( $key, $zone ) ) {
820
				unset( $widgets[ $key ] );
821
			}
822
		}
823
824
		/**
825
		 * Prevent output if no widgets to show.
826
		 * @since 1.16
827
		 */
828
		if ( empty( $widgets ) ) {
829
			do_action( 'gravityview_log_debug', sprintf( 'No widgets for View #%s', $view_id ) );
830
			return;
831
		}
832
833
		// Prevent being called twice
834
		if( did_action( $zone.'_'.$view_id.'_widgets' ) ) {
835
			do_action( 'gravityview_log_debug', sprintf( '%s - Not rendering %s; already rendered', __METHOD__ , $zone.'_'.$view_id.'_widgets' ) );
836
			return;
837
		}
838
839
		$rows = GravityView_Plugin::get_default_widget_areas();
840
841
		// TODO: Move to sep. method, use an action instead
842
		wp_enqueue_style( 'gravityview_default_style' );
843
844
		$default_css_class = 'gv-grid gv-widgets-' . $zone;
845
846
		if( 0 === GravityView_View::getInstance()->getTotalEntries() ) {
847
			$default_css_class .= ' gv-widgets-no-results';
848
		}
849
850
		/**
851
		 * @filter `gravityview/widgets/wrapper_css_class` The CSS class applied to the widget container `<div>`.
852
		 * @since 1.16.2
853
		 * @param string $css_class Default: `gv-grid gv-widgets-{zone}` where `{zone}` is replaced by the current `$zone` value. If the View has no results, adds ` gv-widgets-no-results`
854
		 * @param string $zone Current widget zone, either `header` or `footer`
855
		 * @param array $widgets Array of widget configurations for the current zone, as set by `gravityview_get_current_view_data()['widgets']`
856
		 */
857
		$css_class = apply_filters('gravityview/widgets/wrapper_css_class', $default_css_class, $zone, $widgets );
858
859
		$css_class = gravityview_sanitize_html_class( $css_class );
860
861
		// TODO Convert to partials
862
		?>
863
		<div class="<?php echo $css_class; ?>">
864
			<?php
865
			foreach( $rows as $row ) {
866
				foreach( $row as $col => $areas ) {
867
					$column = ($col == '2-2') ? '1-2 gv-right' : $col.' gv-left';
868
				?>
869
					<div class="gv-grid-col-<?php echo esc_attr( $column ); ?>">
870
						<?php
871
						if( !empty( $areas ) ) {
872
							foreach( $areas as $area ) {
873
								if( !empty( $widgets[ $zone .'_'. $area['areaid'] ] ) ) {
874
									foreach( $widgets[ $zone .'_'. $area['areaid'] ] as $widget ) {
875
										do_action( "gravityview_render_widget_{$widget['id']}", $widget );
876
									}
877
								}
878
							}
879
						} ?>
880
					</div>
881
				<?php } // $row ?>
882
			<?php } // $rows ?>
883
		</div>
884
885
		<?php
886
887
		/**
888
		 * Prevent widgets from being called twice.
889
		 * Checking for loop_start prevents themes and plugins that pre-process shortcodes from triggering the action before displaying. Like, ahem, the Divi theme and WordPress SEO plugin
890
		 */
891
		if( did_action( 'loop_start' ) ) {
892
			do_action( $zone.'_'.$view_id.'_widgets' );
893
		}
894
	}
895
896
}
897
898