diff --git a/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php b/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php index 7e2fd4ea0fab5..5365b17ee7117 100644 --- a/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php +++ b/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php @@ -137,7 +137,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php b/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php index 80fac83fdd7db..cf5fc5777f12d 100644 --- a/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php +++ b/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php @@ -75,7 +75,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/administrator/components/com_modules/tmpl/modules/default_batch_body.php b/administrator/components/com_modules/tmpl/modules/default_batch_body.php index 9174e50228baa..dec388195cf35 100644 --- a/administrator/components/com_modules/tmpl/modules/default_batch_body.php +++ b/administrator/components/com_modules/tmpl/modules/default_batch_body.php @@ -35,7 +35,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); $this->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select') ->useScript('joomla.batch-copymove'); diff --git a/administrator/components/com_templates/tmpl/template/default.php b/administrator/components/com_templates/tmpl/template/default.php index 410f9ea12a827..b94c1b4d96310 100644 --- a/administrator/components/com_templates/tmpl/template/default.php +++ b/administrator/components/com_templates/tmpl/template/default.php @@ -46,7 +46,7 @@ } if ($this->type == 'image') { - $wa->usePreset('cropperjs'); + $wa->useScript('cropperjs'); } if ($this->type == 'font') { diff --git a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php index 47f715e29e95b..ad62afb2a4142 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php @@ -17,7 +17,7 @@ /** @var \Joomla\Component\Templates\Administrator\View\Template\HtmlView $this */ -Factory::getDocument()->getWebAssetManager()->usePreset('choicesjs'); +Factory::getDocument()->getWebAssetManager()->useScript('choicesjs'); // Generate a list of styles for the child creation modal $options = []; diff --git a/build/build-modules-js/settings.json b/build/build-modules-js/settings.json index 9dbb3218c92c2..5dd6ac3e101b6 100644 --- a/build/build-modules-js/settings.json +++ b/build/build-modules-js/settings.json @@ -26,7 +26,10 @@ "uri": "awesomplete.min.js", "attributes": { "defer": true - } + }, + "crossDependencies": [ + "awesomplete#style" + ] }, { "name": "awesomplete", @@ -34,7 +37,9 @@ "dependencies": [ "awesomplete#style", "awesomplete#script" - ] + ], + "deprecated": true, + "deprecatedMsg": "Use script asset directly. Will be removed in 7.0" } ], "dependencies": [], @@ -219,7 +224,10 @@ "name": "cropper-module", "type": "script", "uri": "cropper.esm.js", - "importmap": true + "importmap": true, + "crossDependencies": [ + "cropperjs#style" + ] }, { "name": "cropperjs", @@ -228,7 +236,10 @@ "uri": "cropper.min.js", "attributes": { "defer": true - } + }, + "crossDependencies": [ + "cropperjs#style" + ] }, { "name": "cropperjs", @@ -236,7 +247,9 @@ "dependencies": [ "cropperjs#style", "cropperjs#script" - ] + ], + "deprecated": true, + "deprecatedMsg": "Use script asset directly. Will be removed in 7.0" } ], "dependencies": [], @@ -268,7 +281,10 @@ "uri": "choices.min.js", "attributes": { "defer": true - } + }, + "crossDependencies": [ + "choicesjs#style" + ] }, { "name": "choicesjs", @@ -276,7 +292,9 @@ "dependencies": [ "choicesjs#style", "choicesjs#script" - ] + ], + "deprecated": true, + "deprecatedMsg": "Use script asset directly. Will be removed in 7.0" } ], "dependencies": [], @@ -322,7 +340,10 @@ "uri": "dragula.min.js", "attributes": { "defer": true - } + }, + "crossDependencies": [ + "dragula#style" + ] }, { "name": "dragula", @@ -330,7 +351,9 @@ "dependencies": [ "dragula#style", "dragula#script" - ] + ], + "deprecated": true, + "deprecatedMsg": "Use script asset directly. Will be removed in 7.0" } ], "dependencies": [], diff --git a/components/com_finder/tmpl/search/default_form.php b/components/com_finder/tmpl/search/default_form.php index c6130453e5852..fd61d068cbb4c 100644 --- a/components/com_finder/tmpl/search/default_form.php +++ b/components/com_finder/tmpl/search/default_form.php @@ -19,7 +19,7 @@ * This segment of code sets up the autocompleter. */ if ($this->params->get('show_autosuggest', 1)) { - $this->getDocument()->getWebAssetManager()->usePreset('awesomplete'); + $this->getDocument()->getWebAssetManager()->useScript('awesomplete'); $this->getDocument()->addScriptOptions('finder-search', ['url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false)]); Text::script('COM_FINDER_SEARCH_FORM_LIST_LABEL'); diff --git a/layouts/joomla/form/field/groupedlist-fancy-select.php b/layouts/joomla/form/field/groupedlist-fancy-select.php index 35df462ab80c9..0a695c16d39a3 100644 --- a/layouts/joomla/form/field/groupedlist-fancy-select.php +++ b/layouts/joomla/form/field/groupedlist-fancy-select.php @@ -114,7 +114,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/list-fancy-select.php b/layouts/joomla/form/field/list-fancy-select.php index f117d34a9a96b..9dae7d005f64b 100644 --- a/layouts/joomla/form/field/list-fancy-select.php +++ b/layouts/joomla/form/field/list-fancy-select.php @@ -96,7 +96,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/tag.php b/layouts/joomla/form/field/tag.php index ea6c458af2c64..4761b504c33e6 100644 --- a/layouts/joomla/form/field/tag.php +++ b/layouts/joomla/form/field/tag.php @@ -115,7 +115,6 @@ Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index b1819c578dec8..f62275c551344 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -10,6 +10,7 @@ namespace Joomla\CMS\Document\Renderer\Html; use Joomla\CMS\Document\DocumentRenderer; +use Joomla\CMS\WebAsset\WebAssetItemCrossDependenciesInterface; use Joomla\CMS\WebAsset\WebAssetItemInterface; // phpcs:disable PSR1.Files.SideEffects @@ -163,6 +164,10 @@ private function renderElement($item): string $attribs['data-asset-dependencies'] = implode(',', $asset->getDependencies()); } + if ($asset instanceof WebAssetItemCrossDependenciesInterface && $asset->getCrossDependencies()) { + $attribs['data-asset-cross-dependencies'] = str_replace('"', '', json_encode($asset->getCrossDependencies())); + } + if ($asset->getOption('deprecated')) { @trigger_error( \sprintf('Web Asset script [%s] is deprecated. %s', $asset->getName(), $asset->getOption('deprecatedMsg', '')), diff --git a/libraries/src/HTML/Helpers/DraggableList.php b/libraries/src/HTML/Helpers/DraggableList.php index ebb8a27415449..e0380ac26793d 100644 --- a/libraries/src/HTML/Helpers/DraggableList.php +++ b/libraries/src/HTML/Helpers/DraggableList.php @@ -75,7 +75,6 @@ public static function draggable( } $doc->getWebAssetManager() - ->usePreset('dragula') ->useScript('joomla.draggable'); // Set static array diff --git a/libraries/src/WebAsset/WebAssetItem.php b/libraries/src/WebAsset/WebAssetItem.php index 2632f5cc2efe5..cdc1cd1efab07 100644 --- a/libraries/src/WebAsset/WebAssetItem.php +++ b/libraries/src/WebAsset/WebAssetItem.php @@ -25,7 +25,7 @@ * * @since 4.0.0 */ -class WebAssetItem implements WebAssetItemInterface +class WebAssetItem implements WebAssetItemInterface, WebAssetItemCrossDependenciesInterface { /** * Asset name @@ -67,6 +67,22 @@ class WebAssetItem implements WebAssetItemInterface */ protected $dependencies = []; + /** + * Unparsed cross dependencies + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + private $rawCrossDependencies = []; + + /** + * Asset cross dependencies + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $crossDependencies = []; + /** * Asset version * @@ -78,11 +94,12 @@ class WebAssetItem implements WebAssetItemInterface /** * Class constructor * - * @param string $name The asset name - * @param ?string $uri The URI for the asset - * @param array $options Additional options for the asset - * @param array $attributes Attributes for the asset - * @param array $dependencies Asset dependencies + * @param string $name The asset name + * @param ?string $uri The URI for the asset + * @param array $options Additional options for the asset + * @param array $attributes Attributes for the asset + * @param array $dependencies Asset dependencies, from assets of the same type + * @param array $crossDependencies Asset dependencies, from assets of another type * * @since 4.0.0 */ @@ -91,7 +108,8 @@ public function __construct( ?string $uri = null, array $options = [], array $attributes = [], - array $dependencies = [] + array $dependencies = [], + array $crossDependencies = [] ) { $this->name = $name; $this->uri = $uri; @@ -115,6 +133,13 @@ public function __construct( $this->dependencies = $dependencies; } + if (\array_key_exists('crossDependencies', $options)) { + $this->rawCrossDependencies = (array) $options['crossDependencies']; + unset($options['crossDependencies']); + } else { + $this->rawCrossDependencies = $crossDependencies; + } + $this->options = $options; } @@ -154,6 +179,46 @@ public function getDependencies(): array return $this->dependencies; } + /** + * Return associative list of cross dependencies. + * Example: ['script' => ['script1', 'script2'], 'style' => ['style1', 'style2']] + * + * @return array[] + * + * @since __DEPLOY_VERSION__ + */ + public function getCrossDependencies(): array + { + if ($this->rawCrossDependencies && !$this->crossDependencies) { + // Cross Dependencies as an associative array + if (!\is_int(key($this->rawCrossDependencies))) { + $this->crossDependencies = $this->rawCrossDependencies; + } else { + // Parse Cross Dependencies which comes in ["name#type"] format + foreach ($this->rawCrossDependencies as $crossDependency) { + $pos = strrpos($crossDependency, '#'); + $depType = $pos ? substr($crossDependency, $pos + 1) : ''; + $depName = $pos ? substr($crossDependency, 0, $pos) : ''; + + if (!$depType || !$depName) { + throw new \UnexpectedValueException( + \sprintf('Incomplete definition for cross dependency, for asset "%s"', $this->getName()) + ); + } + + if (empty($this->crossDependencies[$depType])) { + $this->crossDependencies[$depType] = []; + } + + $this->crossDependencies[$depType][] = $depName; + } + } + $this->rawCrossDependencies = []; + } + + return $this->crossDependencies; + } + /** * Get the file path * diff --git a/libraries/src/WebAsset/WebAssetItemCrossDependenciesInterface.php b/libraries/src/WebAsset/WebAssetItemCrossDependenciesInterface.php new file mode 100644 index 0000000000000..a5b3f97a61942 --- /dev/null +++ b/libraries/src/WebAsset/WebAssetItemCrossDependenciesInterface.php @@ -0,0 +1,34 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\WebAsset; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Interface for Web Asset Item with cross dependencies + * + * Asset Item are "read only" object, all properties must be set through class constructor. + * + * @since __DEPLOY_VERSION__ + */ +interface WebAssetItemCrossDependenciesInterface +{ + /** + * Return associative list of cross dependencies. + * Example: ['script' => ['script1', 'script2'], 'style' => ['style1', 'style2']] + * + * @return array[] + * + * @since __DEPLOY_VERSION__ + */ + public function getCrossDependencies(): array; +} diff --git a/libraries/src/WebAsset/WebAssetManager.php b/libraries/src/WebAsset/WebAssetManager.php index 3b9d4df77f1f6..1326cafb38386 100644 --- a/libraries/src/WebAsset/WebAssetManager.php +++ b/libraries/src/WebAsset/WebAssetManager.php @@ -70,6 +70,15 @@ class WebAssetManager implements WebAssetManagerInterface */ public const ASSET_STATE_DEPENDENCY = 2; + /** + * Mark active asset that is enabled as cross dependency to another asset + * + * @var integer + * + * @since __DEPLOY_VERSION__ + */ + public const ASSET_STATE_CROSS_DEPENDENCY = 3; + /** * The WebAsset Registry instance * @@ -390,6 +399,27 @@ protected function usePresetItems($name): WebAssetManagerInterface $this->useAsset($depType, $depName); } + // Call useAsset() to each of its crossDependency + if ($asset instanceof WebAssetItemCrossDependenciesInterface && $asset->getCrossDependencies()) { + foreach ($asset->getCrossDependencies() as $depType => $depItems) { + foreach ($depItems as $depName) { + // Make sure dependency exists + if (!$this->registry->exists($depType, $depName)) { + throw new UnsatisfiedDependencyException( + \sprintf( + 'Unsatisfied dependency "%s" for an asset "%s" of type "%s"', + $depName . '#' . $depType, + $name, + 'preset' + ) + ); + } + + $this->useAsset($depType, $depName); + } + } + } + return $this; } @@ -769,14 +799,17 @@ protected function enableDependencies(?string $type = null, ?WebAssetItem $asset if ($asset) { // Get all dependencies of given asset recursively - $allDependencies = $this->getDependenciesForAsset($type, $asset, true); + $allDependencies = $this->getAllDependenciesForAsset($type, $asset, true); foreach ($allDependencies as $depType => $depItems) { foreach ($depItems as $depItem) { // Set dependency state only when it is inactive, to keep a manually activated Asset in their original state if (empty($this->activeAssets[$depType][$depItem->getName()])) { // Add the dependency at the top of the list of active assets - $this->activeAssets[$depType] = [$depItem->getName() => static::ASSET_STATE_DEPENDENCY] + $this->activeAssets[$depType]; + $this->activeAssets[$depType] = [ + // Try to distinguish a cross dependency from a regular one, however it not always accurate + $depItem->getName() => $depType === $type ? static::ASSET_STATE_DEPENDENCY : static::ASSET_STATE_CROSS_DEPENDENCY, + ] + $this->activeAssets[$depType]; } } } @@ -982,6 +1015,8 @@ protected function getConnectionsGraph(array $assets): array * @throws UnsatisfiedDependencyException When Dependency cannot be found * * @since 4.0.0 + * + * @deprecated __DEPLOY_VERSION__ Use getAllDependenciesForAsset() instead. Will be removed in 8.0. */ protected function getDependenciesForAsset( string $type, @@ -990,18 +1025,41 @@ protected function getDependenciesForAsset( ?string $recursionType = null, ?WebAssetItem $recursionRoot = null ): array { - $assets = []; - $recursionRoot ??= $asset; - $recursionType ??= $type; + return $this->getAllDependenciesForAsset($type, $asset, $recursively); + } + + /** + * Return dependencies for Asset as associative array of WebAssetItem objects, grouped per type + * + * @param string $type The asset type, script or style + * @param WebAssetItemInterface $asset Asset instance + * @param boolean $recursively Whether to search for dependency recursively + * @param array $visited The list of checked assets visited while resolving recursive dependencies + * + * @return array + * + * @throws UnsatisfiedDependencyException When Dependency cannot be found + * + * @since __DEPLOY_VERSION__ + */ + protected function getAllDependenciesForAsset( + string $type, + WebAssetItemInterface $asset, + bool $recursively = false, + array &$visited = [] + ): array { + $assets = []; + + if (!empty($visited[$type][$asset->getName()])) { + return $assets; + } + + $visited[$type][$asset->getName()] = true; + // Check for regular dependencies foreach ($asset->getDependencies() as $depName) { $depType = $type; - // Skip already loaded in recursion - if ($recursionRoot->getName() === $depName && $recursionType === $depType) { - continue; - } - if (!$this->registry->exists($depType, $depName)) { throw new UnsatisfiedDependencyException( \sprintf('Unsatisfied dependency "%s" for an asset "%s" of type "%s"', $depName, $asset->getName(), $depType) @@ -1016,8 +1074,40 @@ protected function getDependenciesForAsset( continue; } - $parentDeps = $this->getDependenciesForAsset($depType, $dep, true, $recursionType, $recursionRoot); - $assets = array_replace_recursive($assets, $parentDeps); + $parentDeps = $this->getAllDependenciesForAsset($depType, $dep, true, $visited); + $assets = $parentDeps ? array_replace_recursive($assets, $parentDeps) : $assets; + } + + // Check for cross dependencies + if ($asset instanceof WebAssetItemCrossDependenciesInterface && $asset->getCrossDependencies()) { + // Loop through each type + foreach ($asset->getCrossDependencies() as $crossType => $crossDependencies) { + // Ignore the same type, it should be defined as "dependencies" and not as "crossDependencies" + if ($crossType === $type) { + continue; + } + + foreach ($crossDependencies as $depName) { + $depType = $crossType; + + if (!$this->registry->exists($depType, $depName)) { + throw new UnsatisfiedDependencyException( + \sprintf('Unsatisfied cross dependency "%s" for an asset "%s" of type "%s"', $depName, $asset->getName(), $depType) + ); + } + + $dep = $this->registry->get($depType, $depName); + + $assets[$depType][$depName] = $dep; + + if (!$recursively) { + continue; + } + + $parentDeps = $this->getAllDependenciesForAsset($depType, $dep, true, $visited); + $assets = $parentDeps ? array_replace_recursive($assets, $parentDeps) : $assets; + } + } } return $assets; diff --git a/modules/mod_finder/tmpl/default.php b/modules/mod_finder/tmpl/default.php index bd81891d524f0..3bff66466821e 100644 --- a/modules/mod_finder/tmpl/default.php +++ b/modules/mod_finder/tmpl/default.php @@ -48,7 +48,7 @@ * This segment of code sets up the autocompleter. */ if ($params->get('show_autosuggest', 1)) { - $wa->usePreset('awesomplete'); + $wa->useScript('awesomplete'); $app->getDocument()->addScriptOptions('finder-search', ['url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false)]); Text::script('COM_FINDER_SEARCH_FORM_LIST_LABEL');