Merge pull request #18334 from civicrm/5.29
[civicrm-core.git] / CRM / Core / Region.php
1 <?php
2
3 /**
4 * Maintain a set of markup/templates to inject inside various regions
5 */
6 class CRM_Core_Region {
7 static private $_instances = NULL;
8
9 /**
10 * Obtain the content for a given region.
11 *
12 * @param string $name
13 * @param bool $autocreate
14 * Whether to automatically create an empty region.
15 * @return CRM_Core_Region
16 */
17 public static function &instance($name, $autocreate = TRUE) {
18 if ($autocreate && !isset(self::$_instances[$name])) {
19 self::$_instances[$name] = new CRM_Core_Region($name);
20 }
21 return self::$_instances[$name];
22 }
23
24 /**
25 * Symbolic name of this region
26 *
27 * @var string
28 */
29 public $_name;
30
31 /**
32 * List of snippets to inject within region.
33 *
34 * e.g. $this->_snippets[3]['type'] = 'template';
35 *
36 * @var array
37 */
38 public $_snippets;
39
40 /**
41 * Whether the snippets array has been sorted
42 *
43 * @var bool
44 */
45 public $_isSorted;
46
47 /**
48 * @param string $name
49 */
50 public function __construct($name) {
51 $this->_name = $name;
52 $this->_snippets = [];
53
54 // Placeholder which represents any of the default content generated by the main Smarty template
55 $this->add([
56 'name' => 'default',
57 'type' => 'markup',
58 'markup' => '',
59 'weight' => 0,
60 ]);
61 $this->_isSorted = TRUE;
62 }
63
64 /**
65 * Add a snippet of content to a region.
66 *
67 * ```
68 * CRM_Core_Region::instance('page-header')->add(array(
69 * 'markup' => '<div style="color:red">Hello!</div>',
70 * ));
71 * CRM_Core_Region::instance('page-header')->add(array(
72 * 'script' => 'alert("Hello");',
73 * ));
74 * CRM_Core_Region::instance('page-header')->add(array(
75 * 'template' => 'CRM/Myextension/Extra.tpl',
76 * ));
77 * CRM_Core_Region::instance('page-header')->add(array(
78 * 'callback' => 'myextension_callback_function',
79 * ));
80 * ```
81 *
82 * Note: This function does not perform any extra encoding of markup, script code, or etc. If
83 * you're passing in user-data, you must clean it yourself.
84 *
85 * @param array $snippet
86 * Array; keys:.
87 * - type: string (auto-detected for markup, template, callback, script, scriptUrl, jquery, style, styleUrl)
88 * - name: string, optional
89 * - weight: int, optional; default=1
90 * - disabled: int, optional; default=0
91 * - markup: string, HTML; required (for type==markup)
92 * - template: string, path; required (for type==template)
93 * - callback: mixed; required (for type==callback)
94 * - arguments: array, optional (for type==callback)
95 * - script: string, Javascript code
96 * - scriptUrl: string, URL of a Javascript file
97 * - jquery: string, Javascript code which runs inside a jQuery(function($){...}); block
98 * - style: string, CSS code
99 * - styleUrl: string, URL of a CSS file
100 *
101 * @return array
102 */
103 public function add($snippet) {
104 static $types = ['markup', 'template', 'callback', 'scriptUrl', 'script', 'jquery', 'style', 'styleUrl'];
105 $defaults = [
106 'region' => $this->_name,
107 'weight' => 1,
108 'disabled' => FALSE,
109 ];
110 $snippet += $defaults;
111 if (!isset($snippet['type'])) {
112 foreach ($types as $type) {
113 // auto-detect
114 if (isset($snippet[$type])) {
115 $snippet['type'] = $type;
116 break;
117 }
118 }
119 }
120 if (!isset($snippet['name'])) {
121 $snippet['name'] = count($this->_snippets);
122 }
123
124 $this->_snippets[$snippet['name']] = $snippet;
125 $this->_isSorted = FALSE;
126 return $snippet;
127 }
128
129 /**
130 * @param string $name
131 * @param $snippet
132 */
133 public function update($name, $snippet) {
134 $this->_snippets[$name] = array_merge($this->_snippets[$name], $snippet);
135 $this->_isSorted = FALSE;
136 }
137
138 /**
139 * Get snippet.
140 *
141 * @param string $name
142 *
143 * @return mixed
144 */
145 public function get($name) {
146 return !empty($this->_snippets[$name]) ? $this->_snippets[$name] : NULL;
147 }
148
149 /**
150 * Render all the snippets in a region.
151 *
152 * @param string $default
153 * HTML, the initial content of the region.
154 * @param bool $allowCmsOverride
155 * Allow CMS to override rendering of region.
156 * @return string, HTML
157 */
158 public function render($default, $allowCmsOverride = TRUE) {
159 // $default is just another part of the region
160 if (is_array($this->_snippets['default'])) {
161 $this->_snippets['default']['markup'] = $default;
162 }
163 // We hand as much of the work off to the CMS as possible
164 $cms = CRM_Core_Config::singleton()->userSystem;
165
166 if (!$this->_isSorted) {
167 uasort($this->_snippets, ['CRM_Core_Region', '_cmpSnippet']);
168 $this->_isSorted = TRUE;
169 }
170
171 $smarty = CRM_Core_Smarty::singleton();
172 $html = '';
173 foreach ($this->_snippets as $snippet) {
174 if ($snippet['disabled']) {
175 continue;
176 }
177 switch ($snippet['type']) {
178 case 'markup':
179 $html .= $snippet['markup'];
180 break;
181
182 case 'template':
183 $tmp = $smarty->get_template_vars('snippet');
184 $smarty->assign('snippet', $snippet);
185 $html .= $smarty->fetch($snippet['template']);
186 $smarty->assign('snippet', $tmp);
187 break;
188
189 case 'callback':
190 $args = $snippet['arguments'] ?? array(&$snippet, &$html);
191 $html .= call_user_func_array($snippet['callback'], $args);
192 break;
193
194 case 'scriptUrl':
195 if (!$allowCmsOverride || !$cms->addScriptUrl($snippet['scriptUrl'], $this->_name)) {
196 $html .= sprintf("<script type=\"text/javascript\" src=\"%s\">\n</script>\n", $snippet['scriptUrl']);
197 }
198 break;
199
200 case 'jquery':
201 $snippet['script'] = sprintf("CRM.\$(function(\$) {\n%s\n});", $snippet['jquery']);
202 // no break - continue processing as script
203 case 'script':
204 if (!$allowCmsOverride || !$cms->addScript($snippet['script'], $this->_name)) {
205 $html .= sprintf("<script type=\"text/javascript\">\n%s\n</script>\n", $snippet['script']);
206 }
207 break;
208
209 case 'styleUrl':
210 if (!$allowCmsOverride || !$cms->addStyleUrl($snippet['styleUrl'], $this->_name)) {
211 $html .= sprintf("<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\"/>\n", $snippet['styleUrl']);
212 }
213 break;
214
215 case 'style':
216 if (!$allowCmsOverride || !$cms->addStyle($snippet['style'], $this->_name)) {
217 $html .= sprintf("<style type=\"text/css\">\n%s\n</style>\n", $snippet['style']);
218 }
219 break;
220
221 default:
222 throw new CRM_Core_Exception(ts('Snippet type %1 is unrecognized',
223 [1 => $snippet['type']]));
224 }
225 }
226 return $html;
227 }
228
229 /**
230 * @param $a
231 * @param $b
232 *
233 * @return int
234 */
235 public static function _cmpSnippet($a, $b) {
236 if ($a['weight'] < $b['weight']) {
237 return -1;
238 }
239 if ($a['weight'] > $b['weight']) {
240 return 1;
241 }
242 // fallback to name sort; don't really want to do this, but it makes results more stable
243 if ($a['name'] < $b['name']) {
244 return -1;
245 }
246 if ($a['name'] > $b['name']) {
247 return 1;
248 }
249 return 0;
250 }
251
252 }