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 | * | |
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 | */ | |
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 | ||
3e8823e3 | 64 | /** |
cc101011 | 65 | * @var array|null |
3e8823e3 TO |
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; | |
c64f69d9 | 77 | $this->modules = []; |
b20ea913 TO |
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']); | |
c64f69d9 CW |
92 | $region->update('default', ['disabled' => TRUE]); |
93 | $region->add(['template' => $this->crmApp['file'], 'weight' => 0]); | |
94 | $this->res->addSetting([ | |
95 | 'crmApp' => [ | |
3e8823e3 | 96 | 'defaultRoute' => $this->crmApp['defaultRoute'], |
c64f69d9 CW |
97 | ], |
98 | ]); | |
3e8823e3 TO |
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 #). | |
c64f69d9 | 103 | $this->res->addSetting([ |
3e8823e3 | 104 | 'angularRoute' => $this->crmApp['activeRoute'], |
c64f69d9 | 105 | ]); |
3e8823e3 TO |
106 | } |
107 | ||
b20ea913 TO |
108 | $moduleNames = $this->findActiveModules(); |
109 | if (!$this->isAllModules($moduleNames)) { | |
c64f69d9 | 110 | $assetParams = ['modules' => implode(',', $moduleNames)]; |
b20ea913 TO |
111 | } |
112 | else { | |
113 | // The module list will be "all modules that the user can see". | |
c64f69d9 | 114 | $assetParams = ['nonce' => md5(implode(',', $moduleNames))]; |
b20ea913 TO |
115 | } |
116 | ||
117 | $res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) { | |
118 | // TODO optimization; client-side caching | |
c64f69d9 | 119 | $result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), [ |
b20ea913 | 120 | 'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(), |
c64f69d9 | 121 | 'angular' => [ |
b20ea913 TO |
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), | |
c64f69d9 CW |
126 | ], |
127 | ]); | |
b20ea913 TO |
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 | */ | |
c64f69d9 CW |
193 | public function useApp($settings = []) { |
194 | $defaults = [ | |
195 | 'modules' => ['crmApp'], | |
3e8823e3 TO |
196 | 'activeRoute' => NULL, |
197 | 'defaultRoute' => NULL, | |
198 | 'region' => 'page-body', | |
199 | 'file' => 'Civi/Angular/Page/Main.tpl', | |
c64f69d9 | 200 | ]; |
3e8823e3 TO |
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 | } |