Как правильно подключать JavaScript и CSS в Joomla 4

  • Вторник, 20 июля 2021

В мире фронтенда многие ресурсы (ассеты) связаны между собой. В Joomla никогда не было простого способа указать эту связь, но Joomla 4 изменила эту ситуацию, введя концепцию Web Assets. Управление JavaScript и CSS в Joomla значительно упростилось, благодаря классу WebAssetManager.

Joomla 4 значительно улучшила свою кодовую базу, выкинув Mootools и по умолчанию отказавшись от jQuery. Вы можете подключать сторонние стили, скрипты и JavaScript библиотеки по своему усмотрению, и это одновременно как плюс, так и минус Joomla 4. Плюс в том, что вы получаете возможность выбора и гибкость в управлении этими ресурсами. Минус же заключается в том, что требуется знать, как правильно подключать стиль или скрипт в Joomla. Как раз о правильном подключении JavaScript и CSS мы расскажем ниже.

Определение скриптов JS и стилей CSS в Joomla

Связанные JavaScript и CSS ресурсы в Joomla определяются в файле JSON, например system/joomla.asset.json. Структура этого файла состоит из определения схемы (schema), имени (name), версии (version), лицензии (license) и затем одного или нескольких определений. Они состоят из списка JavaScript и CSS файлов, связанных с ресурсами, и любых зависимостей (dependencies). Раздел зависимостей - это просто список имён ассетов, который необходим для функционирования данного ассета.

Пример:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "com_example",
  "version": "4.0.0",
  "description": "Joomla CMS",
  "license": "GPL-2.0+",
  "assets": [
    {
      "name": "bar",
      "type": "style",
      "uri": "com_example/bar.css"
    },
    {
      "name": "bar",
      "type": "script",
      "uri": "com_example/bar.js"
    },
    {
      "name": "beer",
      "type": "style",
      "uri": "com_example/beer.css",
      "dependencies": [
        "bar"
      ],
    },
    {
      "name": "beer",
      "type": "script",
      "dependencies": [
        "core",
        "bar"
      ],
      "uri": "com_example/beer.js",
      "attributes": {
        "defer": true,
        "data-foo": "bar"
      }
    }
  ]
}

Атрибут $schema - это файл определения схемы, который позволяет вам проверить ваш файл ресурсов с помощью JSON Schema. Подробнее о работе валидации JSON Schema читайте на официальном сайте.

Наличие joomla.asset.json рекомендуется для вашего расширения или шаблона, но не обязательно для работы WebAsset.

Не рекомендуется добавлять инлайны (куски кода как CSS, так и JS) в json-файл, для этого лучше использовать отдельный файл.

Стадия ресурса (ассета)

У каждого ресурса есть две стадии: зарегистрированный (registered) и используемый (used).

Зарегистрированный - это стадия, при которой ресурс только загружен в реестр WebAssetRegistry. Это означает, что WebAssetManager знает о существовании этих ресурсов, но не будет подключать их к документу во время рендеринга. Все ресурсы, загруженные из joomla.asset.json, находятся в стадии "зарегистрированный".

Используемый - это стадия, при которой ресурс включен через ->useAsset() (->useScript(), ->useStyle(), ->registerAndUseX() и т.д.). Это означает, что WebAssetManager подключит эти ресурсы и их зависимости к документу во время рендеринга.

Ресурс не может быть использован, если ранее он не был зарегистрирован, это вызовет исключение неизвестного ресурса (UnknownAssetException).

Регистрация в Joomla JavaScript и CSS

Все известные JavaScript и CSS ассеты в Joomla загружаются и затем сохраняются в реестр ресурсов WebAssetRegistry (для включения/выключения элемента вы должны использовать WebAssetManager).

Joomla будет искать определение ассетов автоматически во время выполнения в следующем порядке:

media/vendor/joomla.asset.json (при первом обращении к WebAssetRegistry)
media/system/joomla.asset.json
media/legacy/joomla.asset.json
media/{com_active_component}/joomla.asset.json (при Общая информация о принципе действия Joomla (см. на JPath.ru) приложения)
templates/{active_template}/joomla.asset.json

