diff --git a/CRM/Core/Resources.php b/CRM/Core/Resources.php index f8c00772ae59..87cb3c79c92f 100644 --- a/CRM/Core/Resources.php +++ b/CRM/Core/Resources.php @@ -528,6 +528,9 @@ public function glob($ext, $patterns, $flags = NULL) { $patterns = (array) $patterns; $files = array(); foreach ($patterns as $pattern) { + if (preg_match(';^(assetBuilder|ext)://;', $pattern)) { + $files[] = $pattern; + } if (CRM_Utils_File::isAbsolute($pattern)) { // Absolute path. $files = array_merge($files, (array) glob($pattern, $flags)); diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index 4c1163493e95..4f0b0c371325 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -2110,8 +2110,8 @@ public static function alterResourceSettings(&$data) { * ); * $angularModules['myBigAngularModule'] = array( * 'ext' => 'org.example.mymod', - * 'js' => array('js/part1.js', 'js/part2.js'), - * 'css' => array('css/myAngularModule.css'), + * 'js' => array('js/part1.js', 'js/part2.js', 'ext://other.ext.name/file.js', 'assetBuilder://dynamicAsset.js'), + * 'css' => array('css/myAngularModule.css', 'ext://other.ext.name/file.css', 'assetBuilder://dynamicAsset.css'), * 'partials' => array('partials/myBigAngularModule'), * 'requires' => array('otherModuleA', 'otherModuleB'), * 'basePages' => array('civicrm/a'), diff --git a/CRM/Utils/VisualBundle.php b/CRM/Utils/VisualBundle.php new file mode 100644 index 000000000000..27128b176096 --- /dev/null +++ b/CRM/Utils/VisualBundle.php @@ -0,0 +1,110 @@ +addScriptUrl(Civi::service('asset_manager')->getUrl('visual-bundle.js')); + Civi::resources()->addStyleUrl(Civi::service('asset_manager')->getUrl('visual-bundle.css')); + } + + /** + * Generate asset content (when accessed via AssetBuilder). + * + * @param \Civi\Core\Event\GenericHookEvent $event + * @see CRM_Utils_hook::buildAsset() + * @see \Civi\Core\AssetBuilder + */ + public static function buildAssetJs($event) { + if ($event->asset !== 'visual-bundle.js') { + return; + } + + $files = array( + 'crossfilter' => '[civicrm.bower]/crossfilter-1.3.x/crossfilter.min.js', + 'd3' => '[civicrm.bower]/d3-3.5.x/d3.min.js', + 'dc' => '[civicrm.bower]/dc-2.1.x/dc.min.js', + ); + + $content = array(); + $content[] = "(function(){"; + $content[] = "var backups = {d3: window.d3, crossfilter: window.crossfilter, dc: window.dc}"; + $content[] = 'window.CRM = window.CRM || {};'; + $content[] = 'CRM.visual = CRM.visual || {};'; + foreach ($files as $var => $file) { + $content[] = "// File: $file"; + $content[] = file_get_contents(Civi::paths()->getPath($file)); + } + foreach ($files as $var => $file) { + $content[] = "CRM.visual.$var = $var;"; + } + foreach ($files as $var => $file) { + $content[] = "window.$var = backups.$var;"; + } + $content[] = "})();"; + + $event->mimeType = 'application/javascript'; + $event->content = implode("\n", $content); + } + + /** + * Generate asset content (when accessed via AssetBuilder). + * + * @param \Civi\Core\Event\GenericHookEvent $event + * @see CRM_Utils_hook::buildAsset() + * @see \Civi\Core\AssetBuilder + */ + public static function buildAssetCss($event) { + if ($event->asset !== 'visual-bundle.css') { + return; + } + + $files = array( + '[civicrm.bower]/dc-2.1.x/dc.min.css', + ); + + $content = array(); + foreach ($files as $file) { + $content[] = "// File: $file"; + $content[] = file_get_contents(Civi::paths()->getPath($file)); + } + + $event->mimeType = 'text/css'; + $event->content = implode("\n", $content); + } + +} diff --git a/Civi/Angular/Manager.php b/Civi/Angular/Manager.php index 3f4776205156..a10a8096f5f8 100644 --- a/Civi/Angular/Manager.php +++ b/Civi/Angular/Manager.php @@ -327,7 +327,12 @@ public function getResources($moduleNames, $resType, $refType) { $module = $this->getModule($moduleName); if (isset($module[$resType])) { foreach ($module[$resType] as $file) { - switch ($refType) { + $refTypeSuffix = ''; + if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) { + $refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME); + } + + switch ($refType . $refTypeSuffix) { case 'path': $result[] = $this->res->getPath($module['ext'], $file); break; @@ -340,6 +345,33 @@ public function getResources($moduleNames, $resType, $refType) { $result[] = $this->res->getUrl($module['ext'], $file, TRUE); break; + case 'path-assetBuilder': + $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); + $assetParams = array(); + parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); + $result[] = \Civi::service('asset_builder')->getPath($assetName, $assetParams); + break; + + case 'rawUrl-assetBuilder': + case 'cacheUrl-assetBuilder': + $assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH); + $assetParams = array(); + parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams); + $result[] = \Civi::service('asset_builder')->getUrl($assetName, $assetParams); + break; + + case 'path-ext': + $result[] = $this->res->getPath(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); + break; + + case 'rawUrl-ext': + $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/')); + break; + + case 'cacheUrl-ext': + $result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'), TRUE); + break; + case 'settings': case 'requires': if (!empty($module[$resType])) { diff --git a/Civi/Core/AssetBuilder.php b/Civi/Core/AssetBuilder.php index 5c3c8263b072..5f79555650c9 100644 --- a/Civi/Core/AssetBuilder.php +++ b/Civi/Core/AssetBuilder.php @@ -139,6 +139,23 @@ public function getUrl($name, $params = array()) { } } + /** + * @param string $name + * Ex: 'angular.json'. + * @param array $params + * @return string + * URL. + * Ex: '/var/www/files/civicrm/dyn/angular.abcd1234abcd1234.json'. + */ + public function getPath($name, $params = array()) { + if (!$this->isValidName($name)) { + throw new \RuntimeException("Invalid dynamic asset name"); + } + + $fileName = $this->build($name, $params); + return $this->getCachePath($fileName); + } + /** * Build the cached copy of an $asset. * diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 23a5108f380d..ffe1bfbe3bcc 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -298,6 +298,8 @@ public function createEventDispatcher($container) { $dispatcher->addListener('hook_civicrm_eventDefs', array('\Civi\API\Events', 'hookEventDefs')); $dispatcher->addListener('hook_civicrm_eventDefs', array('\Civi\Core\Event\SystemInstallEvent', 'hookEventDefs')); $dispatcher->addListener('hook_civicrm_buildAsset', array('\Civi\Angular\Page\Modules', 'buildAngularModules')); + $dispatcher->addListener('hook_civicrm_buildAsset', array('\CRM_Utils_VisualBundle', 'buildAssetJs')); + $dispatcher->addListener('hook_civicrm_buildAsset', array('\CRM_Utils_VisualBundle', 'buildAssetCss')); $dispatcher->addListener('civi.dao.postInsert', array('\CRM_Core_BAO_RecurringEntity', 'triggerInsert')); $dispatcher->addListener('civi.dao.postUpdate', array('\CRM_Core_BAO_RecurringEntity', 'triggerUpdate')); $dispatcher->addListener('civi.dao.postDelete', array('\CRM_Core_BAO_RecurringEntity', 'triggerDelete')); diff --git a/bower.json b/bower.json index 343c2fb96fbf..3011e5055283 100644 --- a/bower.json +++ b/bower.json @@ -15,6 +15,9 @@ "angular-unsavedChanges": "~0.1.1", "qunit": "~1.10", "d3": "3.4.11", + "d3-3.5.x": "d3#~3.5.17", + "dc-2.1.x": "dc.js#~2.1.8", + "crossfilter-1.3.x": "crossfilter2#~1.3.11", "jquery": "~1.12", "jquery-ui": "~1.12", "lodash-compat": "~3.0", diff --git a/tests/phpunit/Civi/Angular/ManagerTest.php b/tests/phpunit/Civi/Angular/ManagerTest.php index 3174a3518467..23572cc6736e 100644 --- a/tests/phpunit/Civi/Angular/ManagerTest.php +++ b/tests/phpunit/Civi/Angular/ManagerTest.php @@ -124,6 +124,17 @@ public function testGetPartials_Hooked() { // If crmMailing changes, feel free to use a different example. } + public function testGetJs_Asset() { + \CRM_Utils_Hook::singleton()->setHook('civicrm_angularModules', array($this, 'hook_civicrm_angularModules_fooBar')); + + $paths = $this->angular->getResources(array('fooBar'), 'js', 'path'); + $this->assertRegExp('/visual-bundle.[a-z0-9]+.js/', $paths[0]); + $this->assertRegExp('/crossfilter/', file_get_contents($paths[0])); + + $this->assertRegExp('/Common.js/', $paths[1]); + $this->assertRegExp('/console/', file_get_contents($paths[1])); + } + /** * Get a translatable string from an example module. */ @@ -209,4 +220,14 @@ public function hook_civicrm_alterAngular($angular) { ); } + public function hook_civicrm_angularModules_fooBar(&$angularModules) { + $angularModules['fooBar'] = array( + 'ext' => 'civicrm', + 'js' => array( + 'assetBuilder://visual-bundle.js', + 'ext://civicrm/js/Common.js', + ), + ); + } + }