CRM-20600 - Civi\Angular\Manager - Add hook for modifying partials and dependencies
[civicrm-core.git] / Civi / Angular / Manager.php
1 <?php
2 namespace Civi\Angular;
3
4 /**
5 * Manage Angular resources.
6 *
7 * @package Civi\Angular
8 */
9 class Manager {
10
11 /**
12 * @var \CRM_Core_Resources
13 */
14 protected $res = NULL;
15
16 /**
17 * @var array|NULL
18 * Each item has some combination of these keys:
19 * - ext: string
20 * The Civi extension which defines the Angular module.
21 * - js: array(string $relativeFilePath)
22 * List of JS files (relative to the extension).
23 * - css: array(string $relativeFilePath)
24 * List of CSS files (relative to the extension).
25 * - partials: array(string $relativeFilePath)
26 * A list of partial-HTML folders (relative to the extension).
27 * This will be mapped to "~/moduleName" by crmResource.
28 * - settings: array(string $key => mixed $value)
29 * List of settings to preload.
30 */
31 protected $modules = NULL;
32
33 /**
34 * @var \CRM_Utils_Cache_Interface
35 */
36 protected $cache;
37
38 /**
39 * @var array
40 * Array(string $name => ChangeSet $change).
41 */
42 protected $changeSets = NULL;
43
44 /**
45 * @param \CRM_Core_Resources $res
46 * The resource manager.
47 */
48 public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) {
49 $this->res = $res;
50 $this->cache = $cache ? $cache : new \CRM_Utils_Cache_Arraycache(array());
51 }
52
53 /**
54 * Get a list of AngularJS modules which should be autoloaded.
55 *
56 * @return array
57 * Each item has some combination of these keys:
58 * - ext: string
59 * The Civi extension which defines the Angular module.
60 * - js: array(string $relativeFilePath)
61 * List of JS files (relative to the extension).
62 * - css: array(string $relativeFilePath)
63 * List of CSS files (relative to the extension).
64 * - partials: array(string $relativeFilePath)
65 * A list of partial-HTML folders (relative to the extension).
66 * This will be mapped to "~/moduleName" by crmResource.
67 * - settings: array(string $key => mixed $value)
68 * List of settings to preload.
69 */
70 public function getModules() {
71 if ($this->modules === NULL) {
72 $config = \CRM_Core_Config::singleton();
73 global $civicrm_root;
74
75 // Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time
76 // of writing CiviMail and CiviCase have special conditionals.
77
78 $angularModules = array();
79 $angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php";
80 $angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php";
81 $angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php";
82 $angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php";
83 $angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php";
84 // $angularModules['crmExample'] = include "$civicrm_root/ang/crmExample.ang.php";
85 $angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php";
86 $angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php";
87 $angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php";
88 $angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php";
89 $angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php";
90 $angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php";
91 $angularModules['ui.utils'] = include "$civicrm_root/ang/ui.utils.ang.php";
92 $angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php";
93 $angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php";
94 $angularModules['statuspage'] = include "$civicrm_root/ang/crmStatusPage.ang.php";
95
96 foreach (\CRM_Core_Component::getEnabledComponents() as $component) {
97 $angularModules = array_merge($angularModules, $component->getAngularModules());
98 }
99 \CRM_Utils_Hook::angularModules($angularModules);
100 $this->modules = $this->resolvePatterns($angularModules);
101 }
102
103 return $this->modules;
104 }
105
106 /**
107 * Get the descriptor for an Angular module.
108 *
109 * @param string $name
110 * Module name.
111 * @return array
112 * Details about the module:
113 * - ext: string, the name of the Civi extension which defines the module
114 * - js: array(string $relativeFilePath).
115 * - css: array(string $relativeFilePath).
116 * - partials: array(string $relativeFilePath).
117 * @throws \Exception
118 */
119 public function getModule($name) {
120 $modules = $this->getModules();
121 if (!isset($modules[$name])) {
122 throw new \Exception("Unrecognized Angular module");
123 }
124 return $modules[$name];
125 }
126
127 /**
128 * Convert any globs in an Angular module to file names.
129 *
130 * @param array $modules
131 * List of Angular modules.
132 * @return array
133 * Updated list of Angular modules
134 */
135 protected function resolvePatterns($modules) {
136 $newModules = array();
137
138 foreach ($modules as $moduleKey => $module) {
139 foreach (array('js', 'css', 'partials') as $fileset) {
140 if (!isset($module[$fileset])) {
141 continue;
142 }
143 $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]);
144 }
145 $newModules[$moduleKey] = $module;
146 }
147
148 return $newModules;
149 }
150
151 /**
152 * Get the partial HTML documents for a module (unfiltered).
153 *
154 * @param string $name
155 * Angular module name.
156 * @return array
157 * Array(string $extFilePath => string $html)
158 * @throws \Exception
159 * Invalid partials configuration.
160 */
161 public function getRawPartials($name) {
162 $module = $this->getModule($name);
163 $result = array();
164 if (isset($module['partials'])) {
165 foreach ($module['partials'] as $partialDir) {
166 $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir;
167 $files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE);
168 foreach ($files as $file) {
169 $filename = '~/' . $name . '/' . $file;
170 $result[$filename] = file_get_contents($partialDir . '/' . $file);
171 }
172 }
173 return $result;
174 }
175 return $result;
176 }
177
178 /**
179 * Get the partial HTML documents for a module.
180 *
181 * @param string $name
182 * Angular module name.
183 * @return array
184 * Array(string $extFilePath => string $html)
185 * @throws \Exception
186 * Invalid partials configuration.
187 */
188 public function getPartials($name) {
189 $cacheKey = "angular-partials::$name";
190 $cacheValue = $this->cache->get($cacheKey);
191 if ($cacheValue === NULL) {
192 $cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
193 $this->cache->set($cacheKey, $cacheValue);
194 }
195 return $cacheValue;
196 }
197
198 /**
199 * Get list of translated strings for a module.
200 *
201 * @param string $name
202 * Angular module name.
203 * @return array
204 * Translated strings: array(string $orig => string $translated).
205 */
206 public function getTranslatedStrings($name) {
207 $module = $this->getModule($name);
208 $result = array();
209 $strings = $this->getStrings($name);
210 foreach ($strings as $string) {
211 // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]?
212 // It doesn't look like client side really supports the domain right now...
213 $translated = ts($string, array(
214 'domain' => array($module['ext'], NULL),
215 ));
216 if ($translated != $string) {
217 $result[$string] = $translated;
218 }
219 }
220 return $result;
221 }
222
223 /**
224 * Get list of translatable strings for a module.
225 *
226 * @param string $name
227 * Angular module name.
228 * @return array
229 * Translatable strings.
230 */
231 public function getStrings($name) {
232 $module = $this->getModule($name);
233 $result = array();
234 if (isset($module['js'])) {
235 foreach ($module['js'] as $file) {
236 $strings = $this->res->getStrings()->get(
237 $module['ext'],
238 $this->res->getPath($module['ext'], $file),
239 'text/javascript'
240 );
241 $result = array_unique(array_merge($result, $strings));
242 }
243 }
244 $partials = $this->getPartials($name);
245 foreach ($partials as $partial) {
246 $result = array_unique(array_merge($result, \CRM_Utils_JS::parseStrings($partial)));
247 }
248 return $result;
249 }
250
251 /**
252 * Get resources for one or more modules.
253 *
254 * @param string|array $moduleNames
255 * List of module names.
256 * @param string $resType
257 * Type of resource ('js', 'css', 'settings').
258 * @param string $refType
259 * Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings').
260 * @return array
261 * List of URLs or paths.
262 * @throws \CRM_Core_Exception
263 */
264 public function getResources($moduleNames, $resType, $refType) {
265 $result = array();
266 $moduleNames = (array) $moduleNames;
267 foreach ($moduleNames as $moduleName) {
268 $module = $this->getModule($moduleName);
269 if (isset($module[$resType])) {
270 foreach ($module[$resType] as $file) {
271 switch ($refType) {
272 case 'path':
273 $result[] = $this->res->getPath($module['ext'], $file);
274 break;
275
276 case 'rawUrl':
277 $result[] = $this->res->getUrl($module['ext'], $file);
278 break;
279
280 case 'cacheUrl':
281 $result[] = $this->res->getUrl($module['ext'], $file, TRUE);
282 break;
283
284 case 'settings':
285 case 'requires':
286 if (!empty($module[$resType])) {
287 $result[$moduleName] = $module[$resType];
288 }
289 break;
290
291 default:
292 throw new \CRM_Core_Exception("Unrecognized resource format");
293 }
294 }
295 }
296 }
297
298 return ChangeSet::applyResourceFilters($this->getChangeSets(), $resType, $result);
299 }
300
301 /**
302 * @return array
303 * Array(string $name => ChangeSet $changeSet).
304 */
305 public function getChangeSets() {
306 if ($this->changeSets === NULL) {
307 $this->changeSets = array();
308 \CRM_Utils_Hook::alterAngular($this);
309 }
310 return $this->changeSets;
311 }
312
313 /**
314 * @param ChangeSet $changeSet
315 * @return \Civi\Angular\Manager
316 */
317 public function add($changeSet) {
318 $this->changeSets[$changeSet->getName()] = $changeSet;
319 return $this;
320 }
321
322 }