И загрузит их в реестр известных JavaScript и CSS файлов.

Каждое следующее определение будет переопределять элементы ресурсов из предыдущего определения, по имени (name) элемента.

В Joomla вы можете зарегистрировать свои собственные определения ресурсов (ассетов) через WebAssetRegistry:

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wr = $wa->getRegistry();
$wr->addRegistryFile('relative/path/to/your/joomla.asset.json');

Чтобы добавить пользовательский элемент во время выполнения:

$wr->add('script', new Joomla\CMS\WebAsset\WebAssetItem('foobar', 'com_foobar/file.js', ['type' => 'script']));

Или более просто, используя WebAssetManager:

$wa->registerScript('foobar', 'com_foobar/file.js');

Новый элемент foobar будет добавлен в реестр известных ресурсов, но не будет подключен к документу, пока ваш код (макет, шаблон и т.д.) не запросит его.

Методы registerScript (регистрация JavaScript) и registerStyle (регистрация CSS) являются прокси-методами для метода registerAsset класса WebAssetManager, который принимает следующие аргументы:

Параметр Тип По умолчанию Описание
$type string   Тип ресурса: script или style. Определяется автоматически при использовании registerScript и registerStyle.
$asset WebAssetItem|string   Имя ресурса или объёкт класса WebAssetItem.
$uri string '' URI адрес ресурса. Поддерживаются также относительные пути типа 'com_foobar/file.js'.
$options array [] Параметры ресурса , например ['version' => 'auto', 'conditional' => 'lt IE 9'].
$attributes array [] Атрибуты ресурса, например ['defer' => true, 'data-foo' => 'some attribute'].
$dependencies array [] Имена ассетов от которых зависит регистрируемый ассет, например ['core', 'keepalive'].

Чтобы проверить существование ассета в реестре, используйте метод assetExists:

if ($wa->assetExists('script', 'foobar'))
{
    var_dump('Скрипт "foobar" существует!');
}

Включение ресурса (ассета с JS и/или CSS)

Все управление JavaScript и CSS ресурсами в текущем документе осуществляется с помощью WebAssetManager, который доступен с помощью $doc->getWebAssetManager(). Используя WebAssetManager, вы можете легко включить или отключить нужный ресурс в Joomla с помощью стандартных методов.

Чтобы включить ассет на странице, используйте методы useScript (для JavaScript) или useStyle (для CSS), которые являются прокси-методами для useAsset, например:

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */.
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->useScript('keepalive');

// Или несколько
$wa->useScript('keepalive')
    ->useScript('fields.validate')
    ->useStyle('foobar')
    ->useScript('foobar');

// Добавляем новый JS-файл с зависимостью и используем его
$wa->registerAndUseScript('bar', 'com_foobar/bar.js', [], [], ['core', 'foobar']);

WebAssetManager посмотрит в WebAssetRegistry, существует ли запрашиваемый ресурс, и включит его для текущего экземпляра документа. В противном случае будет выброшено исключение UnknownAssetException.

Для отключения ненужных ассетов (JS, CSS файлов) - используйте метод disableAsset. Приведенный ниже пример отключит загрузку ресурса jquery-noconflict.

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->disableScript('jquery-noconflict');

Если есть какие-либо зависимости от отключаемого ресурса, то независимо ни от чего этот ресурс будет снова автоматически включен.

Чтобы проверить, включен ли ассет, и узнать его состояние:

// Проверяем, активен ли ассет (включен вручную или автоматически как зависимость)
if ($wa->isAssetActive('script', 'foobar'))
{
    var_dump('Скрипт "foobar" активен!');
}

// Проверка состояния
switch($wa->getAssetState('script', 'foobar')){
	case Joomla\CMS\WebAsset\WebAssetManager::ASSET_STATE_ACTIVE:
		var_dump('Активен! Был включен вручную');
		break;
	case Joomla\CMS\WebAsset\WebAssetManager::ASSET_STATE_DEPENDENCY:
		var_dump('Активен! Был включен автоматически при разрешении зависимостей');
		break;
	default:
		var_dump('Не активен!');
}

