Written by

Roberto Segura

Category:

Blog

12 May 2014

Some time ago Michael Babker remembered that there is a right way of loading assets in Joomla to allow end users and designers to override them.

After that article I started a discussion with Don Gilbert about a way to simplify the system based on a class Don was using for his developments.

This is the class I ended using to handle assets on my extensions:

/**
 * Asset helper
 *
 * @package     MyExtension.Library
 * @subpackage  Helper
 * @since       1.0
 */
abstract class MyextHelperAsset extends JHtml
{
    /**
     * Includes assets from media directory, looking in the
     * template folder for a style override to include.
     *
     * @param   string  $filename   Path to file.
     * @param   string  $extension  Current extension name. Will auto detect component name if null.
     * @param   array   $attribs    Extra attribs array
     *
     * @return  mixed  False if asset type is unsupported, nothing if a css or js file, and a string if an image
     */
    public static function load($filename, $extension = null, $attribs = array())
    {
        if (is_null($extension))
        {
            $extensionParts = explode(DIRECTORY_SEPARATOR, JPATH_COMPONENT);
            $extension = array_pop($extensionParts);
        }

        $toLoad = "$extension/$filename";

        // Discover the asset type from the file name
        $type = substr($filename, (strrpos($filename, '.') + 1));

        switch (strtolower($type))
        {
            case 'css':
                return self::stylesheet($toLoad, $attribs, true, false);
                break;
            case 'js':
                return self::script($toLoad, false, true);
                break;
            case 'gif':
            case 'jpg':
            case 'jpeg':
            case 'png':
            case 'bmp':
                $alt = null;

                if (isset($attribs['alt']))
                {
                    $alt = $attribs['alt'];
                    unset($attribs['alt']);
                }

                return self::image($toLoad, $alt, $attribs, true);
                break;
            default:
                return false;
        }
    }
}

Basically Don's awesomeness with "alt" support for images. You can see the original gist here.

This covers 99% of the requirements when you are dealing with assets without adding extra never-needed code. It also allows a more logical approach to load assets.

Direct asset loading support

But there is a base problem on the way that Joomla! loads assets. It's hard to load assets which full URL you already know. Example something like:

JHtml::script('com_finder/indexer.js', false, true);

would search indexer.js file in:

/templates/my_template/js/com_finder/indexer.js
/media/com_finder/js/indexer.js

This is confusing because:

  • The path you have to enter as paremeter is not a real path. You have to remove the `js` part because otherwise Joomla! won't find it.
  • Assets that share same folder for JS & CSS (most of them actually) have to be divided on separated folders.

For example if you include bootstrap on your extension you will have to divide it into:

/media/my_extension/css/bootstrap/bootstrap.css
/media/my_extension/js/bootstrap/bootstrap.js

Such system is adding complexity where it's not required and avoiding you from using things like Bower or other dependency managers. Why not allow users to directly enter something like:

JHtml::script('my_extension/bootstrap/js/bootstrap.min.js', false, true);
JHtml::stylesheet('my_extension/bootstrap/css/bootstrap.min.css', false, true);

 That's what current web development expects. To allow that kind of calls I created a new function that simplifies all the asset loading that most developers need:

/**
 * Function to add support to direct loading try to simplify all the work to be done to load an asset
 *
 * @param   string   $fileRoute           Path to file.
 * @param   string   $extension           Current extension name. Will auto detect component name if null.
 * @param   array    $attribs             Extra attribs array
 * @param   boolean  $searchUncompressed  Search for uncompressed files (if debug is enabled)?
 *
 * @return  mixed  False if asset type is unsupported, nothing if a css or js file, and a string if an image
 */
