Commit | Line | Data |
---|---|---|
b20ea913 TO |
1 | <?php |
2 | namespace Civi\Angular; | |
3 | ||
4 | /** | |
5 | * The AngularLoader loads any JS/CSS/JSON resources | |
6 | * required for setting up AngularJS. | |
7 | * | |
f5c157f5 | 8 | * This class is returned by 'angularjs.loader' service. Example use: |
b20ea913 | 9 | * |
0b882a86 | 10 | * ``` |
f5c157f5 CW |
11 | * Civi::service('angularjs.loader') |
12 | * ->addModules('moduleFoo') | |
13 | * ->useApp(); // Optional, if Civi's routing is desired (full-page apps only) | |
0b882a86 | 14 | * ``` |
b20ea913 TO |
15 | * |
16 | * @link https://docs.angularjs.org/guide/bootstrap | |
17 | */ | |
18 | class AngularLoader { | |
19 | ||
20 | /** | |
21 | * The weight to assign to any Angular JS module files. | |
22 | */ | |
23 | const DEFAULT_MODULE_WEIGHT = 200; | |
24 | ||
25 | /** | |
26 | * The resource manager. | |
27 | * | |
28 | * Do not use publicly. Inject your own copy! | |
29 | * | |
30 | * @var \CRM_Core_Resources | |
31 | */ | |
32 | protected $res; | |
33 | ||
34 | /** | |
35 | * The Angular module manager. | |
36 | * | |
37 | * Do not use publicly. Inject your own copy! | |
38 | * | |
39 | * @var \Civi\Angular\Manager | |
40 | */ | |
41 | protected $angular; | |
42 | ||
43 | /** | |
44 | * The region of the page into which JavaScript will be loaded. | |
45 | * | |
46 | * @var string | |
47 | */ | |
48 | protected $region; | |
49 | ||
50 | /** | |
51 | * @var string | |
52 | * Ex: 'civicrm/a'. | |
53 | */ | |
54 | protected $pageName; | |
55 | ||
56 | /** | |
57 | * @var array | |
58 | * A list of modules to load. | |
59 | */ | |
60 | protected $modules; | |
61 | ||
3e8823e3 | 62 | /** |
cc101011 | 63 | * @var array|null |
3e8823e3 TO |
64 | */ |
65 | protected $crmApp = NULL; | |
66 | ||
b20ea913 TO |
67 | /** |
68 | * AngularLoader constructor. | |
69 | */ | |
70 | public function __construct() { | |
71 | $this->res = \CRM_Core_Resources::singleton(); | |
72 | $this->angular = \Civi::service('angular'); | |
73 | $this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header'; | |
292054ac | 74 | $this->pageName = \CRM_Utils_System::currentPath(); |
c64f69d9 | 75 | $this->modules = []; |
b20ea913 TO |
76 | } |
77 | ||
78 | /** | |
9bd30577 | 79 | * Calling this method from outside this class is deprecated. |
cba659e7 | 80 | * |
f5c157f5 | 81 | * Use the `angularjs.loader` service instead. |
9bd30577 | 82 | * |
9bd30577 | 83 | * @deprecated |
f5c157f5 | 84 | * @return $this |
b20ea913 TO |
85 | */ |
86 | public function load() { | |
f5c157f5 CW |
87 | \CRM_Core_Error::deprecatedFunctionWarning('angularjs.loader service'); |
88 | return $this->loadAngularResources(); | |
89 | } | |
90 | ||
91 | /** | |
92 | * Load scripts, styles & settings for the active modules. | |
93 | * | |
94 | * @return $this | |
95 | * @throws \CRM_Core_Exception | |
96 | */ | |
97 | private function loadAngularResources() { | |
b20ea913 TO |
98 | $angular = $this->getAngular(); |
99 | $res = $this->getRes(); | |
100 | ||
3e8823e3 TO |
101 | if ($this->crmApp !== NULL) { |
102 | $this->addModules($this->crmApp['modules']); | |
9bd30577 | 103 | |
c64f69d9 CW |
104 | $this->res->addSetting([ |
105 | 'crmApp' => [ | |
3e8823e3 | 106 | 'defaultRoute' => $this->crmApp['defaultRoute'], |
c64f69d9 CW |
107 | ], |
108 | ]); | |
3e8823e3 TO |
109 | |
110 | // If trying to load an Angular page via AJAX, the route must be passed as a | |
111 | // URL parameter, since the server doesn't receive information about | |
112 | // URL fragments (i.e, what comes after the #). | |
c64f69d9 | 113 | $this->res->addSetting([ |
3e8823e3 | 114 | 'angularRoute' => $this->crmApp['activeRoute'], |
c64f69d9 | 115 | ]); |
3e8823e3 TO |
116 | } |
117 | ||
b20ea913 TO |
118 | $moduleNames = $this->findActiveModules(); |
119 | if (!$this->isAllModules($moduleNames)) { | |
c64f69d9 | 120 | $assetParams = ['modules' => implode(',', $moduleNames)]; |
b20ea913 TO |
121 | } |
122 | else { | |
123 | // The module list will be "all modules that the user can see". | |
c64f69d9 | 124 | $assetParams = ['nonce' => md5(implode(',', $moduleNames))]; |
b20ea913 TO |
125 | } |
126 | ||
127 | $res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) { | |
aa882f9b CW |
128 | // Merge static settings with the results of settingsFactory functions |
129 | $settingsByModule = $angular->getResources($moduleNames, 'settings', 'settings'); | |
130 | foreach ($angular->getResources($moduleNames, 'settingsFactory', 'settingsFactory') as $moduleName => $factory) { | |
131 | $settingsByModule[$moduleName] = array_merge($settingsByModule[$moduleName] ?? [], $factory()); | |
132 | } | |
66c46618 CW |
133 | // Add clientside permissions |
134 | $permissions = []; | |
135 | $toCheck = $angular->getResources($moduleNames, 'permissions', 'permissions'); | |
136 | foreach ($toCheck as $perms) { | |
137 | foreach ((array) $perms as $perm) { | |
138 | if (!isset($permissions[$perm])) { | |
139 | $permissions[$perm] = \CRM_Core_Permission::check($perm); | |
140 | } | |
141 | } | |
142 | } | |
b20ea913 | 143 | // TODO optimization; client-side caching |
66c46618 | 144 | return array_merge($settingsByModule, ['permissions' => $permissions], [ |
b20ea913 | 145 | 'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(), |
c64f69d9 | 146 | 'angular' => [ |
b20ea913 TO |
147 | 'modules' => $moduleNames, |
148 | 'requires' => $angular->getResources($moduleNames, 'requires', 'requires'), | |
149 | 'cacheCode' => $res->getCacheCode(), | |
150 | 'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams), | |
c64f69d9 CW |
151 | ], |
152 | ]); | |
b20ea913 TO |
153 | }); |
154 | ||
155 | $res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE); | |
b20ea913 TO |
156 | |
157 | $headOffset = 0; | |
158 | $config = \CRM_Core_Config::singleton(); | |
159 | if ($config->debug) { | |
1daa5384 | 160 | // FIXME: The `resetLocationProviderHashPrefix.js` has to stay in sync with `\Civi\Angular\Page\Modules::buildAngularModules()`. |
6e78c430 | 161 | $res->addScriptFile('civicrm', 'ang/resetLocationProviderHashPrefix.js', 101, $this->getRegion(), FALSE); |
b20ea913 TO |
162 | foreach ($moduleNames as $moduleName) { |
163 | foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) { | |
164 | $res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion()); | |
165 | } | |
166 | foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) { | |
167 | $res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion()); | |
168 | // addScriptUrl() bypasses the normal string-localization of addScriptFile(), | |
169 | // but that's OK because all Angular strings (JS+HTML) will load via crmResource. | |
170 | } | |
171 | } | |
172 | } | |
173 | else { | |
174 | // Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(), | |
175 | // but that's OK because all Angular strings (JS+HTML) will load via crmResource. | |
176 | // $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $res->getCacheCode(), FALSE, NULL, FALSE); | |
177 | $aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams); | |
178 | $res->addScriptUrl($aggScriptUrl, 120, $this->getRegion()); | |
179 | ||
180 | // FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons. | |
181 | //$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $res->getCacheCode(), FALSE, NULL, FALSE); | |
182 | //$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams); | |
183 | //$res->addStyleUrl($aggStyleUrl, 120, $this->getRegion()); | |
184 | ||
185 | foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) { | |
186 | $res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion()); | |
187 | } | |
188 | } | |
d67ff852 CW |
189 | // Add bundles |
190 | foreach ($this->angular->getResources($moduleNames, 'bundles', 'bundles') as $bundles) { | |
191 | $res->addBundle($bundles); | |
192 | } | |
cba659e7 TO |
193 | |
194 | return $this; | |
b20ea913 TO |
195 | } |
196 | ||
3e8823e3 TO |
197 | /** |
198 | * Use Civi's generic "application" module. | |
199 | * | |
200 | * This is suitable for use on a basic, standalone Angular page | |
201 | * like `civicrm/a`. (If you need to integrate Angular with pre-existing, | |
202 | * non-Angular pages... then this probably won't help.) | |
203 | * | |
204 | * The Angular bootstrap process requires an HTML directive like | |
205 | * `<div ng-app="foo">`. | |
206 | * | |
207 | * Calling useApp() will replace the page's main body with the | |
208 | * `<div ng-app="crmApp">...</div>` and apply some configuration options | |
209 | * for the `crmApp` module. | |
210 | * | |
211 | * @param array $settings | |
212 | * A list of settings. Accepted values: | |
213 | * - activeRoute: string, the route to open up immediately | |
23a258ce TO |
214 | * Ex: '/case/list' |
215 | * - defaultRoute: string, use this to redirect the default route (`/`) to another page | |
216 | * Ex: '/case/list' | |
3e8823e3 | 217 | * - region: string, the place on the page where we should insert the angular app |
23a258ce | 218 | * Ex: 'page-body' |
3e8823e3 TO |
219 | * @return AngularLoader |
220 | * @link https://code.angularjs.org/1.5.11/docs/guide/bootstrap | |
221 | */ | |
c64f69d9 CW |
222 | public function useApp($settings = []) { |
223 | $defaults = [ | |
224 | 'modules' => ['crmApp'], | |
3e8823e3 TO |
225 | 'activeRoute' => NULL, |
226 | 'defaultRoute' => NULL, | |
227 | 'region' => 'page-body', | |
228 | 'file' => 'Civi/Angular/Page/Main.tpl', | |
c64f69d9 | 229 | ]; |
3e8823e3 | 230 | $this->crmApp = array_merge($defaults, $settings); |
9bd30577 CW |
231 | $region = \CRM_Core_Region::instance($this->crmApp['region']); |
232 | $region->update('default', ['disabled' => TRUE]); | |
233 | $region->add(['template' => $this->crmApp['file'], 'weight' => 0]); | |
3e8823e3 TO |
234 | return $this; |
235 | } | |
236 | ||
b20ea913 TO |
237 | /** |
238 | * Get a list of all Angular modules which should be activated on this | |
239 | * page. | |
240 | * | |
241 | * @return array | |
242 | * List of module names. | |
243 | * Ex: array('angularFileUpload', 'crmUi', 'crmUtil'). | |
244 | */ | |
245 | public function findActiveModules() { | |
246 | return $this->angular->resolveDependencies(array_merge( | |
247 | $this->getModules(), | |
248 | $this->angular->resolveDefaultModules($this->getPageName()) | |
249 | )); | |
250 | } | |
251 | ||
252 | /** | |
253 | * @param $moduleNames | |
254 | * @return int | |
255 | */ | |
256 | private function isAllModules($moduleNames) { | |
257 | $allModuleNames = array_keys($this->angular->getModules()); | |
258 | return count(array_diff($allModuleNames, $moduleNames)) === 0; | |
259 | } | |
260 | ||
261 | /** | |
262 | * @return \CRM_Core_Resources | |
263 | */ | |
264 | public function getRes() { | |
265 | return $this->res; | |
266 | } | |
267 | ||
268 | /** | |
269 | * @param \CRM_Core_Resources $res | |
cba659e7 | 270 | * @return AngularLoader |
b20ea913 TO |
271 | */ |
272 | public function setRes($res) { | |
273 | $this->res = $res; | |
cba659e7 | 274 | return $this; |
b20ea913 TO |
275 | } |
276 | ||
277 | /** | |
278 | * @return \Civi\Angular\Manager | |
279 | */ | |
280 | public function getAngular() { | |
281 | return $this->angular; | |
282 | } | |
283 | ||
284 | /** | |
285 | * @param \Civi\Angular\Manager $angular | |
cba659e7 | 286 | * @return AngularLoader |
b20ea913 TO |
287 | */ |
288 | public function setAngular($angular) { | |
289 | $this->angular = $angular; | |
cba659e7 | 290 | return $this; |
b20ea913 TO |
291 | } |
292 | ||
293 | /** | |
294 | * @return string | |
295 | */ | |
296 | public function getRegion() { | |
297 | return $this->region; | |
298 | } | |
299 | ||
300 | /** | |
301 | * @param string $region | |
cba659e7 | 302 | * @return AngularLoader |
b20ea913 TO |
303 | */ |
304 | public function setRegion($region) { | |
305 | $this->region = $region; | |
cba659e7 | 306 | return $this; |
b20ea913 TO |
307 | } |
308 | ||
309 | /** | |
310 | * @return string | |
311 | * Ex: 'civicrm/a'. | |
312 | */ | |
313 | public function getPageName() { | |
314 | return $this->pageName; | |
315 | } | |
316 | ||
317 | /** | |
318 | * @param string $pageName | |
319 | * Ex: 'civicrm/a'. | |
cba659e7 | 320 | * @return AngularLoader |
b20ea913 TO |
321 | */ |
322 | public function setPageName($pageName) { | |
323 | $this->pageName = $pageName; | |
cba659e7 | 324 | return $this; |
b20ea913 TO |
325 | } |
326 | ||
d2f38ec9 TO |
327 | /** |
328 | * @param array|string $modules | |
329 | * @return AngularLoader | |
330 | */ | |
331 | public function addModules($modules) { | |
332 | $modules = (array) $modules; | |
333 | $this->modules = array_unique(array_merge($this->modules, $modules)); | |
334 | return $this; | |
335 | } | |
336 | ||
b20ea913 TO |
337 | /** |
338 | * @return array | |
339 | */ | |
340 | public function getModules() { | |
341 | return $this->modules; | |
342 | } | |
343 | ||
344 | /** | |
b1510da4 CW |
345 | * Replace all previously set modules. |
346 | * | |
347 | * Use with caution, as it can cause conflicts with other extensions who have added modules. | |
166ed6b2 CW |
348 | * @internal |
349 | * @deprecated | |
b20ea913 | 350 | * @param array $modules |
cba659e7 | 351 | * @return AngularLoader |
b20ea913 TO |
352 | */ |
353 | public function setModules($modules) { | |
166ed6b2 | 354 | \CRM_Core_Error::deprecatedFunctionWarning('addModules'); |
b20ea913 | 355 | $this->modules = $modules; |
cba659e7 | 356 | return $this; |
b20ea913 TO |
357 | } |
358 | ||
9bd30577 | 359 | /** |
f5c157f5 CW |
360 | * Loader service callback when rendering a page region. |
361 | * | |
362 | * Loads Angular resources if any modules have been requested for this page. | |
363 | * | |
9bd30577 CW |
364 | * @param \Civi\Core\Event\GenericHookEvent $e |
365 | */ | |
366 | public function onRegionRender($e) { | |
367 | if ($e->region->_name === $this->region && ($this->modules || $this->crmApp)) { | |
f5c157f5 | 368 | $this->loadAngularResources(); |
9bd30577 CW |
369 | $this->res->addScriptFile('civicrm', 'js/crm-angularjs-loader.js', 200, $this->getRegion(), FALSE); |
370 | } | |
371 | } | |
372 | ||
b20ea913 | 373 | } |