Переопределение подключаемых стилей CSS и JS скриптов в Joomla

Переопределение может быть полезно, когда вам нужно переопределить URI элемента ресурса или его зависимостей. Как было отмечено выше, каждое следующее определение будет переопределять элементы из предыдущего определения, по имени элемента. Это означает, что если вы предоставите свой joomla.asset.json, содержащий уже загруженные элементы ресурсов с указанными файлами JavaScript и CSS, то они будут заменены на ваши элементы.

Другой способ переопределения в коде - зарегистрировать элемент с тем же именем. Например, у нас есть скрипт foobar, который загружает библиотеку com_example/foobar.js, и мы хотим использовать CDN именно для этой библиотеки. Изначально ресурс определён в системе следующим образом:

...
{
  "name": "foobar",
  "type": "script",
  "uri": "com_example/foobar.js",
  "dependencies": ["core"]
}
...

Чтобы переопределить URI, мы определяем элемент ресурса с именем foobar в нашем joomla.asset.json:

...
{
  "name": "foobar",
  "type": "script",
  "uri": "https://foobar.cdn.blabla/foobar.js",
  "dependencies": ["core"]
}
...

Или зарегистрируйте новый элемент ресурса через WebAssetManager:

$wa->registerScript('foobar', 'https://fobar.cdn.blabla/foobar.js', [], [], ['core']);

Работа с CSS в Joomla 4

В Joomla WebAssetManager позволяет управлять файлами стилей (CSS). Такой элемент обозначается как "style". Пример json определения элемента CSS в joomla.asset.json:

...
{
  "name": "foobar",
  "type": "style",
  "uri": "com_example/foobar.css"
}
...

Методы для работы со стилями CSS

В Joomla WebAssetManager предлагает следующие методы работы с файлами стилей CSS:

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

// Подключаем foobar к документу
$wa->useStyle('foobar');

// Отключаем foobar от документа
$wa->disableStyle('foobar');

// Регистрируем пользовательский элемент без определения json
$wa->registerStyle('bar', 'com_example/bar.css', [], ['data-foo' => 'some attribute'], ['some.dependency']);

// И используем его позже
$wa->useStyle('bar');

// Одновременно регистрируем и подключаем пользовательский элемент
$wa->registerAndUseStyle('bar', 'com_example/bar.css', [], ['data-foo' => 'some attribute'], ['some.dependency']);

Добавление инлайн-стиля

Дополнительно к файлам стилей CSS, WebAssetManager позволяет добавлять инлайн-стили (inline style), и поддерживать их отношение к файлу ресурса. Инлайн-стили могут быть размещены непосредственно перед зависимостью, после зависимости, или как обычно после всех стилей CSS.

Инлайн-ресурс может иметь имя, как и другие ресурсы (но не обязательно). Имя может быть использовано для получения элемента ресурса из реестра или как зависимость от другого встроенного ресурса. Если имя не указано, то будет использовано сгенерированное имя, основанное на хэше содержимого.

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

// Добавляем инлайн-стиль CSS как обычно, он будет рендерится в потоке после всех файлов CSS.
$wa->addInlineStyle('content of inline1');

// Добавляем инлайн-стиль CSS, который будет размещён после "foobar".
$wa->addInlineStyle('content of inline2', ['position' => 'after'], ['data-foo' => 'bar'], ['foobar']);

// Добавляем инлайн-стиль CSS, который будет размещён до "foobar".
$wa->addInlineStyle('content of inline3', ['position' => 'before'], [], ['foobar']);

// Инлайн-стиль CSS с именем
$wa->addInlineStyle('content of inline4', ['name' => 'my.inline.asset']);

В примере выше "foobar" должен существовать в реестре ресурсов, иначе вы получите исключение неполной зависимости.

Пример выше выведет:

...
<style>content of inline3</style>
<link rel="stylesheet" href="/foobar.css" />
<style data-foo="bar">content of inline2</style>
...
...
<style>content of inline1</style>
<style>content of inline4</style>
...

Если инлайн-ресурс CSS имеет несколько зависимостей, то для позиционирования будет использоваться последняя. Пример:

