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