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(), ]; }