Merge pull request #12368 from totten/master-cache-schema
[civicrm-core.git] / Civi / Angular / AngularLoader.php
CommitLineData
b20ea913
TO
1<?php
2namespace Civi\Angular;
3
4/**
5 * The AngularLoader loads any JS/CSS/JSON resources
6 * required for setting up AngularJS.
7 *
8 * The AngularLoader stops short of bootstrapping AngularJS. You may
9 * need to `<div ng-app="..."></div>` or `angular.bootstrap(...)`.
10 *
11 * @code
12 * $loader = new AngularLoader();
13 * $loader->setPageName('civicrm/case/a');
14 * $loader->setModules(array('crmApp'));
15 * $loader->load();
16 * @endCode
17 *
18 * @link https://docs.angularjs.org/guide/bootstrap
19 */
20class AngularLoader {
21
22 /**
23 * The weight to assign to any Angular JS module files.
24 */
25 const DEFAULT_MODULE_WEIGHT = 200;
26
27 /**
28 * The resource manager.
29 *
30 * Do not use publicly. Inject your own copy!
31 *
32 * @var \CRM_Core_Resources
33 */
34 protected $res;
35
36 /**
37 * The Angular module manager.
38 *
39 * Do not use publicly. Inject your own copy!
40 *
41 * @var \Civi\Angular\Manager
42 */
43 protected $angular;
44
45 /**
46 * The region of the page into which JavaScript will be loaded.
47 *
48 * @var string
49 */
50 protected $region;
51
52 /**
53 * @var string
54 * Ex: 'civicrm/a'.
55 */
56 protected $pageName;
57
58 /**
59 * @var array
60 * A list of modules to load.
61 */
62 protected $modules;
63
3e8823e3
TO
64 /**
65 * @var array|NULL
66 */
67 protected $crmApp = NULL;
68
b20ea913
TO
69 /**
70 * AngularLoader constructor.
71 */
72 public function __construct() {
73 $this->res = \CRM_Core_Resources::singleton();
74 $this->angular = \Civi::service('angular');
75 $this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
76 $this->pageName = isset($_GET['q']) ? $_GET['q'] : NULL;
77 $this->modules = array();
78 }
79
80 /**
81 * Register resources required by Angular.
cba659e7
TO
82 *
83 * @return AngularLoader
b20ea913
TO
84 */
85 public function load() {
86 $angular = $this->getAngular();
87 $res = $this->getRes();
88
3e8823e3
TO
89 if ($this->crmApp !== NULL) {
90 $this->addModules($this->crmApp['modules']);
91 $region = \CRM_Core_Region::instance($this->crmApp['region']);
92 $region->update('default', array('disabled' => TRUE));
93 $region->add(array('template' => $this->crmApp['file'], 'weight' => 0));
94 $this->res->addSetting(array(
95 'crmApp' => array(
96 'defaultRoute' => $this->crmApp['defaultRoute'],
97 ),
98 ));
99
100 // If trying to load an Angular page via AJAX, the route must be passed as a
101 // URL parameter, since the server doesn't receive information about
102 // URL fragments (i.e, what comes after the #).
103 $this->res->addSetting(array(
104 'angularRoute' => $this->crmApp['activeRoute'],
105 ));
106 }
107
b20ea913
TO
108 $moduleNames = $this->findActiveModules();
109 if (!$this->isAllModules($moduleNames)) {
110 $assetParams = array('modules' => implode(',', $moduleNames));
111 }
112 else {
113 // The module list will be "all modules that the user can see".
114 $assetParams = array('nonce' => md5(implode(',', $moduleNames)));
115 }
116
117 $res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) {
118 // TODO optimization; client-side caching
119 $result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), array(
120 'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
121 'angular' => array(
122 'modules' => $moduleNames,
123 'requires' => $angular->getResources($moduleNames, 'requires', 'requires'),
124 'cacheCode' => $res->getCacheCode(),
125 'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
126 ),
127 ));
128 return $result;
129 });
130
131 $res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE);
132 $res->addScriptFile('civicrm', 'js/crm.angular.js', 101, $this->getRegion(), FALSE);
133
134 $headOffset = 0;
135 $config = \CRM_Core_Config::singleton();
136 if ($config->debug) {
137 foreach ($moduleNames as $moduleName) {
138 foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) {
139 $res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
140 }
141 foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) {
142 $res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
143 // addScriptUrl() bypasses the normal string-localization of addScriptFile(),
144 // but that's OK because all Angular strings (JS+HTML) will load via crmResource.
145 }
146 }
147 }
148 else {
149 // Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(),
150 // but that's OK because all Angular strings (JS+HTML) will load via crmResource.
151 // $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
152 $aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams);
153 $res->addScriptUrl($aggScriptUrl, 120, $this->getRegion());
154
155 // FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons.
156 //$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
157 //$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams);
158 //$res->addStyleUrl($aggStyleUrl, 120, $this->getRegion());
159
160 foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) {
161 $res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
162 }
163 }
cba659e7
TO
164
165 return $this;
b20ea913
TO
166 }
167
3e8823e3
TO
168 /**
169 * Use Civi's generic "application" module.
170 *
171 * This is suitable for use on a basic, standalone Angular page
172 * like `civicrm/a`. (If you need to integrate Angular with pre-existing,
173 * non-Angular pages... then this probably won't help.)
174 *
175 * The Angular bootstrap process requires an HTML directive like
176 * `<div ng-app="foo">`.
177 *
178 * Calling useApp() will replace the page's main body with the
179 * `<div ng-app="crmApp">...</div>` and apply some configuration options
180 * for the `crmApp` module.
181 *
182 * @param array $settings
183 * A list of settings. Accepted values:
184 * - activeRoute: string, the route to open up immediately
23a258ce
TO
185 * Ex: '/case/list'
186 * - defaultRoute: string, use this to redirect the default route (`/`) to another page
187 * Ex: '/case/list'
3e8823e3 188 * - region: string, the place on the page where we should insert the angular app
23a258ce 189 * Ex: 'page-body'
3e8823e3
TO
190 * @return AngularLoader
191 * @link https://code.angularjs.org/1.5.11/docs/guide/bootstrap
192 */
193 public function useApp($settings = array()) {
194 $defaults = array(
195 'modules' => array('crmApp'),
196 'activeRoute' => NULL,
197 'defaultRoute' => NULL,
198 'region' => 'page-body',
199 'file' => 'Civi/Angular/Page/Main.tpl',
200 );
201 $this->crmApp = array_merge($defaults, $settings);
202 return $this;
203 }
204
b20ea913
TO
205 /**
206 * Get a list of all Angular modules which should be activated on this
207 * page.
208 *
209 * @return array
210 * List of module names.
211 * Ex: array('angularFileUpload', 'crmUi', 'crmUtil').
212 */
213 public function findActiveModules() {
214 return $this->angular->resolveDependencies(array_merge(
215 $this->getModules(),
216 $this->angular->resolveDefaultModules($this->getPageName())
217 ));
218 }
219
220 /**
221 * @param $moduleNames
222 * @return int
223 */
224 private function isAllModules($moduleNames) {
225 $allModuleNames = array_keys($this->angular->getModules());
226 return count(array_diff($allModuleNames, $moduleNames)) === 0;
227 }
228
229 /**
230 * @return \CRM_Core_Resources
231 */
232 public function getRes() {
233 return $this->res;
234 }
235
236 /**
237 * @param \CRM_Core_Resources $res
cba659e7 238 * @return AngularLoader
b20ea913
TO
239 */
240 public function setRes($res) {
241 $this->res = $res;
cba659e7 242 return $this;
b20ea913
TO
243 }
244
245 /**
246 * @return \Civi\Angular\Manager
247 */
248 public function getAngular() {
249 return $this->angular;
250 }
251
252 /**
253 * @param \Civi\Angular\Manager $angular
cba659e7 254 * @return AngularLoader
b20ea913
TO
255 */
256 public function setAngular($angular) {
257 $this->angular = $angular;
cba659e7 258 return $this;
b20ea913
TO
259 }
260
261 /**
262 * @return string
263 */
264 public function getRegion() {
265 return $this->region;
266 }
267
268 /**
269 * @param string $region
cba659e7 270 * @return AngularLoader
b20ea913
TO
271 */
272 public function setRegion($region) {
273 $this->region = $region;
cba659e7 274 return $this;
b20ea913
TO
275 }
276
277 /**
278 * @return string
279 * Ex: 'civicrm/a'.
280 */
281 public function getPageName() {
282 return $this->pageName;
283 }
284
285 /**
286 * @param string $pageName
287 * Ex: 'civicrm/a'.
cba659e7 288 * @return AngularLoader
b20ea913
TO
289 */
290 public function setPageName($pageName) {
291 $this->pageName = $pageName;
cba659e7 292 return $this;
b20ea913
TO
293 }
294
d2f38ec9
TO
295 /**
296 * @param array|string $modules
297 * @return AngularLoader
298 */
299 public function addModules($modules) {
300 $modules = (array) $modules;
301 $this->modules = array_unique(array_merge($this->modules, $modules));
302 return $this;
303 }
304
b20ea913
TO
305 /**
306 * @return array
307 */
308 public function getModules() {
309 return $this->modules;
310 }
311
312 /**
313 * @param array $modules
cba659e7 314 * @return AngularLoader
b20ea913
TO
315 */
316 public function setModules($modules) {
317 $this->modules = $modules;
cba659e7 318 return $this;
b20ea913
TO
319 }
320
321}