public static function directLoad($fileRoute, $extension = null, $attribs = array(), $searchUncompressed = true)
{
    $fileName      = basename($fileRoute);
    $fileNameOnly  = pathinfo($fileName, PATHINFO_FILENAME);
    $fileExtension = pathinfo($fileRoute, PATHINFO_EXTENSION);

    // Detect debug mode
    if ($searchUncompressed && JFactory::getConfig()->get('debug'))
    {
        /*
         * Detect if we received a file in the format name.min.ext
         * If so, strip the .min part out, otherwise append -uncompressed
         */
        if (strrpos($fileNameOnly, '.min', '-4'))
        {
            $position = strrpos($fileNameOnly, '.min', '-4');
            $uncompressedFileName = str_replace('.min', '.', $fileNameOnly, $position);
            $uncompressedFileName  = $uncompressedFileName . $fileExtension;
        }
        else
        {
            $uncompressedFileName = $fileNameOnly . '-uncompressed.' . $fileExtension;
        }

        $uncompressedRoute = str_replace($fileName, $uncompressedFileName, $fileRoute);

        if ($uncompressedLoad = static::directLoad($uncompressedRoute, $extension, $attribs, false))
        {
            return $uncompressedLoad;
        }
    }

    $template = JFactory::getApplication()->getTemplate();

    $baseRoute = $extension ? JPATH_SITE . '/media/' . $extension : JPATH_SITE . '/media';
    $overrideBaseRoute = $extension ? JPATH_THEMES . '/' . $template . '/' . $extension : JPATH_THEMES . '/' . $template;

    $searchPaths = array(
        dirname($overrideBaseRoute . '/' . $fileRoute),
        dirname($overrideBaseRoute . '/' . strtolower($fileExtension) . '/' . $fileRoute),
        dirname($baseRoute . '/' . $fileRoute),
        dirname($baseRoute . '/' . strtolower($fileExtension) . '/' . $fileRoute),
    );

    if ($fileLocation = JPath::find($searchPaths, $fileName))
    {
        $fileUrl = str_replace(JPATH_SITE, JUri::root(true), $fileLocation);

        switch (strtolower($fileExtension))
        {
            case 'css':
                JFactory::getDocument()->addStylesheet($fileUrl, 'text/css', null, $attribs);
                break;
            case 'js':
                JFactory::getDocument()->addScript($fileUrl);
                break;
            case 'gif':
            case 'jpg':
            case 'jpeg':
            case 'png':
            case 'bmp':
                $alt = null;

                if (isset($attribs['alt']))
                {
                    $alt = $attribs['alt'];
                    unset($attribs['alt']);
                }

                $html = '<img src="' . $fileUrl . '" alt="' . $alt . '" '
                    . trim((is_array($attribs) ? JArrayHelper::toString($attribs) : $attribs) . ' /')
                    . '>';

                return $html;
                break;
            default:
                return false;
        }

        return true;
    }

    return false;
}

To ensure backwards compatibility I wanted to keep the original load function so I opted to plug it into load() :

/**
 * Includes assets from media directory, looking in the
 * template folder for a style override to include.
 *
 * @param   string  $filename   Path to file.
 * @param   string  $extension  Current extension name. Will auto detect component name if null.
 * @param   array   $attribs    Extra attribs array
 *
 * @return  mixed  False if asset type is unsupported, nothing if a css or js file, and a string if an image
 */
public static function load($filename, $extension = null, $attribs = array())
{
    if (is_null($extension))
    {
        $extensionParts = explode(DIRECTORY_SEPARATOR, JPATH_COMPONENT);
        $extension = array_pop($extensionParts);
    }

    // Try to use the directLoad function easier to debug & with direct load support
    if ($result = static::directLoad($filename, $extension, $attribs))
    {
        return $result;
    }

    $toLoad = "$extension/$filename";

    // Discover the asset type from the file name
    $type = substr($filename, (strrpos($filename, '.') + 1));

    switch (strtoupper($type))
    {
        case 'CSS':
            return self::stylesheet($toLoad, $attribs, true, false);
            break;
        case 'JS':
            return self::script($toLoad, false, true);
            break;
        case 'GIF':
        case 'JPG':
        case 'JPEG':
        case 'PNG':
        case 'BMP':
            $alt = null;

            if (isset($attribs['alt']))
            {
                $alt = $attribs['alt'];
                unset($attribs['alt']);
            }

            return self::image($toLoad, $alt, $attribs, true);
            break;
        default:
            return false;
    }
}

It will try to first load the file through the new direct load and relay in the old method if it fails. Now you can do:

MyextHelperAsset::load('my_extension/bootstrap/js/bootstrap.min.js');

You can also call it directly to avoid relaying in the old method if you are sure you don't need it:

MyextHelperAsset::directLoad('my_extension/bootstrap/js/bootstrap.min.js');

Note also that I created directLoad fully independent to avoid extending JHtml if you don't require the old asset loading. 

You can find the final class in this gist.

And that's all. Don't forget to make all your assets overridable!!