AngularJS - Remove UI-Utils library
[civicrm-core.git] / Civi / Angular / Manager.php
CommitLineData
16072ce1
TO
1<?php
2namespace Civi\Angular;
3
4/**
5 * Manage Angular resources.
6 *
7 * @package Civi\Angular
8 */
9class Manager {
10
11 /**
12 * @var \CRM_Core_Resources
13 */
14 protected $res = NULL;
15
16 /**
cc101011 17 * Modules.
18 *
19 * @var array|null
16072ce1
TO
20 * Each item has some combination of these keys:
21 * - ext: string
8671b4f2 22 * The Civi extension which defines the Angular module.
16072ce1 23 * - js: array(string $relativeFilePath)
8671b4f2 24 * List of JS files (relative to the extension).
16072ce1 25 * - css: array(string $relativeFilePath)
8671b4f2 26 * List of CSS files (relative to the extension).
16072ce1 27 * - partials: array(string $relativeFilePath)
8671b4f2
TO
28 * A list of partial-HTML folders (relative to the extension).
29 * This will be mapped to "~/moduleName" by crmResource.
1da632e0
TO
30 * - settings: array(string $key => mixed $value)
31 * List of settings to preload.
aa882f9b
CW
32 * - settingsFactory: callable
33 * Callback function to fetch settings.
66c46618
CW
34 * - permissions: array
35 * List of permissions to make available client-side
aa882f9b
CW
36 * - requires: array
37 * List of other modules required
16072ce1
TO
38 */
39 protected $modules = NULL;
40
53d00841
TO
41 /**
42 * @var \CRM_Utils_Cache_Interface
43 */
44 protected $cache;
45
6dc348de
TO
46 /**
47 * @var array
48 * Array(string $name => ChangeSet $change).
49 */
50 protected $changeSets = NULL;
51
16072ce1
TO
52 /**
53 * @param \CRM_Core_Resources $res
54 * The resource manager.
34f3bbd9 55 * @param $cache
16072ce1 56 */
53d00841 57 public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) {
16072ce1 58 $this->res = $res;
c33f1df1 59 $this->cache = $cache ? $cache : new \CRM_Utils_Cache_ArrayCache([]);
16072ce1
TO
60 }
61
76b9562a
TO
62 /**
63 * Clear out any runtime-cached metadata.
64 *
65 * This is useful if, eg, you have recently added or destroyed Angular modules.
66 *
67 * @return static
68 */
69 public function clear() {
70 $this->cache->clear();
71 $this->modules = NULL;
72 $this->changeSets = NULL;
a19c781a
CW
73 // Force-refresh assetBuilder files
74 \Civi::container()->get('asset_builder')->clear(FALSE);
76b9562a
TO
75 return $this;
76 }
77
16072ce1 78 /**
fe482240 79 * Get a list of AngularJS modules which should be autoloaded.
16072ce1
TO
80 *
81 * @return array
8671b4f2
TO
82 * Each item has some combination of these keys:
83 * - ext: string
84 * The Civi extension which defines the Angular module.
85 * - js: array(string $relativeFilePath)
86 * List of JS files (relative to the extension).
87 * - css: array(string $relativeFilePath)
88 * List of CSS files (relative to the extension).
89 * - partials: array(string $relativeFilePath)
90 * A list of partial-HTML folders (relative to the extension).
91 * This will be mapped to "~/moduleName" by crmResource.
1da632e0
TO
92 * - settings: array(string $key => mixed $value)
93 * List of settings to preload.
16072ce1
TO
94 */
95 public function getModules() {
96 if ($this->modules === NULL) {
1da632e0 97 $config = \CRM_Core_Config::singleton();
8456e727 98 global $civicrm_root;
16072ce1 99
8456e727
TO
100 // Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time
101 // of writing CiviMail and CiviCase have special conditionals.
16072ce1 102
c64f69d9 103 $angularModules = [];
8456e727 104 $angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php";
b9a4dfbd 105 $angularModules['checklist-model'] = include "$civicrm_root/ang/checklist-model.ang.php";
8456e727
TO
106 $angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php";
107 $angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php";
108 $angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php";
109 $angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php";
138b5733 110 $angularModules['crmDialog'] = include "$civicrm_root/ang/crmDialog.ang.php";
9a78af90 111 $angularModules['crmMonaco'] = include "$civicrm_root/ang/crmMonaco.ang.php";
8456e727 112 $angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php";
67341f3c 113 $angularModules['crmRouteBinder'] = include "$civicrm_root/ang/crmRouteBinder.ang.php";
8456e727
TO
114 $angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php";
115 $angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php";
116 $angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php";
10d00207 117 $angularModules['jsonFormatter'] = include "$civicrm_root/ang/jsonFormatter.ang.php";
8456e727
TO
118 $angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php";
119 $angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php";
e88c001e 120 $angularModules['ui.bootstrap'] = include "$civicrm_root/ang/ui.bootstrap.ang.php";
8456e727
TO
121 $angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php";
122 $angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php";
cd6afc75 123 $angularModules['crmStatusPage'] = include "$civicrm_root/ang/crmStatusPage.ang.php";
33044927 124 $angularModules['exportui'] = include "$civicrm_root/ang/exportui.ang.php";
0b873c9a
CW
125 $angularModules['api4Explorer'] = include "$civicrm_root/ang/api4Explorer.ang.php";
126 $angularModules['api4'] = include "$civicrm_root/ang/api4.ang.php";
f263929f 127 $angularModules['crmDashboard'] = include "$civicrm_root/ang/crmDashboard.ang.php";
bca4106f 128 $angularModules['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php";
c0f7f681 129
16072ce1
TO
130 foreach (\CRM_Core_Component::getEnabledComponents() as $component) {
131 $angularModules = array_merge($angularModules, $component->getAngularModules());
132 }
133 \CRM_Utils_Hook::angularModules($angularModules);
aa882f9b
CW
134 foreach ($angularModules as $module => $info) {
135 // Merge in defaults
136 $angularModules[$module] += ['basePages' => ['civicrm/a']];
137 // Validate settingsFactory callables
138 if (isset($info['settingsFactory'])) {
139 // To keep the cache small, we want `settingsFactory` to contain the string names of class & function, not an object
140 if (!is_array($info['settingsFactory']) && !is_string($info['settingsFactory'])) {
141 throw new \CRM_Core_Exception($module . ' settingsFactory must be a callable array or string');
142 }
143 // To keep the cache small, convert full object to just the class name
144 if (is_array($info['settingsFactory']) && is_object($info['settingsFactory'][0])) {
145 $angularModules[$module]['settingsFactory'][0] = get_class($info['settingsFactory'][0]);
146 }
8da6c9b8
TO
147 }
148 }
16072ce1
TO
149 $this->modules = $this->resolvePatterns($angularModules);
150 }
151
152 return $this->modules;
153 }
154
155 /**
156 * Get the descriptor for an Angular module.
157 *
158 * @param string $name
159 * Module name.
160 * @return array
161 * Details about the module:
162 * - ext: string, the name of the Civi extension which defines the module
163 * - js: array(string $relativeFilePath).
164 * - css: array(string $relativeFilePath).
165 * - partials: array(string $relativeFilePath).
166 * @throws \Exception
167 */
168 public function getModule($name) {
169 $modules = $this->getModules();
170 if (!isset($modules[$name])) {
171 throw new \Exception("Unrecognized Angular module");
172 }
173 return $modules[$name];
174 }
175
8da6c9b8
TO
176 /**
177 * Resolve a full list of Angular dependencies.
178 *
179 * @param array $names
180 * List of Angular modules.
181 * Ex: array('crmMailing').
182 * @return array
183 * List of Angular modules, include all dependencies.
184 * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
51feb18e 185 * @throws \CRM_Core_Exception
8da6c9b8
TO
186 */
187 public function resolveDependencies($names) {
188 $allModules = $this->getModules();
c64f69d9 189 $visited = [];
8da6c9b8
TO
190 $result = $names;
191 while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) {
192 foreach ($missingModules as $module) {
193 $visited[$module] = 1;
194 if (!isset($allModules[$module])) {
51feb18e 195 throw new \CRM_Core_Exception("Unrecognized Angular module {$module}. Please ensure that all Angular modules are declared.");
8da6c9b8
TO
196 }
197 elseif (isset($allModules[$module]['requires'])) {
198 $result = array_unique(array_merge($result, $allModules[$module]['requires']));
199 }
200 }
201 }
202 sort($result);
203 return $result;
204 }
205
206 /**
207 * Get a list of Angular modules that should be loaded on the given
208 * base-page.
209 *
210 * @param string $basePage
211 * The name of the base-page for which we want a list of moudles.
212 * @return array
213 * List of Angular modules.
214 * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
215 */
216 public function resolveDefaultModules($basePage) {
217 $modules = $this->getModules();
c64f69d9 218 $result = [];
8da6c9b8
TO
219 foreach ($modules as $moduleName => $module) {
220 if (in_array($basePage, $module['basePages']) || in_array('*', $module['basePages'])) {
221 $result[] = $moduleName;
222 }
223 }
224 return $result;
225 }
226
16072ce1
TO
227 /**
228 * Convert any globs in an Angular module to file names.
229 *
230 * @param array $modules
231 * List of Angular modules.
232 * @return array
233 * Updated list of Angular modules
234 */
235 protected function resolvePatterns($modules) {
c64f69d9 236 $newModules = [];
16072ce1
TO
237
238 foreach ($modules as $moduleKey => $module) {
c64f69d9 239 foreach (['js', 'css', 'partials'] as $fileset) {
16072ce1
TO
240 if (!isset($module[$fileset])) {
241 continue;
242 }
243 $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]);
244 }
245 $newModules[$moduleKey] = $module;
246 }
247
248 return $newModules;
249 }
250
251 /**
6dc348de 252 * Get the partial HTML documents for a module (unfiltered).
16072ce1
TO
253 *
254 * @param string $name
255 * Angular module name.
256 * @return array
257 * Array(string $extFilePath => string $html)
a2dc0f82
TO
258 * @throws \Exception
259 * Invalid partials configuration.
16072ce1 260 */
6dc348de 261 public function getRawPartials($name) {
16072ce1 262 $module = $this->getModule($name);
90c62ad3
TO
263 $result = !empty($module['partialsCallback'])
264 ? \Civi\Core\Resolver::singleton()->call($module['partialsCallback'], [$name, $module])
265 : [];
16072ce1 266 if (isset($module['partials'])) {
a2dc0f82
TO
267 foreach ($module['partials'] as $partialDir) {
268 $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir;
269 $files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE);
270 foreach ($files as $file) {
271 $filename = '~/' . $name . '/' . $file;
272 $result[$filename] = file_get_contents($partialDir . '/' . $file);
273 }
16072ce1 274 }
6dc348de 275 return $result;
16072ce1
TO
276 }
277 return $result;
278 }
279
6dc348de
TO
280 /**
281 * Get the partial HTML documents for a module.
282 *
283 * @param string $name
284 * Angular module name.
285 * @return array
286 * Array(string $extFilePath => string $html)
287 * @throws \Exception
288 * Invalid partials configuration.
289 */
290 public function getPartials($name) {
8b25b1fe 291 $cacheKey = "angular-partials_$name";
6dc348de
TO
292 $cacheValue = $this->cache->get($cacheKey);
293 if ($cacheValue === NULL) {
294 $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
295 $this->cache->set($cacheKey, $cacheValue);
296 }
297 return $cacheValue;
298 }
299
16072ce1
TO
300 /**
301 * Get list of translated strings for a module.
302 *
303 * @param string $name
304 * Angular module name.
305 * @return array
306 * Translated strings: array(string $orig => string $translated).
307 */
308 public function getTranslatedStrings($name) {
e3d90d6c 309 $module = $this->getModule($name);
c64f69d9 310 $result = [];
16072ce1
TO
311 $strings = $this->getStrings($name);
312 foreach ($strings as $string) {
313 // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]?
314 // It doesn't look like client side really supports the domain right now...
c64f69d9
CW
315 $translated = ts($string, [
316 'domain' => [$module['ext'], NULL],
317 ]);
16072ce1
TO
318 if ($translated != $string) {
319 $result[$string] = $translated;
320 }
321 }
322 return $result;
323 }
324
325 /**
326 * Get list of translatable strings for a module.
327 *
328 * @param string $name
329 * Angular module name.
330 * @return array
331 * Translatable strings.
332 */
333 public function getStrings($name) {
334 $module = $this->getModule($name);
c64f69d9 335 $result = [];
16072ce1
TO
336 if (isset($module['js'])) {
337 foreach ($module['js'] as $file) {
338 $strings = $this->res->getStrings()->get(
339 $module['ext'],
340 $this->res->getPath($module['ext'], $file),
341 'text/javascript'
342 );
343 $result = array_unique(array_merge($result, $strings));
344 }
345 }
53d00841
TO
346 $partials = $this->getPartials($name);
347 foreach ($partials as $partial) {
348 $result = array_unique(array_merge($result, \CRM_Utils_JS::parseStrings($partial)));
16072ce1
TO
349 }
350 return $result;
351 }
352
353 /**
27a90ef6
TO
354 * Get resources for one or more modules.
355 *
356 * @param string|array $moduleNames
357 * List of module names.
358 * @param string $resType
1da632e0 359 * Type of resource ('js', 'css', 'settings').
27a90ef6 360 * @param string $refType
1da632e0 361 * Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings').
16072ce1 362 * @return array
27a90ef6
TO
363 * List of URLs or paths.
364 * @throws \CRM_Core_Exception
16072ce1 365 */
27a90ef6 366 public function getResources($moduleNames, $resType, $refType) {
c64f69d9 367 $result = [];
27a90ef6
TO
368 $moduleNames = (array) $moduleNames;
369 foreach ($moduleNames as $moduleName) {
370 $module = $this->getModule($moduleName);
371 if (isset($module[$resType])) {
372 foreach ($module[$resType] as $file) {
e5c376e7
TO
373 $refTypeSuffix = '';
374 if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) {
375 $refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME);
376 }
377
378 switch ($refType . $refTypeSuffix) {
27a90ef6
TO
379 case 'path':
380 $result[] = $this->res->getPath($module['ext'], $file);
381 break;
16072ce1 382
27a90ef6
TO
383 case 'rawUrl':
384 $result[] = $this->res->getUrl($module['ext'], $file);
385 break;
386
387 case 'cacheUrl':
388 $result[] = $this->res->getUrl($module['ext'], $file, TRUE);
389 break;
390
e5c376e7
TO
391 case 'path-assetBuilder':
392 $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH);
c64f69d9 393 $assetParams = [];
e5c376e7
TO
394 parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams);
395 $result[] = \Civi::service('asset_builder')->getPath($assetName, $assetParams);
396 break;
397
398 case 'rawUrl-assetBuilder':
399 case 'cacheUrl-assetBuilder':
400 $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH);
c64f69d9 401 $assetParams = [];
e5c376e7
TO
402 parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams);
403 $result[] = \Civi::service('asset_builder')->getUrl($assetName, $assetParams);
404 break;
405
406 case 'path-ext':
407 $result[] = $this->res->getPath(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'));
408 break;
409
410 case 'rawUrl-ext':
411 $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'));
412 break;
413
414 case 'cacheUrl-ext':
415 $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'), TRUE);
416 break;
417
1da632e0 418 case 'settings':
aa882f9b 419 case 'settingsFactory':
5438399c 420 case 'requires':
66c46618 421 case 'permissions':
d67ff852 422 case 'bundles':
1da632e0
TO
423 if (!empty($module[$resType])) {
424 $result[$moduleName] = $module[$resType];
425 }
426 break;
427
27a90ef6
TO
428 default:
429 throw new \CRM_Core_Exception("Unrecognized resource format");
430 }
431 }
16072ce1
TO
432 }
433 }
6dc348de
TO
434
435 return ChangeSet::applyResourceFilters($this->getChangeSets(), $resType, $result);
436 }
437
438 /**
439 * @return array
440 * Array(string $name => ChangeSet $changeSet).
441 */
442 public function getChangeSets() {
443 if ($this->changeSets === NULL) {
c64f69d9 444 $this->changeSets = [];
6dc348de
TO
445 \CRM_Utils_Hook::alterAngular($this);
446 }
447 return $this->changeSets;
448 }
449
450 /**
451 * @param ChangeSet $changeSet
452 * @return \Civi\Angular\Manager
453 */
454 public function add($changeSet) {
455 $this->changeSets[$changeSet->getName()] = $changeSet;
456 return $this;
16072ce1 457 }
8671b4f2 458
16072ce1 459}