$wa->addInlineStyle('content of inline1', ['position' => 'before'], [], ['foo', 'bar']);
$wa->addInlineStyle('content of inline2', ['position' => 'after'], [], ['foo', 'bar']);

Выведет:

...
<link rel="stylesheet" href="/foo.css" />
<style>content of inline1</style>
<link rel="stylesheet" href="/bar.css" />
<style>content of inline2</style>
...

Именованные инлайн-ресурсы CSS могут быть зависимы от другого инлайна, однако не рекомендуется использовать такие конструкции в качестве зависимости от неинлайн-ресурса. Это будет работать, но такое поведение может измениться в будущем. Вместо этого лучше использовать "позицию".

Работа с JavaScript в Joomla 4

В Joomla WebAssetManager позволяет управлять файлами скриптов (JavaScript). Такой элемент обозначается как "script". Пример json определения элемента в joomla.asset.json:

...
{
  "name": "foobar",
  "type": "script",
  "uri": "com_example/foobar.js",
  "dependencies": ["core"]
}
...

Пример json-определения скрипта модуля ES6 с откатом к legacy:

...
{
  "name": "foobar-legacy",
  "type": "script",
  "uri": "com_example/foobar-as5.js",
  "attributes": {
    "nomodule": true,
    "defer": true
  },
  "dependencies": ["core"]
}
{
  "name": "foobar",
  "type": "script",
  "uri": "com_example/foobar.js",
  "attributes": {
    "type": "module"
  },
  "dependencies": [
    "core", 
    "foobar-legacy"
  ]
}
...

Методы для работы со скриптами JS

В Joomla WebAssetManager предлагает следующие методы работы с файлами JS скриптов:

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

// Подключаем foobar к документу
$wa->useScript('foobar');

// Отключаем foobar от документа
$wa->disableScript('foobar');

// Регистрируем пользовательский элемент JS без определения json
$wa->registerScript('bar', 'com_example/bar.js', [], ['defer' => true], ['core']);

// И используем его позже
$wa->useScript('bar');

// Одновременно регистрируем и подключаем пользовательский элемент JS
$wa->registerAndUseScript('bar','com_example/bar.js', [], ['defer' => true], ['core']);

Добавление инлайн-скрипта JS

Дополнительно к файлам скриптов JS, WebAssetManager позволяет добавлять инлайн-скрипты (inline script), и поддерживать их отношение к файлу ресурса. Инлайн-скрипты JS могут быть размещены непосредственно перед зависимостью, после зависимости, или как обычно после всех JS скриптов.

Инлайн-ресурс JS может иметь имя, как и другие ресурсы (но не обязательно). Имя может быть использовано для получения элемента ресурса из реестра или как зависимость от другого встроенного ресурса. Если имя не указано, то будет использовано сгенерированное имя, основанное на хэше содержимого.

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

// Добавляем инлайн-скрипт JS как обычно, он будет рендерится в потоке после всех JS файлов.
$wa->addInlineScript('content of inline1');

// Добавляем инлайн-скрипт JS, который будет размещён после "foobar".
$wa->addInlineScript('content of inline2', ['position' => 'after'], ['data-foo' => 'bar'], ['foobar']);

// Добавляем инлайн-скрипт JS, который будет размещён до "foobar".
$wa->addInlineScript('content of inline3', ['position' => 'before'], [], ['foobar']);

// Инлайн-скрипт JS с именем
$wa->addInlineScript('content of inline4', ['name' => 'my.inline.asset']);

В примере выше "foobar" должен существовать в реестре ресурсов, иначе вы получите исключение неполной зависимости.

Пример выше выведет:

...
<script>content of inline3</script>
<link rel="javascript" href="/foobar.js" />
<script data-foo="bar">content of inline2</script>
...
...
<script>content of inline1</script>
<script>content of inline4</script>
...

Если инлайн-скрипт JS имеет несколько зависимостей, то для позиционирования будет использоваться последняя. Пример:

 $wa->addInlineScript('content of inline2', ['position' => 'after'], [], ['foo', 'bar']);

