Let modules provide css/js assets to be used by other modules (#3961)

This commit is contained in:
Johannes Meyer 2019-09-25 09:53:53 +02:00 committed by GitHub
parent f98f988aff
commit d699191629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 291 additions and 9 deletions

View File

@ -4,24 +4,23 @@
namespace Icinga\Application\Modules;
use Exception;
use Zend_Controller_Router_Route;
use Zend_Controller_Router_Route_Abstract;
use Zend_Controller_Router_Route_Regex;
use Icinga\Application\ApplicationBootstrap;
use Icinga\Application\Config;
use Icinga\Application\Hook;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Application\Modules\DashboardContainer;
use Icinga\Application\Modules\MenuItemContainer;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Setup\SetupWizard;
use Icinga\Util\File;
use Icinga\Util\Translator;
use Icinga\Web\Controller\Dispatcher;
use Icinga\Application\Hook;
use Icinga\Web\Navigation\Navigation;
use Icinga\Web\Widget;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Zend_Controller_Router_Route;
use Zend_Controller_Router_Route_Abstract;
use Zend_Controller_Router_Route_Regex;
/**
* Module handling
@ -44,6 +43,13 @@ class Module
*/
private $basedir;
/**
* Directory for assets
*
* @var string
*/
private $assetDir;
/**
* Directory for styles
*
@ -184,6 +190,34 @@ class Module
*/
protected $jsFiles = array();
/**
* Globally provided CSS assets
*
* @var array
*/
protected $cssAssets = [];
/**
* Globally provided JS assets
*
* @var array
*/
protected $jsAssets = [];
/**
* Required CSS assets
*
* @var array
*/
protected $cssRequires = [];
/**
* Required JS assets
*
* @var array
*/
protected $jsRequires = [];
/**
* Routes to add to the route chain
*
@ -247,6 +281,7 @@ class Module
$this->app = $app;
$this->name = $name;
$this->basedir = $basedir;
$this->assetDir = $basedir . '/asset';
$this->cssdir = $basedir . '/public/css';
$this->jsdir = $basedir . '/public/js';
$this->libdir = $basedir . '/library';
@ -435,6 +470,7 @@ class Module
return false;
}
$this->registerWebIntegration();
$this->registerAssets();
$this->registered = true;
return true;
@ -468,7 +504,7 @@ class Module
* @param string $name
* @param bool $autoload
*
* @return mixed
* @return self
*
* @throws ProgrammingError When the module is not yet loaded
*/
@ -484,6 +520,162 @@ class Module
return $manager->getModule($name);
}
/**
* Provide a static CSS asset which can be required by other modules
*
* @param string $path The path, relative to the module's base
*
* @return $this
*/
protected function provideCssAsset($path)
{
$fullPath = join(DIRECTORY_SEPARATOR, [$this->basedir, $path]);
$this->cssAssets[] = $fullPath;
$this->cssRequires[] = $fullPath; // A module should not have to require its own assets
return $this;
}
/**
* Get the CSS assets provided by this module
*
* @return array
*/
public function getCssAssets()
{
return $this->cssAssets;
}
/**
* Require CSS from a different module
*
* @param string $path The file's path, relative to the module's asset or base directory
* @param string $from The module's name
*
* @return $this
*/
protected function requireCssFile($path, $from)
{
$module = self::get($from);
$cssAssetDir = join(DIRECTORY_SEPARATOR, [$module->assetDir, 'css']);
foreach ($module->getCssAssets() as $assetPath) {
if (substr($assetPath, 0, strlen($cssAssetDir)) === $cssAssetDir) {
$relativePath = ltrim(substr($assetPath, strlen($cssAssetDir)), '/\\');
} else {
$relativePath = ltrim(substr($assetPath, strlen($module->basedir)), '/\\');
}
if ($path === $relativePath) {
$this->cssRequires[] = $assetPath;
break; // Exact match, won't match again..
} elseif (fnmatch($path, $relativePath)) {
$this->cssRequires[] = $assetPath;
}
}
return $this;
}
/**
* Check whether this module requires CSS from a different module
*
* @return bool
*/
public function requiresCss()
{
$this->launchConfigScript();
return ! empty($this->cssRequires);
}
/**
* List the CSS assets required by this module
*
* @return array
*/
public function getCssRequires()
{
$this->launchConfigScript();
return $this->cssRequires;
}
/**
* Provide a static Javascript asset which can be required by other modules
*
* @param string $path The path, relative to the module's base
*
* @return $this
*/
protected function provideJsAsset($path)
{
$fullPath = join(DIRECTORY_SEPARATOR, [$this->basedir, $path]);
$this->jsAssets[] = $fullPath;
$this->jsRequires[] = $fullPath; // A module should not have to require its own assets
return $this;
}
/**
* Get the Javascript assets provided by this module
*
* @return array
*/
public function getJsAssets()
{
return $this->jsAssets;
}
/**
* Require Javascript from a different module
*
* @param string $path The file's path, relative to the module's asset or base directory
* @param string $from The module's name
*
* @return $this
*/
protected function requireJsFile($path, $from)
{
$module = self::get($from);
$jsAssetDir = join(DIRECTORY_SEPARATOR, [$module->assetDir, 'js']);
foreach ($module->getJsAssets() as $assetPath) {
if (substr($assetPath, 0, strlen($jsAssetDir)) === $jsAssetDir) {
$relativePath = ltrim(substr($assetPath, strlen($jsAssetDir)), '/\\');
} else {
$relativePath = ltrim(substr($assetPath, strlen($module->basedir)), '/\\');
}
if ($path === $relativePath) {
$this->jsRequires[] = $assetPath;
break; // Exact match, won't match again..
} elseif (fnmatch($path, $relativePath)) {
$this->jsRequires[] = $assetPath;
}
}
return $this;
}
/**
* Check whether this module requires Javascript from a different module
*
* @return bool
*/
public function requiresJs()
{
$this->launchConfigScript();
return ! empty($this->jsRequires);
}
/**
* List the Javascript assets required by this module
*
* @return array
*/
public function getJsRequires()
{
$this->launchConfigScript();
return $this->jsRequires;
}
/**
* Provide an additional CSS/LESS file
*
@ -1095,6 +1287,40 @@ class Module
return $this;
}
/**
* Register this module's assets
*
* @return $this
*/
protected function registerAssets()
{
if ($this->app->isCli() || ! is_dir($this->assetDir)) {
return $this;
}
$listAssets = function ($type) {
$dir = join(DIRECTORY_SEPARATOR, [$this->assetDir, $type]);
if (! is_dir($dir)) {
return [];
}
return new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
$dir,
RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
));
};
foreach ($listAssets('css') as $assetPath) {
$this->provideCssAsset(ltrim(substr($assetPath, strlen($this->basedir)), '/\\'));
}
foreach ($listAssets('js') as $assetPath) {
$this->provideJsAsset(ltrim(substr($assetPath, strlen($this->basedir)), '/\\'));
}
return $this;
}
/**
* Bind text domain for i18n
*

View File

@ -76,6 +76,7 @@ class JavaScript
$jsFiles[] = $basedir . '/' . $file;
}
$sharedFiles = [];
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $name => $module) {
if ($module->hasJs()) {
foreach ($module->getJsFiles() as $path) {
@ -84,8 +85,16 @@ class JavaScript
}
}
}
if ($module->requiresJs()) {
foreach ($module->getJsRequires() as $path) {
$sharedFiles[] = $path;
}
}
}
$files = array_merge($vendorFiles, $jsFiles);
$sharedFiles = array_unique($sharedFiles);
$files = array_merge($vendorFiles, $jsFiles, $sharedFiles);
$request = Icinga::app()->getRequest();
$noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache';
@ -116,6 +125,14 @@ class JavaScript
$js .= file_get_contents($file) . "\n\n\n";
}
foreach ($sharedFiles as $file) {
if (substr($file, -7, 7) === '.min.js') {
$out .= ';' . ltrim(trim(file_get_contents($file)), ';') . "\n";
} else {
$js .= file_get_contents($file) . "\n\n\n";
}
}
if ($minified) {
require_once 'JShrink/Minifier.php';
$out .= Minifier::minify($js, array('flaggedComments' => false));

View File

@ -36,6 +36,13 @@ class LessCompiler
*/
protected $moduleLessFiles = array();
/**
* Array of module names indexed by LESS asset paths
*
* @var array
*/
protected $moduleRequires = [];
/**
* LESS source
*
@ -91,6 +98,20 @@ class LessCompiler
return $this;
}
/**
* Add a LESS asset requirement
*
* @param string $moduleName
* @param string $lessFile
*
* @return $this
*/
public function addModuleRequire($moduleName, $lessFile)
{
$this->moduleRequires[$lessFile][] = $moduleName;
return $this;
}
/**
* Get the list of LESS files added to the compiler
*
@ -104,6 +125,8 @@ class LessCompiler
$lessFiles = array_merge($lessFiles, $moduleLessFiles);
}
$lessFiles = array_merge($lessFiles, array_keys($this->moduleRequires));
if ($this->theme !== null) {
$lessFiles[] = $this->theme;
}
@ -149,6 +172,16 @@ class LessCompiler
$this->source .= file_get_contents($lessFile);
}
$requireCss = '';
foreach ($this->moduleRequires as $requiredFile => $modules) {
$containers = array_map(function ($name) {
return '.icinga-module.module-' . $name;
}, $modules);
$requireCss .= join(',', $containers) . ' {' . file_get_contents($requiredFile) . '}';
}
$this->source .= $requireCss;
$moduleCss = '';
foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
$moduleCss .= '.icinga-module.module-' . $moduleName . ' {';

View File

@ -104,6 +104,12 @@ class StyleSheet
$this->lessCompiler->addModuleLessFile($moduleName, $lessFilePath);
}
}
if ($module->requiresCss()) {
foreach ($module->getCssRequires() as $lessFilePath) {
$this->lessCompiler->addModuleRequire($moduleName, $lessFilePath);
}
}
}
$themingConfig = $this->app->getConfig()->getSection('themes');