2 namespace Civi\Angular
;
5 * Manage Angular resources.
7 * @package Civi\Angular
12 * @var \CRM_Core_Resources
14 protected $res = NULL;
20 * Each item has some combination of these keys:
22 * The Civi extension which defines the Angular module.
23 * - js: array(string $relativeFilePath)
24 * List of JS files (relative to the extension).
25 * - css: array(string $relativeFilePath)
26 * List of CSS files (relative to the extension).
27 * - partials: array(string $relativeFilePath)
28 * A list of partial-HTML folders (relative to the extension).
29 * This will be mapped to "~/moduleName" by crmResource.
30 * - settings: array(string $key => mixed $value)
31 * List of settings to preload.
32 * - settingsFactory: callable
33 * Callback function to fetch settings.
34 * - permissions: array
35 * List of permissions to make available client-side
37 * List of other modules required
39 protected $modules = NULL;
42 * @var \CRM_Utils_Cache_Interface
48 * Array(string $name => ChangeSet $change).
50 protected $changeSets = NULL;
53 * @param \CRM_Core_Resources $res
54 * The resource manager.
57 public function __construct($res, \CRM_Utils_Cache_Interface
$cache = NULL) {
59 $this->cache
= $cache ?
$cache : new \
CRM_Utils_Cache_ArrayCache([]);
63 * Clear out any runtime-cached metadata.
65 * This is useful if, eg, you have recently added or destroyed Angular modules.
69 public function clear() {
70 $this->cache
->clear();
71 $this->modules
= NULL;
72 $this->changeSets
= NULL;
77 * Get a list of AngularJS modules which should be autoloaded.
80 * Each item has some combination of these keys:
82 * The Civi extension which defines the Angular module.
83 * - js: array(string $relativeFilePath)
84 * List of JS files (relative to the extension).
85 * - css: array(string $relativeFilePath)
86 * List of CSS files (relative to the extension).
87 * - partials: array(string $relativeFilePath)
88 * A list of partial-HTML folders (relative to the extension).
89 * This will be mapped to "~/moduleName" by crmResource.
90 * - settings: array(string $key => mixed $value)
91 * List of settings to preload.
93 public function getModules() {
94 if ($this->modules
=== NULL) {
95 $config = \CRM_Core_Config
::singleton();
98 // Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time
99 // of writing CiviMail and CiviCase have special conditionals.
101 $angularModules = [];
102 $angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php";
103 $angularModules['checklist-model'] = include "$civicrm_root/ang/checklist-model.ang.php";
104 $angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php";
105 $angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php";
106 $angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php";
107 $angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php";
108 $angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php";
109 $angularModules['crmRouteBinder'] = include "$civicrm_root/ang/crmRouteBinder.ang.php";
110 $angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php";
111 $angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php";
112 $angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php";
113 $angularModules['jsonFormatter'] = include "$civicrm_root/ang/jsonFormatter.ang.php";
114 $angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php";
115 $angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php";
116 $angularModules['ui.utils'] = include "$civicrm_root/ang/ui.utils.ang.php";
117 $angularModules['ui.bootstrap'] = include "$civicrm_root/ang/ui.bootstrap.ang.php";
118 $angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php";
119 $angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php";
120 $angularModules['statuspage'] = include "$civicrm_root/ang/crmStatusPage.ang.php";
121 $angularModules['exportui'] = include "$civicrm_root/ang/exportui.ang.php";
122 $angularModules['api4Explorer'] = include "$civicrm_root/ang/api4Explorer.ang.php";
123 $angularModules['api4'] = include "$civicrm_root/ang/api4.ang.php";
124 $angularModules['crmDashboard'] = include "$civicrm_root/ang/crmDashboard.ang.php";
125 $angularModules['crmD3'] = include "$civicrm_root/ang/crmD3.ang.php";
127 foreach (\CRM_Core_Component
::getEnabledComponents() as $component) {
128 $angularModules = array_merge($angularModules, $component->getAngularModules());
130 \CRM_Utils_Hook
::angularModules($angularModules);
131 foreach ($angularModules as $module => $info) {
133 $angularModules[$module] +
= ['basePages' => ['civicrm/a']];
134 // Validate settingsFactory callables
135 if (isset($info['settingsFactory'])) {
136 // To keep the cache small, we want `settingsFactory` to contain the string names of class & function, not an object
137 if (!is_array($info['settingsFactory']) && !is_string($info['settingsFactory'])) {
138 throw new \
CRM_Core_Exception($module . ' settingsFactory must be a callable array or string');
140 // To keep the cache small, convert full object to just the class name
141 if (is_array($info['settingsFactory']) && is_object($info['settingsFactory'][0])) {
142 $angularModules[$module]['settingsFactory'][0] = get_class($info['settingsFactory'][0]);
146 $this->modules
= $this->resolvePatterns($angularModules);
149 return $this->modules
;
153 * Get the descriptor for an Angular module.
155 * @param string $name
158 * Details about the module:
159 * - ext: string, the name of the Civi extension which defines the module
160 * - js: array(string $relativeFilePath).
161 * - css: array(string $relativeFilePath).
162 * - partials: array(string $relativeFilePath).
165 public function getModule($name) {
166 $modules = $this->getModules();
167 if (!isset($modules[$name])) {
168 throw new \
Exception("Unrecognized Angular module");
170 return $modules[$name];
174 * Resolve a full list of Angular dependencies.
176 * @param array $names
177 * List of Angular modules.
178 * Ex: array('crmMailing').
180 * List of Angular modules, include all dependencies.
181 * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
183 public function resolveDependencies($names) {
184 $allModules = $this->getModules();
187 while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) {
188 foreach ($missingModules as $module) {
189 $visited[$module] = 1;
190 if (!isset($allModules[$module])) {
191 \Civi
::log()->warning('Unrecognized Angular module {name}. Please ensure that all Angular modules are declared.', [
193 'civi.tag' => 'deprecated',
196 elseif (isset($allModules[$module]['requires'])) {
197 $result = array_unique(array_merge($result, $allModules[$module]['requires']));
206 * Get a list of Angular modules that should be loaded on the given
209 * @param string $basePage
210 * The name of the base-page for which we want a list of moudles.
212 * List of Angular modules.
213 * Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
215 public function resolveDefaultModules($basePage) {
216 $modules = $this->getModules();
218 foreach ($modules as $moduleName => $module) {
219 if (in_array($basePage, $module['basePages']) ||
in_array('*', $module['basePages'])) {
220 $result[] = $moduleName;
227 * Convert any globs in an Angular module to file names.
229 * @param array $modules
230 * List of Angular modules.
232 * Updated list of Angular modules
234 protected function resolvePatterns($modules) {
237 foreach ($modules as $moduleKey => $module) {
238 foreach (['js', 'css', 'partials'] as $fileset) {
239 if (!isset($module[$fileset])) {
242 $module[$fileset] = $this->res
->glob($module['ext'], $module[$fileset]);
244 $newModules[$moduleKey] = $module;
251 * Get the partial HTML documents for a module (unfiltered).
253 * @param string $name
254 * Angular module name.
256 * Array(string $extFilePath => string $html)
258 * Invalid partials configuration.
260 public function getRawPartials($name) {
261 $module = $this->getModule($name);
262 $result = !empty($module['partialsCallback'])
263 ? \Civi\Core\Resolver
::singleton()->call($module['partialsCallback'], [$name, $module])
265 if (isset($module['partials'])) {
266 foreach ($module['partials'] as $partialDir) {
267 $partialDir = $this->res
->getPath($module['ext']) . '/' . $partialDir;
268 $files = \CRM_Utils_File
::findFiles($partialDir, '*.html', TRUE);
269 foreach ($files as $file) {
270 $filename = '~/' . $name . '/' . $file;
271 $result[$filename] = file_get_contents($partialDir . '/' . $file);
280 * Get the partial HTML documents for a module.
282 * @param string $name
283 * Angular module name.
285 * Array(string $extFilePath => string $html)
287 * Invalid partials configuration.
289 public function getPartials($name) {
290 $cacheKey = "angular-partials_$name";
291 $cacheValue = $this->cache
->get($cacheKey);
292 if ($cacheValue === NULL) {
293 $cacheValue = ChangeSet
::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
294 $this->cache
->set($cacheKey, $cacheValue);
300 * Get list of translated strings for a module.
302 * @param string $name
303 * Angular module name.
305 * Translated strings: array(string $orig => string $translated).
307 public function getTranslatedStrings($name) {
308 $module = $this->getModule($name);
310 $strings = $this->getStrings($name);
311 foreach ($strings as $string) {
312 // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]?
313 // It doesn't look like client side really supports the domain right now...
314 $translated = ts($string, [
315 'domain' => [$module['ext'], NULL],
317 if ($translated != $string) {
318 $result[$string] = $translated;
325 * Get list of translatable strings for a module.
327 * @param string $name
328 * Angular module name.
330 * Translatable strings.
332 public function getStrings($name) {
333 $module = $this->getModule($name);
335 if (isset($module['js'])) {
336 foreach ($module['js'] as $file) {
337 $strings = $this->res
->getStrings()->get(
339 $this->res
->getPath($module['ext'], $file),
342 $result = array_unique(array_merge($result, $strings));
345 $partials = $this->getPartials($name);
346 foreach ($partials as $partial) {
347 $result = array_unique(array_merge($result, \CRM_Utils_JS
::parseStrings($partial)));
353 * Get resources for one or more modules.
355 * @param string|array $moduleNames
356 * List of module names.
357 * @param string $resType
358 * Type of resource ('js', 'css', 'settings').
359 * @param string $refType
360 * Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings').
362 * List of URLs or paths.
363 * @throws \CRM_Core_Exception
365 public function getResources($moduleNames, $resType, $refType) {
367 $moduleNames = (array) $moduleNames;
368 foreach ($moduleNames as $moduleName) {
369 $module = $this->getModule($moduleName);
370 if (isset($module[$resType])) {
371 foreach ($module[$resType] as $file) {
373 if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) {
374 $refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME
);
377 switch ($refType . $refTypeSuffix) {
379 $result[] = $this->res
->getPath($module['ext'], $file);
383 $result[] = $this->res
->getUrl($module['ext'], $file);
387 $result[] = $this->res
->getUrl($module['ext'], $file, TRUE);
390 case 'path-assetBuilder':
391 $assetName = parse_url($file, PHP_URL_HOST
) . parse_url($file, PHP_URL_PATH
);
393 parse_str('' . parse_url($file, PHP_URL_QUERY
), $assetParams);
394 $result[] = \Civi
::service('asset_builder')->getPath($assetName, $assetParams);
397 case 'rawUrl-assetBuilder':
398 case 'cacheUrl-assetBuilder':
399 $assetName = parse_url($file, PHP_URL_HOST
) . parse_url($file, PHP_URL_PATH
);
401 parse_str('' . parse_url($file, PHP_URL_QUERY
), $assetParams);
402 $result[] = \Civi
::service('asset_builder')->getUrl($assetName, $assetParams);
406 $result[] = $this->res
->getPath(parse_url($file, PHP_URL_HOST
), ltrim(parse_url($file, PHP_URL_PATH
), '/'));
410 $result[] = $this->res
->getUrl(parse_url($file, PHP_URL_HOST
), ltrim(parse_url($file, PHP_URL_PATH
), '/'));
414 $result[] = $this->res
->getUrl(parse_url($file, PHP_URL_HOST
), ltrim(parse_url($file, PHP_URL_PATH
), '/'), TRUE);
418 case 'settingsFactory':
422 if (!empty($module[$resType])) {
423 $result[$moduleName] = $module[$resType];
428 throw new \
CRM_Core_Exception("Unrecognized resource format");
434 return ChangeSet
::applyResourceFilters($this->getChangeSets(), $resType, $result);
439 * Array(string $name => ChangeSet $changeSet).
441 public function getChangeSets() {
442 if ($this->changeSets
=== NULL) {
443 $this->changeSets
= [];
444 \CRM_Utils_Hook
::alterAngular($this);
446 return $this->changeSets
;
450 * @param ChangeSet $changeSet
451 * @return \Civi\Angular\Manager
453 public function add($changeSet) {
454 $this->changeSets
[$changeSet->getName()] = $changeSet;