Выведет:

...
<link rel="javascript" href="/foo.js" />
<script>content of inline1</script>
<link rel="javascript" href="/bar.js" />
<script>content of inline2</script>
...

Именованные JS инлайны могут быть зависимы от другого JS инлайнов, однако не рекомендуется использовать инлайн в качестве зависимости от неинлайн-ресурса. Это будет работать, но такое поведение может измениться в будущем. Вместо этого лучше использовать "позицию".

Работа с веб-компонентами

Joomla позволяет вам использовать веб-компоненты. В Joomla веб-компоненты загружаются не как обычный скрипт, а асинхронно через загрузчик веб-компонентов. Поэтому элемент ресурса (ассета) веб-компонента должен иметь флаг webcomponent, установленный в булево значение true. Во всех остальных аспектах работа с веб-компонентами в WebAssetManager аналогична работе с элементом типа "script".

Пример json определения некоторых веб-компонентов в joomla.asset.json (как модуль ES6):

...
{
  "name": "webcomponent.foobar",
  "type": "style",
  "uri": "com_example/foobar-custom-element.css",
},
{
  "name": "webcomponent.foobar",
  "type": "script",
  "uri": "com_example/foobar-custom-element.js",
  "attributes": {
     "type": "module"
  },
}
...

Пример с откатом, для браузеров, не поддерживающих функцию ES6 "module". Обратите внимание, что унаследованный скрипт должен иметь зависимость wcpolyfill, а скрипт модуля должен иметь зависимость от унаследованного скрипта:

...
{
  "name": "webcomponent.foobar",
  "type": "style",
  "uri": "com_example/foobar-custom-element.css",
},
{
  "name": "webcomponent.foobar-legacy",
  "type": "script",
  "uri": "com_example/foobar-custom-element-es5.js",
  "attributes": {
    "nomodule": true,
    "defer": true
  },
  "dependencies": [
    "wcpolyfill"
  ]
},
{
  "name": "webcomponent.foobar",
  "type": "script",
  "uri": "com_example/foobar-custom-element.js",
  "attributes": {
    "type": "module"
  },
  "dependencies": [
    "webcomponent.foobar-legacy"
  ]
}
...

В качестве альтернативы вы можете зарегистрировать их в PHP (как модуль ES6):

$wa->registerStyle('webcomponent.foobar', 'com_example/foobar-custom-element.css')
    ->registerScript('webcomponent.foobar', 'com_example/foobar-custom-element.js', ['type' => 'module']);

Подключение к документу:

$wa->useStyle('webcomponent.foobar')
    ->useScript('webcomponent.foobar');

Предпочтительно для имени использовать префикс "webcomponent", чтобы его можно было легко обнаружить и отличить от обычных скриптов в макете.

Работа с пресетами

"Пресет" (preset) - это особый вид элемента asset в Joomla, который содержит список элементов, которые должны быть включены, так же, как и прямой вызов useAsset() для каждого элемента в списке. Пресет может содержать смешанные типы ассетов (скрипт, стиль, другой пресет и т.д.). Тип должен указываться после символа # и следовать после имени ресурса, например: foo#style, bar#script.

Пример json определения элемента в joomla.asset.json:

...
{
  "name": "foobar",
  "type": "preset",
  "uri": "",
  "dependencies": [
    "core#script",
    "foobar#style",
    "foobar#script",
  ]
}
...

Методы для работы с пресетами

В Joomla WebAssetManager предлагает следующие методы работы с пресетами:

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();

// Подключаем все элементы пресета foobar к документу
$wa->usePreset('foobar');

// Отключаем все элементы foobar
$wa->disablePreset('foobar');

// Регистрируем пользовательский пресет без определения json
$wa->registerPreset('bar', '', [], [], ['core#script', 'bar#script']);

// И используем его позже
$wa->usePreset('bar');

// Одновременно регистрируем и подключаем пользовательский пресет
$wa->registerAndUsePreset('bar','', [], [], ['core#script', 'bar#script']);

Пользовательский класс WebAssetItem 

