diff --git a/.drone.yml b/.drone.yml
index 9f197ff68a3..f37d0bfe6e4 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -269,6 +269,6 @@ steps:
---
kind: signature
-hmac: 71729a12ae3556e03f924ac95c8a5cd14bb6a3c2d20ef6768fdd1e853397fd22
+hmac: 53e23beca546bde246f812a726320210f33d231106cb154a65cc900ecbf7502e
...
diff --git a/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-06-27.sql b/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-06-27.sql
new file mode 100644
index 00000000000..20e5a05614c
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-06-27.sql
@@ -0,0 +1,2 @@
+INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
+(0, 'plg_system_addmodulebutton', 'plugin', 'addmodulebutton', 'system', 0, 1, 1, 0, 1, '', '', '', NULL, NULL, 0, 0);
diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-06-27.sql b/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-06-27.sql
new file mode 100644
index 00000000000..20e5a05614c
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-06-27.sql
@@ -0,0 +1,2 @@
+INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
+(0, 'plg_system_addmodulebutton', 'plugin', 'addmodulebutton', 'system', 0, 1, 1, 0, 1, '', '', '', NULL, NULL, 0, 0);
diff --git a/administrator/components/com_modules/forms/preview_positions.xml b/administrator/components/com_modules/forms/preview_positions.xml
new file mode 100644
index 00000000000..ac8df237613
--- /dev/null
+++ b/administrator/components/com_modules/forms/preview_positions.xml
@@ -0,0 +1,24 @@
+
+
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 2af5c7d562a..11aba886386 100644
--- a/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php
+++ b/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php
@@ -76,13 +76,38 @@
->usePreset('choicesjs')
->useScript('webcomponent.field-fancy-select');
-?>
-> $id,
- 'list.select' => $value,
- 'list.attr' => implode(' ', $selectAttr),
- )
- );
-?>
+$app = Factory::getApplication();
+$clientId = $app->input->getBool('client_id', 0);
+
+$modalUrl = 'index.php?option=com_modules&view=module&layout=preview_positions&id=1&client_id=' . $clientId;
+
+// &tmpl=component doesn't redirect if the user isn't logged into backend hence we are adding it conditionally
+$modalUrl = $app->isClient('site') ? 'administrator/' . $modalUrl : $modalUrl . '&tmpl=component';
+?>
+>
+ $id,
+ 'list.select' => $value,
+ 'list.attr' => implode(' ', $selectAttr),
+ )
+);
+?>
+
+ Text::_('COM_MODULES_MODULE_SELECT_POSITION'),
+ 'url' => $modalUrl,
+ 'bodyHeight' => 70,
+ 'modalWidth' => 95,
+ 'footer' => '',
+ )
+);
+?>
+
diff --git a/administrator/components/com_modules/src/Controller/ModuleController.php b/administrator/components/com_modules/src/Controller/ModuleController.php
index 3a1d093ab77..eff78df5545 100644
--- a/administrator/components/com_modules/src/Controller/ModuleController.php
+++ b/administrator/components/com_modules/src/Controller/ModuleController.php
@@ -68,6 +68,38 @@ public function add()
$app->setUserState('com_modules.add.module.params', $params);
}
+ /**
+ * Set Position and Menu ID in State for the New Module in Frontend Module Placement.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function selectPosition()
+ {
+ $app = $this->app;
+
+ // Get the Menu ID and Position.
+ $menuId = $this->input->getInt('menu', 0);
+ $position = $this->input->get('position');
+
+ if (empty($position))
+ {
+ $redirectUrl = $this->input->server->getString('HTTP_REFERER');
+
+ $this->setRedirect(Route::_($redirectUrl, false));
+
+ $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_POSITION'), 'warning');
+ }
+
+ $app->setUserState('com_modules.add.module.menu_id', $menuId);
+ $app->setUserState('com_modules.add.module.position', $position);
+
+ // Select Position is only used in the Frontend Module Placement so we pass client_id as 0
+ $redirectUrl = 'index.php?option=com_modules&view=select&client_id=0';
+ $this->setRedirect(Route::_($redirectUrl, false));
+ }
+
/**
* Override parent cancel method to reset the add module state.
*
@@ -82,7 +114,9 @@ public function cancel($key = null)
$result = parent::cancel();
$this->app->setUserState('com_modules.add.module.extension_id', null);
+ $this->app->setUserState('com_modules.add.module.menu_id', null);
$this->app->setUserState('com_modules.add.module.params', null);
+ $this->app->setUserState('com_modules.add.module.position', null);
if ($return = $this->input->get('return', '', 'BASE64'))
{
@@ -205,7 +239,9 @@ protected function postSaveHook(BaseDatabaseModel $model, $validData = array())
break;
}
+ $this->app->setUserState('com_modules.add.module.menu_id', null);
$this->app->setUserState('com_modules.add.module.params', null);
+ $this->app->setUserState('com_modules.add.module.position', null);
}
/**
diff --git a/administrator/components/com_modules/src/Model/ModuleModel.php b/administrator/components/com_modules/src/Model/ModuleModel.php
index f0634bbd374..603bf63a815 100644
--- a/administrator/components/com_modules/src/Model/ModuleModel.php
+++ b/administrator/components/com_modules/src/Model/ModuleModel.php
@@ -548,6 +548,8 @@ public function &getClient()
*/
public function getForm($data = array(), $loadData = true)
{
+ $app = Factory::getApplication();
+
// The folder and element vars are passed when saving the form.
if (empty($data))
{
@@ -572,19 +574,26 @@ public function getForm($data = array(), $loadData = true)
$this->setState('item.module', $module);
// Get the form.
- if ($clientId == 1)
+ if ($app->input->getCMD('layout') == 'preview_positions')
{
- $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true);
-
- // Display language field to filter admin custom menus per language
- if (!ModuleHelper::isAdminMultilang())
- {
- $form->setFieldAttribute('language', 'type', 'hidden');
- }
+ $form = $this->loadForm('com_modules.preview_positions', 'preview_positions', array('control' => 'jform', 'load_data' => $loadData), true);
}
else
{
- $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true);
+ if ($clientId == 1)
+ {
+ $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true);
+
+ // Display language field to filter admin custom menus per language
+ if (!ModuleHelper::isAdminMultilang())
+ {
+ $form->setFieldAttribute('language', 'type', 'hidden');
+ }
+ }
+ else
+ {
+ $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true);
+ }
}
if (empty($form))
@@ -642,7 +651,17 @@ protected function loadFormData()
$clientId = $app->input->getInt('client_id', 0);
$filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter');
$data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null)));
- $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null)));
+
+ // Pre-select Module Position set by Frontend Placement if it exists.
+ if ($position = $app->getUserState('com_modules.add.module.position'))
+ {
+ $data->set('position', $position);
+ }
+ else
+ {
+ $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null)));
+ }
+
$data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)));
$data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))));
}
@@ -676,6 +695,7 @@ protected function loadFormData()
*/
public function getItem($pk = null)
{
+ $app = Factory::getApplication();
$pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id');
$db = $this->getDbo();
@@ -757,8 +777,17 @@ public function getItem($pk = null)
if (empty($pk))
{
- // If this is a new module, assign to all pages.
- $assignment = 0;
+ if ($menuId = (int) $app->getUserState('com_modules.add.module.menu_id'))
+ {
+ // If a Menu ID is selected via Frontend Placements then use that.
+ $assignment = 1;
+ $assigned[] = $menuId;
+ }
+ else
+ {
+ // If this is a new module, assign to all pages.
+ $assignment = 0;
+ }
}
elseif (empty($assigned))
{
diff --git a/administrator/components/com_modules/tmpl/module/preview_positions.php b/administrator/components/com_modules/tmpl/module/preview_positions.php
new file mode 100644
index 00000000000..42e92c48625
--- /dev/null
+++ b/administrator/components/com_modules/tmpl/module/preview_positions.php
@@ -0,0 +1,42 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Uri\Uri;
+
+/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
+$wa = $this->document->getWebAssetManager();
+$wa->useScript('com_modules.admin-modules-preview_positions');
+
+$isAdmin = Factory::getApplication()->input->get('client_id');
+
+// Get the URL of the iframe that displays template preview.
+$iframeBaseURL = Uri::root();
+$iframeBaseURL .= $isAdmin ? 'administrator' : '';
+
+// Conditionally render the admin or site template select field.
+$templateField = 'template_style_';
+$templateField .= $isAdmin ? 'admin' : 'site';
+
+// Render the template select field.
+$this->fieldsets = $this->form->getFieldsets();
+echo $this->form->renderField($templateField);
+?>
+
+
+
diff --git a/administrator/components/com_modules/tmpl/modules/default.php b/administrator/components/com_modules/tmpl/modules/default.php
index bd95ae2d29c..00cd011b19c 100644
--- a/administrator/components/com_modules/tmpl/modules/default.php
+++ b/administrator/components/com_modules/tmpl/modules/default.php
@@ -132,7 +132,7 @@
editor, $item->checked_out_time, 'modules.', $canCheckin); ?>
-
+
escape($item->title); ?>
escape($item->title); ?>
diff --git a/administrator/language/en-GB/com_modules.ini b/administrator/language/en-GB/com_modules.ini
index cfb0cc3c87b..7aa298dc6a0 100644
--- a/administrator/language/en-GB/com_modules.ini
+++ b/administrator/language/en-GB/com_modules.ini
@@ -32,6 +32,7 @@ COM_MODULES_EMPTYSTATE_TITLE_SITE="No Site Modules have been created yet."
COM_MODULES_ERR_XML="Module XML data not available"
COM_MODULES_ERROR_CANNOT_FIND_MODULE="Can't find module"
COM_MODULES_ERROR_INVALID_EXTENSION="Invalid module"
+COM_MODULES_ERROR_INVALID_POSITION="Invalid position"
COM_MODULES_ERROR_NO_MODULES_SELECTED="No module selected."
COM_MODULES_EXPAND="Expand"
COM_MODULES_EXTENSION_PUBLISHED_DISABLED="Module disabled and published."
@@ -90,7 +91,10 @@ COM_MODULES_MENU_ITEM_URL="URL"
COM_MODULES_MODULE="Module"
COM_MODULES_MODULE_ASSIGN="Module Assignment"
COM_MODULES_MODULE_DESCRIPTION="Module Description"
+COM_MODULES_MODULE_SELECT_POSITION="Select Module Position from Template Preview"
+COM_MODULES_MODULE_SELECT_POSITION_BUTTON="Select from Template Preview"
COM_MODULES_MODULE_TEMPLATE_POSITION="%1$s (%2$s)"
+COM_MODULES_MODULE_TEMPLATE_SELECT="Select Template to Preview"
COM_MODULES_MODULES="Modules"
COM_MODULES_MODULES_FILTER_SEARCH_DESC="Search in module title and note. Prefix with ID: to search for a module ID."
COM_MODULES_MODULES_FILTER_SEARCH_LABEL="Search Modules"
diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini
index f52986c131e..2fe2bc88b0a 100644
--- a/administrator/language/en-GB/joomla.ini
+++ b/administrator/language/en-GB/joomla.ini
@@ -556,6 +556,8 @@ JGLOBAL_PASSWORD="Password"
JGLOBAL_PASSWORD_RESET_REQUIRED="You are required to reset your password before proceeding."
JGLOBAL_PERMISSIONS_ANCHOR="Set Permissions"
JGLOBAL_PREVIEW="Preview"
+JGLOBAL_PREVIEW_PLACE_MODULE="Place Module Here"
+JGLOBAL_PREVIEW_PLACE_MODULE_POSITION="in Position: %s"
JGLOBAL_PREVIEW_POSITION="Position: %s"
JGLOBAL_PREVIEW_STYLE="Style: %s"
JGLOBAL_PUBLISHED_DATE="Published Date"
diff --git a/administrator/language/en-GB/plg_system_addmodulebutton.ini b/administrator/language/en-GB/plg_system_addmodulebutton.ini
new file mode 100644
index 00000000000..adabe9720ca
--- /dev/null
+++ b/administrator/language/en-GB/plg_system_addmodulebutton.ini
@@ -0,0 +1,10 @@
+; Joomla! Project
+; (C) 2021 Open Source Matters, Inc.
+; License GNU General Public License version 2 or later; see LICENSE.txt
+; Note : All ini files need to be saved as UTF-8
+
+PLG_SYSTEM_ADDMODULEBUTTON="System - Add Module Button"
+PLG_SYSTEM_ADD_MODULE_BUTTON_LABEL="Add Module to this Menu"
+PLG_ADD_MODULE_BUTTON_XML_DESCRIPTION="Displays a button to insert a module in a menu directly from the Frontend."
+PLG_SYSTEM_ADD_MODULE_BUTTON_CREATE_MODULE_PERMISSIONS_WARNING="Please login with an account having permission to Select Module Position"
+PLG_SYSTEM_ADD_MODULE_BUTTON_EDIT_MODULE_PERMISSIONS_WARNING="Please login with an account having permission to Edit Module Position"
diff --git a/administrator/language/en-GB/plg_system_addmodulebutton.sys.ini b/administrator/language/en-GB/plg_system_addmodulebutton.sys.ini
new file mode 100644
index 00000000000..127eff4b24d
--- /dev/null
+++ b/administrator/language/en-GB/plg_system_addmodulebutton.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; (C) 2021 Open Source Matters, Inc.
+; License GNU General Public License version 2 or later; see LICENSE.txt
+; Note : All ini files need to be saved as UTF-8
+
+PLG_SYSTEM_ADDMODULEBUTTON="System - Add Module Button"
+PLG_ADD_MODULE_BUTTON_XML_DESCRIPTION="Displays a button to insert a module in a menu directly from the Frontend."
diff --git a/api/language/en-GB/joomla.ini b/api/language/en-GB/joomla.ini
index d77a4d87690..919884ad1c6 100644
--- a/api/language/en-GB/joomla.ini
+++ b/api/language/en-GB/joomla.ini
@@ -552,6 +552,8 @@ JGLOBAL_PASSWORD="Password"
JGLOBAL_PASSWORD_RESET_REQUIRED="You are required to reset your password before proceeding."
JGLOBAL_PERMISSIONS_ANCHOR="Set Permissions"
JGLOBAL_PREVIEW="Preview"
+JGLOBAL_PREVIEW_PLACE_MODULE="Place Module Here"
+JGLOBAL_PREVIEW_PLACE_MODULE_POSITION="in Position: %s"
JGLOBAL_PREVIEW_POSITION="Position: %s"
JGLOBAL_PREVIEW_STYLE="Style: %s"
JGLOBAL_PUBLISHED_DATE="Published Date"
diff --git a/build/media_source/com_modules/joomla.asset.json b/build/media_source/com_modules/joomla.asset.json
index 8797814a122..fceb5d6cc0c 100644
--- a/build/media_source/com_modules/joomla.asset.json
+++ b/build/media_source/com_modules/joomla.asset.json
@@ -97,6 +97,29 @@
"type": "module"
}
},
+ {
+ "name": "com_modules.admin-modules-preview_positions.es5",
+ "type": "script",
+ "uri": "com_modules/admin-modules-preview_positions-es5.min.js",
+ "dependencies": [
+ "core"
+ ],
+ "attributes": {
+ "nomodule": true,
+ "defer": true
+ }
+ },
+ {
+ "name": "com_modules.admin-modules-preview_positions",
+ "type": "script",
+ "uri": "com_modules/admin-modules-preview_positions.min.js",
+ "dependencies": [
+ "com_modules.admin-modules-preview_positions.es5"
+ ],
+ "attributes": {
+ "type": "module"
+ }
+ },
{
"name": "com_modules.admin-select-modal.es5",
"type": "script",
diff --git a/build/media_source/com_modules/js/admin-modules-preview_positions.es6.js b/build/media_source/com_modules/js/admin-modules-preview_positions.es6.js
new file mode 100644
index 00000000000..a5bb4debdbe
--- /dev/null
+++ b/build/media_source/com_modules/js/admin-modules-preview_positions.es6.js
@@ -0,0 +1,21 @@
+/**
+ * @copyright (C) 2021 Open Source Matters, Inc.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+/**
+ * Add a keyboard event listener to the Select Template Style element.
+ *
+ * This script is meant to be loaded deferred. This means that it's non-blocking
+ * (the browser can load it whenever) and it doesn't need an on DOMContentLoaded event handler
+ * because the browser is guaranteed to execute it only after the DOM content has loaded, the
+ * whole point of it being deferred.
+ *
+ */
+
+const elIframe = document.getElementById('module-position-select');
+const elTemplateSelect = document.getElementById('jform_template_style_select');
+
+elTemplateSelect.addEventListener('change', (event) => {
+ elIframe.src = elIframe.src.substring(0, elIframe.src.indexOf('templateStyle=') + 14) + event.target.value;
+});
diff --git a/build/media_source/layouts/js/chromes/outline.es6.js b/build/media_source/layouts/js/chromes/outline.es6.js
new file mode 100644
index 00000000000..3e44523b444
--- /dev/null
+++ b/build/media_source/layouts/js/chromes/outline.es6.js
@@ -0,0 +1,40 @@
+/**
+ * @copyright (C) 2021 Open Source Matters, Inc.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+(() => {
+ 'use strict';
+
+ /**
+ * Javascript to insert the onclick event
+ * We get the position associated to the button that has been clicked
+ * and we use it to set the position of joomla field fancy select element
+ * of the parent window.
+ */
+
+ document.addEventListener('DOMContentLoaded', () => {
+ // Get the elements
+ const elements = document.querySelectorAll('.jmod-position-select');
+
+ for (let i = 0, l = elements.length; l > i; i += 1) {
+ // Listen for click event
+ elements[i].addEventListener('click', (event) => {
+ const position = event.target.getAttribute('data-position');
+
+ // Select value of the choices field by triggering mousedown event on the position option
+ const elPositionSelect = window.parent.parent.document.querySelector('#jform_position');
+ const elPositionOptions = elPositionSelect.closest('.choices').querySelectorAll('.choices__item--selectable');
+ for (let j = 0; j < elPositionOptions.length; j += 1) {
+ if (elPositionOptions[j].dataset.value === position) {
+ elPositionOptions[j].dispatchEvent(new Event('mousedown'));
+ }
+ }
+
+ if (window.parent.parent.Joomla.Modal) {
+ window.parent.parent.Joomla.Modal.getCurrent().close();
+ }
+ });
+ }
+ });
+})();
diff --git a/build/media_source/plg_system_addmodulebutton/js/addmodulebutton.es6.js b/build/media_source/plg_system_addmodulebutton/js/addmodulebutton.es6.js
new file mode 100644
index 00000000000..6ee7a748592
--- /dev/null
+++ b/build/media_source/plg_system_addmodulebutton/js/addmodulebutton.es6.js
@@ -0,0 +1,32 @@
+/**
+ * @copyright (C) 2021 Open Source Matters, Inc.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+((document) => {
+ 'use strict';
+
+ document.addEventListener('DOMContentLoaded', () => {
+ const addModuleBtnLabel = Joomla.getOptions('js-addModuleBtn');
+ const elMain = document.querySelector('main');
+
+ // Create the form node
+ const elForm = document.createElement('form');
+ elForm.setAttribute('method', 'get');
+
+ // Hidden input field to pass the get param for place module view
+ const placeModuleFlag = document.createElement('input');
+ placeModuleFlag.setAttribute('type', 'hidden');
+ placeModuleFlag.setAttribute('name', 'pm');
+ placeModuleFlag.setAttribute('value', '1');
+ elForm.appendChild(placeModuleFlag);
+
+ const addModuleBtn = document.createElement('button');
+ addModuleBtn.setAttribute('type', 'submit');
+ addModuleBtn.classList.add('btn', 'jmodadd');
+ addModuleBtn.innerText = addModuleBtnLabel;
+ elForm.appendChild(addModuleBtn);
+
+ // Append the form
+ elMain.appendChild(elForm);
+ });
+})(document);
diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql
index 99199575edf..3f14624af1f 100644
--- a/installation/sql/mysql/base.sql
+++ b/installation/sql/mysql/base.sql
@@ -321,6 +321,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`,
(0, 'plg_sampledata_multilang', 'plugin', 'multilang', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 2, 0),
(0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', '', 1, 0),
(0, 'plg_system_actionlogs', 'plugin', 'actionlogs', 'system', 0, 1, 1, 0, 1, '', '{}', '', 2, 0),
+(0, 'plg_system_addmodulebutton', 'plugin', 'addmodulebutton', 'system', 0, 1, 1, 0, 1, '', '{}', '', 1, 0),
(0, 'plg_system_cache', 'plugin', 'cache', 'system', 0, 0, 1, 0, 1, '', '{"browsercache":"0","cachetime":"15"}', '', 3, 0),
(0, 'plg_system_debug', 'plugin', 'debug', 'system', 0, 1, 1, 0, 1, '', '{"profile":"1","queries":"1","memory":"1","language_files":"1","language_strings":"1","strip-first":"1","strip-prefix":"","strip-suffix":""}', '', 4, 0),
(0, 'plg_system_fields', 'plugin', 'fields', 'system', 0, 1, 1, 0, 1, '', '', '', 5, 0),
diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql
index 8cda51c8fee..81848f38e74 100644
--- a/installation/sql/postgresql/base.sql
+++ b/installation/sql/postgresql/base.sql
@@ -327,6 +327,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder",
(0, 'plg_sampledata_multilang', 'plugin', 'multilang', 'sampledata', 0, 1, 1, 0, 1, '', '', '', 2, 0),
(0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', '', 1, 0),
(0, 'plg_system_actionlogs', 'plugin', 'actionlogs', 'system', 0, 1, 1, 0, 1, '', '{}', '', 2, 0),
+(0, 'plg_system_addmodulebutton', 'plugin', 'addmodulebutton', 'system', 0, 1, 1, 0, 1, '', '{}', '', 1, 0),
(0, 'plg_system_cache', 'plugin', 'cache', 'system', 0, 0, 1, 0, 1, '', '{"browsercache":"0","cachetime":"15"}', '', 3, 0),
(0, 'plg_system_debug', 'plugin', 'debug', 'system', 0, 1, 1, 0, 1, '', '{"profile":"1","queries":"1","memory":"1","language_files":"1","language_strings":"1","strip-first":"1","strip-prefix":"","strip-suffix":""}', '', 4, 0),
(0, 'plg_system_fields', 'plugin', 'fields', 'system', 0, 1, 1, 0, 1, '', '', '', 5, 0),
diff --git a/language/en-GB/joomla.ini b/language/en-GB/joomla.ini
index 7a6ffb736e9..03dece34e4d 100644
--- a/language/en-GB/joomla.ini
+++ b/language/en-GB/joomla.ini
@@ -323,6 +323,8 @@ JGLOBAL_NUM="#"
JGLOBAL_OTPMETHOD_NONE="Disable Two Factor Authentication"
JGLOBAL_PASSWORD="Password"
JGLOBAL_PASSWORD_RESET_REQUIRED="You are required to reset your password before proceeding."
+JGLOBAL_PREVIEW_PLACE_MODULE="Place Module Here"
+JGLOBAL_PREVIEW_PLACE_MODULE_POSITION="in Position: %s"
JGLOBAL_PREVIEW_POSITION="Position: %s"
JGLOBAL_PREVIEW_STYLE="Style: %s"
JGLOBAL_PRINT="Print"
diff --git a/layouts/chromes/outline.php b/layouts/chromes/outline.php
index 1312c50ffc7..1367adfca49 100644
--- a/layouts/chromes/outline.php
+++ b/layouts/chromes/outline.php
@@ -12,21 +12,48 @@
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
-Factory::getApplication()->getDocument()
+$app = Factory::getApplication();
+$app->getDocument()
->getWebAssetManager()
- ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css');
+ ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css')
+ ->registerAndUseScript('layouts.chromes.outline', 'layouts/chromes/outline.js');
$module = $displayData['module'];
+// Place Modules Button
+$showPlaceModuleButton = $app->input->getBool('pm') && ($app->getName() == 'site');
+
+// Attributes of Select Position Tag for Placing Modules
+$menuId = $app->getMenu()->getActive();
+$action = isset($menuId->id) ? 'href="administrator/index.php?option=com_modules&task=module.selectPosition&position=' . $module->position . "&menu=" . $menuId->id . '"' : '';
+$tag = "a";
+
+// True for Backend Edit Module Position's Modal Iframe
+if ($showPlaceModuleButton && $app->input->getBool('edit'))
+{
+ $tag = "button";
+ $action = 'data-position="' . $module->position . '"';
+}
?>
position); ?>
-
- style); ?>
-
+
+
+ < class="btn btn-sm btn-info jmod-position-select" >
+
+
+ position); ?>
+
+ >
+
+
+
+ style); ?>
+
+
content; ?>
diff --git a/libraries/src/Document/Renderer/Html/ModulesRenderer.php b/libraries/src/Document/Renderer/Html/ModulesRenderer.php
index 70629c347e1..90f277bc080 100644
--- a/libraries/src/Document/Renderer/Html/ModulesRenderer.php
+++ b/libraries/src/Document/Renderer/Html/ModulesRenderer.php
@@ -40,7 +40,7 @@ public function render($position, $params = array(), $content = null)
$app = Factory::getApplication();
$user = Factory::getUser();
- $frontediting = ($app->isClient('site') && $app->get('frontediting', 1) && !$user->guest);
+ $frontediting = ($app->isClient('site') && $app->get('frontediting', 1) && !$user->guest && !$app->input->get('pm'));
$menusEditing = ($app->get('frontediting', 1) == 2) && $user->authorise('core.edit', 'com_menus');
foreach (ModuleHelper::getModules($position) as $mod)
diff --git a/libraries/src/Helper/ModuleHelper.php b/libraries/src/Helper/ModuleHelper.php
index b955e276469..1efad8f7bae 100644
--- a/libraries/src/Helper/ModuleHelper.php
+++ b/libraries/src/Helper/ModuleHelper.php
@@ -15,6 +15,7 @@
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
+use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
@@ -100,7 +101,7 @@ public static function &getModules($position)
if (\count($result) === 0)
{
- if ($input->getBool('tp') && ComponentHelper::getParams('com_templates')->get('template_positions_display'))
+ if ($input->getBool('pm') || ($input->getBool('tp') && ComponentHelper::getParams('com_templates')->get('template_positions_display')))
{
$result[0] = static::createDummyModule();
$result[0]->title = $position;
@@ -208,7 +209,7 @@ public static function renderModule($module, $attribs = array())
}
// Dynamically add outline style
- if ($app->input->getBool('tp') && ComponentHelper::getParams('com_templates')->get('template_positions_display'))
+ if (($app->input->getBool('tp') && ComponentHelper::getParams('com_templates')->get('template_positions_display')) || ($app->input->getBool('pm') && ContentHelper::getActions('com_modules')->get('core.create')))
{
$attribs['style'] .= ' outline';
}
diff --git a/plugins/system/addmodulebutton/addmodulebutton.php b/plugins/system/addmodulebutton/addmodulebutton.php
new file mode 100644
index 00000000000..eeb3e1c942e
--- /dev/null
+++ b/plugins/system/addmodulebutton/addmodulebutton.php
@@ -0,0 +1,88 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ * @since __DEPLOY_VERSION__
+ */
+
+defined('_JEXEC') or die;
+use Joomla\CMS\Helper\ContentHelper;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\CMS\WebAsset\WebAssetManager;
+/**
+ * Displays the Add Module button for Frontend Placement.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+class PlgSystemAddModuleButton extends CMSPlugin
+{
+ /**
+ * Load plugin language files automatically
+ *
+ * @var boolean
+ * @since 3.9.0
+ */
+ protected $autoloadLanguage = true;
+
+ /**
+ * Application object
+ *
+ * @var \Joomla\CMS\Application\CMSApplication
+ * @since 3.2
+ */
+ protected $app;
+
+ /**
+ * Listener for the `onBeforeRender` event
+ *
+ * @return void
+ *
+ * @since 1.0
+ */
+ public function onBeforeRender()
+ {
+
+ if ($this->app->isClient('administrator'))
+ {
+ return;
+ }
+
+ // Frontend Module Placement Variables
+ $canCreateModules = ContentHelper::getActions('com_modules')->get('core.create');
+ $canEditModules = ContentHelper::getActions('com_modules')->get('core.edit');
+ $placeModules = $this->app->input->getBool('pm');
+ $editPosition = $this->app->input->getBool('edit');
+ $showAddModuleBtn = $canCreateModules && !$this->app->input->getBool('tp') && !$placeModules;
+
+ // Display Warning message when user is not logged in or does not have permissions
+ if ($placeModules)
+ {
+ if ($editPosition && !$canEditModules)
+ {
+ $this->app->enqueueMessage(Text::sprintf('PLG_SYSTEM_ADD_MODULE_BUTTON_EDIT_MODULE_PERMISSIONS_WARNING'), 'warning');
+
+ return;
+ }
+ elseif (!$canCreateModules)
+ {
+ $this->app->enqueueMessage(Text::sprintf('PLG_SYSTEM_ADD_MODULE_BUTTON_CREATE_MODULE_PERMISSIONS_WARNING'), 'warning');
+
+ return;
+ }
+ }
+
+ // Display the Add Module Button
+ if ($showAddModuleBtn)
+ {
+ // Add Script Options to pass the Button label Language Constant
+ $this->app->getDocument()->addScriptOptions('js-addModuleBtn', Text::_('PLG_SYSTEM_ADD_MODULE_BUTTON_LABEL'));
+
+ // Script for appending the Add Module Button
+ $this->app->getDocument()->getWebAssetManager()
+ ->registerAndUseScript('plg_system_addmodulebutton_js', 'media/plg_system_addmodulebutton/js/addmodulebutton.js', [], ['defer' => true]);
+ }
+ }
+}
diff --git a/plugins/system/addmodulebutton/addmodulebutton.xml b/plugins/system/addmodulebutton/addmodulebutton.xml
new file mode 100644
index 00000000000..e0a44a2c51f
--- /dev/null
+++ b/plugins/system/addmodulebutton/addmodulebutton.xml
@@ -0,0 +1,22 @@
+
+
+ plg_system_addmodulebutton
+ Joomla! Project
+ June 2021
+ (C) 2021 Open Source Matters, Inc.
+ GNU General Public License version 2 or later; see LICENSE.txt
+ admin@joomla.org
+ www.joomla.org
+ __DEPLOY_VERSION__
+ PLG_ADD_MODULE_BUTTON_XML_DESCRIPTION
+
+ addmodulebutton.php
+
+
+ language/en-GB/en-GB.plg_system_addmodulebutton.ini
+ language/en-GB/en-GB.plg_system_addmodulebutton.sys.ini
+
+
+ js
+
+
diff --git a/templates/cassiopeia/index.php b/templates/cassiopeia/index.php
index 517fc465b0e..43eae6fb284 100644
--- a/templates/cassiopeia/index.php
+++ b/templates/cassiopeia/index.php
@@ -10,6 +10,7 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
+use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
@@ -34,6 +35,10 @@
$menu = $app->getMenu()->getActive();
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';
+// Display inactive positions for Place Module View
+$modulesPermissions = ContentHelper::getActions('com_modules');
+$showAllPositions = $app->input->getBool('pm') && ($modulesPermissions->get('core.edit') || $modulesPermissions->get('core.create'));
+
// Template path
$templatePath = 'templates/' . $this->template;
@@ -79,12 +84,12 @@
$hasClass = '';
-if ($this->countModules('sidebar-left', true))
+if ($this->countModules('sidebar-left', true) || $showAllPositions)
{
$hasClass .= ' has-sidebar-left';
}
-if ($this->countModules('sidebar-right', true))
+if ($this->countModules('sidebar-right', true) || $showAllPositions)
{
$hasClass .= ' has-sidebar-right';
}
@@ -119,19 +124,19 @@
?>">