diff --git a/libraries/cms/html/behavior.php b/libraries/cms/html/behavior.php
index 5e3fb6c71baa7..518f4335dacaf 100644
--- a/libraries/cms/html/behavior.php
+++ b/libraries/cms/html/behavior.php
@@ -68,7 +68,7 @@ public static function core()
}
HTMLHelper::_('form.csrf');
- Factory::getContainer()->get('webasset')->enableAsset('core');
+ Factory::getDocument()->getWebAssetManager()->enableAsset('core');
// Add core and base uri paths so javascript scripts can use them.
Factory::getDocument()->addScriptOptions(
@@ -140,7 +140,7 @@ public static function formvalidator()
Text::script('JLIB_FORM_FIELD_REQUIRED_CHECK');
Text::script('JLIB_FORM_FIELD_INVALID_VALUE');
- Factory::getContainer()->get('webasset')->enableAsset('fields.validate');
+ Factory::getDocument()->getWebAssetManager()->enableAsset('fields.validate');
static::$loaded[__METHOD__] = true;
}
@@ -169,7 +169,7 @@ public static function switcher()
*/
public static function combobox()
{
- Factory::getContainer()->get('webasset')->enableAsset('awesomplete');
+ Factory::getDocument()->getWebAssetManager()->enableAsset('awesomplete');
}
/**
@@ -247,7 +247,7 @@ public static function multiselect($id = 'adminForm')
return;
}
- Factory::getContainer()->get('webasset')->enableAsset('multiselect');
+ Factory::getDocument()->getWebAssetManager()->enableAsset('multiselect');
// Pass the required options to the javascript
Factory::getDocument()->addScriptOptions('js-multiselect', ['formName' => $id]);
@@ -426,7 +426,7 @@ public static function keepalive()
// Add keepalive script options.
Factory::getDocument()->addScriptOptions('system.keepalive', array('interval' => $refreshTime * 1000, 'uri' => Route::_($uri)));
- Factory::getContainer()->get('webasset')->enableAsset('keepalive');
+ Factory::getDocument()->getWebAssetManager()->enableAsset('keepalive');
static::$loaded[__METHOD__] = true;
diff --git a/libraries/cms/html/bootstrap.php b/libraries/cms/html/bootstrap.php
index 2984164a705a5..3e37cbd6f764f 100644
--- a/libraries/cms/html/bootstrap.php
+++ b/libraries/cms/html/bootstrap.php
@@ -159,7 +159,7 @@ public static function framework($debug = null)
$debug = (isset($debug) && $debug != JDEBUG) ? $debug : JDEBUG;
// Load the needed scripts
- Factory::getContainer()->get('webasset')
+ Factory::getDocument()->getWebAssetManager()
->enableAsset('core')
->enableAsset('bootstrap.js.bundle');
HTMLHelper::_('script', 'legacy/bootstrap-init.min.js', array('version' => 'auto', 'relative' => true, 'detectDebug' => $debug));
diff --git a/libraries/src/Service/Provider/WebAsset.php b/libraries/src/Service/Provider/WebAsset.php
index e465fcf445eeb..2cb3475f3baef 100644
--- a/libraries/src/Service/Provider/WebAsset.php
+++ b/libraries/src/Service/Provider/WebAsset.php
@@ -43,8 +43,8 @@ function (Container $container)
$registry->setDispatcher($container->get('Joomla\Event\DispatcherInterface'));
// Add Core registry files
- $registry->addRegistryFile('media/system/joomla.asset.json')
- ->addRegistryFile('media/vendor/joomla.asset.json')
+ $registry->addRegistryFile('media/vendor/joomla.asset.json')
+ ->addRegistryFile('media/system/joomla.asset.json')
->addRegistryFile('media/legacy/joomla.asset.json');
return $registry;
diff --git a/libraries/src/WebAsset/WebAssetItem.php b/libraries/src/WebAsset/WebAssetItem.php
index 80caad128bdfb..95a71276665ff 100644
--- a/libraries/src/WebAsset/WebAssetItem.php
+++ b/libraries/src/WebAsset/WebAssetItem.php
@@ -37,15 +37,6 @@ class WebAssetItem
*/
const ASSET_STATE_ACTIVE = 1;
- /**
- * Mark active asset. Enabled WITH all dependency
- *
- * @var integer
- *
- * @since __DEPLOY_VERSION__
- */
- const ASSET_STATE_RESOLVED = 2;
-
/**
* Mark active asset that is enabled as dependency to another asset
*
@@ -53,7 +44,7 @@ class WebAssetItem
*
* @since __DEPLOY_VERSION__
*/
- const ASSET_STATE_DEPENDANCY = 3;
+ const ASSET_STATE_DEPENDANCY = 2;
/**
* Asset state
diff --git a/libraries/src/WebAsset/WebAssetRegistry.php b/libraries/src/WebAsset/WebAssetRegistry.php
index f953e6379419c..9325a1c6b2646 100644
--- a/libraries/src/WebAsset/WebAssetRegistry.php
+++ b/libraries/src/WebAsset/WebAssetRegistry.php
@@ -168,8 +168,7 @@ function($asset)
}
);
- // Order them by weight and return
- return $assets ? $this->sortByWeight($assets) : [];
+ return $assets;
}
/**
@@ -192,8 +191,7 @@ function($asset) use ($state)
}
);
- // Order them by weight and return
- return $assets ? $this->sortByWeight($assets) : [];
+ return $assets;
}
/**
@@ -259,10 +257,10 @@ public function setAssetState(string $name, int $state = WebAssetItem::ASSET_STA
throw new \RuntimeException('Asset "' . $name . '" does not exist');
}
- $oldState = $asset->getState();
+ $currentState = $asset->getState();
// Asset already has the requested state
- if ($oldState === $state)
+ if ($currentState === $state)
{
return $this;
}
@@ -270,21 +268,17 @@ public function setAssetState(string $name, int $state = WebAssetItem::ASSET_STA
// Change state
$asset->setState($state);
- // Update last weight, to keep an order of enabled items
- if ($asset->isActive())
- {
- $this->lastItemWeight = $this->lastItemWeight + 1;
- $asset->setWeight($this->lastItemWeight);
- }
+ // Update Dependency
+ $this->updateDependency();
// Trigger the event
$event = AbstractEvent::create(
- 'onWebAssetStateChanged',
+ 'onWebAssetStateChangedExternally',
[
'eventClass' => 'Joomla\\CMS\\Event\\WebAsset\\WebAssetStateChangedEvent',
'subject' => $this,
'asset' => $asset,
- 'oldState' => $oldState,
+ 'oldState' => $currentState,
'newState' => $state,
]
);
@@ -333,7 +327,7 @@ public function disableAsset(string $name): self
public function attachActiveAssetsToDocument(Document $doc): self
{
// Resolve Dependency
- $this->resolveDependency();
+ $this->updateDependency()->calculateWeightOfActiveAssets();
// Trigger the event
$event = AbstractEvent::create(
@@ -346,7 +340,7 @@ public function attachActiveAssetsToDocument(Document $doc): self
);
$this->getDispatcher()->dispatch($event->getName(), $event);
- $assets = $this->getActiveAssets();
+ $assets = $this->sortAssetsByWeight($this->getActiveAssets());
// Pre-save existing Scripts, and attach them after requested assets.
$jsBackup = $doc->_scripts;
@@ -381,29 +375,33 @@ public function attachActiveAssetsToDocument(Document $doc): self
}
/**
- * Resolve Dependency for just added assets
+ * Update Dependencies state for all active Assets
*
* @return self
*
- * @throws \RuntimeException When Dependency cannot be resolved
- *
* @since __DEPLOY_VERSION__
*/
- protected function resolveDependency(): self
+ protected function updateDependency(): self
{
+ // First, deactivate all Dependency
+ foreach ($this->getAssetsByState(WebAssetItem::ASSET_STATE_DEPENDANCY) as $depItem)
+ {
+ $depItem->setState(WebAssetItem::ASSET_STATE_INACTIVE);
+ }
+
+ // Second, get list of active assets and enable their dependencies
$assets = $this->getAssetsByState(WebAssetItem::ASSET_STATE_ACTIVE);
foreach ($assets as $asset)
{
- $this->resolveItemDependency($asset);
- $asset->setState(WebAssetItem::ASSET_STATE_RESOLVED);
+ $this->updateItemDependency($asset);
}
return $this;
}
/**
- * Resolve Dependency for given asset
+ * Update Dependencies state for given Asset
*
* @param WebAssetItem $asset Asset instance
*
@@ -413,42 +411,98 @@ protected function resolveDependency(): self
*
* @since __DEPLOY_VERSION__
*/
- protected function resolveItemDependency(WebAssetItem $asset): self
+ protected function updateItemDependency(WebAssetItem $asset): self
{
- foreach ($this->getDependenciesForAsset($asset) as $depItem)
+ foreach ($this->getDependenciesForAsset($asset, true) as $depItem)
{
- $oldState = $depItem->isActive();
-
- // Make active
- if (!$oldState)
+ // Set dependency state only when it is inactive, to keep a manually activated Asset in their original state
+ if (!$depItem->isActive())
{
$depItem->setState(WebAssetItem::ASSET_STATE_DEPENDANCY);
}
+ }
+
+ return $this;
+ }
+
+ /**
+ * Calculate weight of active Assets, by its Dependencies
+ *
+ * @return self
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function calculateWeightOfActiveAssets(): self
+ {
+ // See https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
+ $result = [];
+ $graphOutgoing = [];
+ $graphIncoming = [];
+ $activeAssets = $this->getActiveAssets();
+
+ // Build Graphs of Outgoing and Incoming connections
+ foreach ($activeAssets as $asset)
+ {
+ $name = $asset->getName();
+ $graphOutgoing[$name] = array_combine($asset->getDependencies(), $asset->getDependencies());
+
+ if (!array_key_exists($name, $graphIncoming))
+ {
+ $graphIncoming[$name] = [];
+ }
- // Calculate weight, make it a bit lighter
- $depWeight = $depItem->getWeight();
- $assetWeight = $asset->getWeight();
+ foreach ($asset->getDependencies() as $depName)
+ {
+ $graphIncoming[$depName][$name] = $name;
+ }
+ }
- $depWeight = $depWeight === 0 ? $this->lastItemWeight : $depWeight;
- $weight = $depWeight > $assetWeight ? $assetWeight : $depWeight;
- $weight = $weight - 0.01;
+ // Find items without incoming connections
+ $emptyIncoming = array_keys(
+ array_filter(
+ $graphIncoming,
+ function ($el){
+ return !$el;
+ }
+ )
+ );
- $depItem->setWeight($weight);
+ // Loop through, and sort the graph
+ while ($emptyIncoming)
+ {
+ // Add the node without incoming connection to the result
+ $item = array_shift($emptyIncoming);
+ $result[] = $item;
- // Prevent duplicated work if Dependency was already activated
- if (!$oldState)
+ // Check of each neighbor of the node
+ foreach (array_reverse($graphOutgoing[$item]) as $neighbor)
{
- $this->resolveItemDependency($depItem);
+ // Remove incoming connection of already visited node
+ unset($graphIncoming[$neighbor][$item]);
+
+ // If there no more incoming connections add the node to queue
+ if (empty($graphIncoming[$neighbor]))
+ {
+ $emptyIncoming[] = $neighbor;
+ }
}
}
+ // Update a weight for each active asset
+ foreach (array_reverse($result) as $index => $name)
+ {
+ $activeAssets[$name]->setWeight($index + 1);
+ }
+
return $this;
}
/**
* Return dependancy for Asset as array of AssetItem objects
*
- * @param WebAssetItem $asset Asset instance
+ * @param WebAssetItem $asset Asset instance
+ * @param boolean $recursively Whether to search for dependancy recursively
+ * @param WebAssetItem $recursionRoot Initial item to prevent loop
*
* @return WebAssetItem[]
*
@@ -456,12 +510,19 @@ protected function resolveItemDependency(WebAssetItem $asset): self
*
* @since __DEPLOY_VERSION__
*/
- protected function getDependenciesForAsset(WebAssetItem $asset): array
+ protected function getDependenciesForAsset(WebAssetItem $asset, $recursively = false, WebAssetItem $recursionRoot = null): array
{
- $assets = [];
+ $assets = [];
+ $recursionRoot = $recursionRoot ?? $asset;
foreach ($asset->getDependencies() as $depName)
{
+ // Skip already loaded in recursion
+ if ($recursionRoot->getName() === $depName)
+ {
+ continue;
+ }
+
$dep = $this->getAsset($depName);
if (!$dep)
@@ -470,21 +531,29 @@ protected function getDependenciesForAsset(WebAssetItem $asset): array
}
$assets[$depName] = $dep;
+
+ if (!$recursively)
+ {
+ continue;
+ }
+
+ $parentDeps = $this->getDependenciesForAsset($dep, true, $recursionRoot);
+ $assets = array_replace($assets, $parentDeps);
}
return $assets;
}
/**
- * Sort assets by it`s weight
+ * Sort assets by its weight
*
- * @param WebAssetItem[] $assets Linked array of assets
+ * @param WebAssetItem[] $assets Array of assets to sort
*
* @return WebAssetItem[]
*
* @since __DEPLOY_VERSION__
*/
- protected function sortByWeight(array $assets): array
+ public function sortAssetsByWeight(array $assets): array
{
uasort(
$assets,
@@ -561,6 +630,11 @@ function($state) use ($constantIsNew)
}
);
+ if (!$files)
+ {
+ return;
+ }
+
foreach (array_keys($files) as $path)
{
$this->parseRegistryFile($path);
@@ -619,20 +693,28 @@ protected function parseRegistryFile($path)
/**
* Dump available assets to simple array, with some basic info
*
+ * @param bool $onlyActive Return only active Assets
+ *
* @return array
*
* @since __DEPLOY_VERSION__
*/
- public function debugAssets(): array
+ public function debugAssets(bool $onlyActive = false): array
{
- $assets = $this->assets;
+ // Update dependencies
+ $this->updateDependency()->calculateWeightOfActiveAssets();
+
+ $assets = $onlyActive ? $this->getActiveAssets() : $this->assets;
+ $assets = $this->sortAssetsByWeight($assets);
$result = [];
foreach ($assets as $asset)
{
$result[$asset->getName()] = [
- 'deps' => implode(', ', $asset->getDependencies()),
- 'state' => $asset->getState(),
+ 'name' => $asset->getName(),
+ 'deps' => implode(', ', $asset->getDependencies()),
+ 'state' => $asset->getState(),
+ 'weight' => $asset->getWeight(),
];
}