В Joomla классом по умолчанию для всех элементов WebAsset является Joomla\CMS\WebAsset\WebAssetItem.

Вы также можете использовать пользовательский класс, который должен реализовывать интерфейс Joomla\CMS\WebAsset\WebAssetItemInterface или расширять Joomla\CMS\WebAsset\WebAssetItem.

Пользовательский класс может позволить вам выполнять дополнительные действия, например, включать файл ассета в зависимости от активного языка:

class MyComExampleAssetItem extends WebAssetItem
{
	public function getUri($resolvePath = true): string
	{
		$langTag = Factory::getApplication()->getLanguage()->getTag();
		
		// Для скрипта используем ".js", а для стиля используем ".css"
		$path = 'com_example/bar-' . $langTag . '.js';

		if ($resolvePath)
		{
			// Для скрипта используем "script", а для стиля используем "stylesheet"
			$path = $this->resolvePath($path, 'script');
		}

		return $path;
	}
}

Кроме того, реализация Joomla\CMS\WebAsset\WebAssetAttachBehaviorInterface позволяет вам добавить опции сценария (которые могут зависеть от окружения), когда ваш ресурс включен и подключен к документу:

class MyFancyFoobarAssetItem extends WebAssetItem implements WebAssetAttachBehaviorInterface
{
	public function onAttachCallback(Document $doc): void
	{
		$user = Factory::getApplication()->getIdentity();
		$doc->addScriptOptions('com_example.fancyfoobar', ['userName' => $user->username]);
	}
}

Элемент ресурса, реализующий WebAssetAttachBehaviorInterface, должен быть включен до события onBeforeCompileHead, иначе 'onAttachCallback' будет проигнорировано.

Определение пользовательского класса WebAssetItem в joomla.asset.json

В joomla.asset.json вы можете определить, какой класс должен использоваться с конкретным WebAsset. Для этого вы можете использовать два свойства namespace и class. namespace может быть определено на уровне Root (тогда оно будет использоваться как пространство имен по умолчанию для всех элементов Asset в joomla.asset.json) или на уровне элемента. Например:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "com_example",
  "version": "4.0.0",
  "namespace": "Joomla\Component\Example\WebAsset",
  "assets": [
    {
      "name": "foo",
      "type": "script",
      "class": "FooAssetItem",
      "uri": "com_example/foo.js"
    },
    {
      "name": "bar",
      "type": "script",
      "namespace": "MyFooBar\Library\Example\WebAsset",
      "class": "BarAssetItem",
      "uri": "com_example/bar.js"
    }
  ]
}

Здесь ассет foo будет связан с классом Joomla\Component\Example\WebAsset\FooAssetItem, а bar с классом MyFooBar\Library\Example\WebAsset\BarAssetItem.

Если пространство имен не определено, то по умолчанию будет использоваться Joomla\CMS\WebAsset. Если пространство имен определено, но пустое, то не будет использоваться никакое пространство имен, только класс. Пример:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "com_example",
  "assets": [
    {
      "name": "foo",
      "type": "script",
      "class": "FooAssetItem",
      "uri": "com_example/foo.js"
    },
    {
      "name": "bar",
      "type": "script",
      "namespace": "",
      "class": "BarAssetItem",
      "uri": "com_example/bar.js"
    }
  ]
}

Здесь foo будет связан с классом Joomla\CMS\WebAsset\FooAssetItem, а bar с классом BarAssetItem (без пространства имен).

Источник: Как правильно подключать JavaScript и CSS в Joomla 4. JPath.ru

Дмитрий Рекун

Дмитрий Рекун

Пишу везде и понемногу ;)

Латвия, Рига. https://jpath.ru/

Статьи автора

Joomla!® CMS — пожалуй, лучшая система управления контентом с открытым исходным кодом

Логотип Joomla

Joomla! — это больше, чем просто программное обеспечение, это люди, включающие разработчиков, дизайнеров, системных администраторов, переводчиков, копирайтеров, и, что самое главное — простых пользователей.

Мы рады пригласить вас в ряды нашего сообщества!

Свернуть

Коротко о главном в Joomla

Новости портала

Новое в блогах

Видео