├── src ├── Resources │ ├── contao │ │ ├── templates │ │ │ ├── form_rsce_plain.html5 │ │ │ ├── be_rsce_data.html5 │ │ │ ├── be_rsce_group.html5 │ │ │ ├── be_rsce_list.html5 │ │ │ └── be_rsce_convert.html5 │ │ ├── languages │ │ │ ├── de │ │ │ │ ├── tl_module.php │ │ │ │ ├── tl_content.php │ │ │ │ ├── tl_form_field.php │ │ │ │ ├── default.php │ │ │ │ ├── tl_maintenance.php │ │ │ │ └── rocksolid_custom_elements.php │ │ │ └── en │ │ │ │ ├── tl_module.php │ │ │ │ ├── tl_content.php │ │ │ │ ├── tl_form_field.php │ │ │ │ ├── default.php │ │ │ │ ├── tl_maintenance.php │ │ │ │ └── rocksolid_custom_elements.php │ │ ├── dca │ │ │ ├── tl_templates.php │ │ │ ├── tl_module.php │ │ │ ├── tl_content.php │ │ │ └── tl_form_field.php │ │ └── config │ │ │ └── config.php │ ├── public │ │ ├── img │ │ │ └── logo.png │ │ ├── css │ │ │ └── be_main.css │ │ └── js │ │ │ └── be_main.js │ └── config │ │ └── services.yaml ├── ContaoManagerPlugin.php ├── Model │ └── DummyModel.php ├── Form │ ├── CustomWidgetNoInput.php │ └── CustomWidget.php ├── RockSolidCustomElementsBundle.php ├── Widget │ ├── ListItemStop.php │ ├── ListStop.php │ ├── Data.php │ ├── GroupStart.php │ ├── ListStart.php │ └── ListItemStart.php ├── EventListener │ └── AddAssetsListener.php ├── Migration │ └── BasicEntitiesMigration.php ├── DependencyInjection │ └── RockSolidCustomElementsExtension.php ├── Template │ └── CustomTemplate.php ├── CustomElementsConvert.php ├── Element │ └── CustomElement.php └── CustomElements.php ├── README.md ├── LICENSE └── composer.json /src/Resources/contao/templates/form_rsce_plain.html5: -------------------------------------------------------------------------------- 1 | generate(); 3 | -------------------------------------------------------------------------------- /src/Resources/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeyourday/contao-rocksolid-custom-elements/HEAD/src/Resources/public/img/logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RockSolid Custom Elements Contao Extension 2 | 3 | ## Installation 4 | 5 | ```sh 6 | $ composer require madeyourday/contao-rocksolid-custom-elements 7 | ``` 8 | 9 | ## Documentation: 10 | 11 | * English: https://rocksolidthemes.com/en/contao/plugins/custom-content-elements 12 | * German: https://rocksolidthemes.com/de/contao/plugins/custom-content-elements 13 | -------------------------------------------------------------------------------- /src/Resources/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autoconfigure: true 4 | 5 | MadeYourDay\RockSolidCustomElements\Migration\BasicEntitiesMigration: 6 | arguments: 7 | - '@database_connection' 8 | 9 | MadeYourDay\RockSolidCustomElements\EventListener\AddAssetsListener: 10 | arguments: 11 | - '@contao.routing.scope_matcher' 12 | tags: 13 | - kernel.event_listener 14 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/tl_module.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_module translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_module']['rsce_data'][0] = 'Custom Elements Daten'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/tl_module.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_module translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_module']['rsce_data'][0] = 'Custom Elements Data'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/tl_content.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_content translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_content']['rsce_data'][0] = 'Custom Elements Daten'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/tl_content.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_content translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_content']['rsce_data'][0] = 'Custom Elements Data'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/tl_form_field.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_form_field translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_form_field']['rsce_data'][0] = 'Custom Elements Data'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/tl_form_field.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements tl_form_field translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_form_field']['rsce_data'][0] = 'Custom Elements Daten'; 16 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/be_rsce_data.html5: -------------------------------------------------------------------------------- 1 | 2 | =')): ?> 3 | generate() ?> 4 | 5 | =')): ?>
6 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/be_rsce_group.html5: -------------------------------------------------------------------------------- 1 | 2 |
=')): ?>
3 | generate() ?> 4 | 5 | =')): ?>
6 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/be_rsce_list.html5: -------------------------------------------------------------------------------- 1 | 2 |
=')): ?>
3 | generate() ?> 4 | 5 | =')): ?>
6 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/default.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements category translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['CTE']['custom_elements'] = 'Custom Elements'; 16 | $GLOBALS['TL_LANG']['FMD']['custom_elements'] = 'Custom Elements'; 17 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/default.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements category translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['CTE']['custom_elements'] = 'Custom Elements'; 16 | $GLOBALS['TL_LANG']['FMD']['custom_elements'] = 'Custom Elements'; 17 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/tl_maintenance.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements maintenance job translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_maintenance_jobs']['rocksolid_custom_elements'][0] = 'Clear custom elements cache'; 16 | $GLOBALS['TL_LANG']['tl_maintenance_jobs']['rocksolid_custom_elements'][1] = 'Deletes the saved DCA files of all RockSolid Custom Elements.'; 17 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/tl_maintenance.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements maintenance job translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['tl_maintenance_jobs']['rocksolid_custom_elements'][0] = 'Custom Elements Cache leeren'; 16 | $GLOBALS['TL_LANG']['tl_maintenance_jobs']['rocksolid_custom_elements'][1] = 'Löscht die gespeicherten DCA-Dateien aller RockSolid Custom Elements.'; 17 | -------------------------------------------------------------------------------- /src/ContaoManagerPlugin.php: -------------------------------------------------------------------------------- 1 | setLoadAfter([ContaoCoreBundle::class]) 20 | ->setReplace(['rocksolid-custom-elements']), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Model/DummyModel.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Model; 10 | 11 | use Contao\Database\Result; 12 | use Contao\Model; 13 | 14 | /** 15 | * Dummy model 16 | * 17 | * @author Martin Auswöger 18 | */ 19 | class DummyModel extends Model 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function __construct(?Result $objResult = null, $data = array()) 25 | { 26 | $this->arrModified = array(); 27 | $this->setRow(is_array($data) ? $data : array()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_templates.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements DCA 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | use Contao\Config; 16 | 17 | if (!empty($GLOBALS['TL_DCA']['tl_templates']['config']['validFileTypes'])) { 18 | $GLOBALS['TL_DCA']['tl_templates']['config']['validFileTypes'] .= ',php'; 19 | } 20 | if (!empty($GLOBALS['TL_DCA']['tl_templates']['config']['editableFileTypes'])) { 21 | $GLOBALS['TL_DCA']['tl_templates']['config']['editableFileTypes'] .= ',php'; 22 | } 23 | -------------------------------------------------------------------------------- /src/Form/CustomWidgetNoInput.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Form; 10 | 11 | /** 12 | * Custom form widget 13 | * 14 | * @author Martin Auswöger 15 | */ 16 | class CustomWidgetNoInput extends CustomWidget 17 | { 18 | protected $blnSubmitInput = false; 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function generate() 24 | { 25 | return $this->customElement->generate(); 26 | } 27 | 28 | /** 29 | * Do not validate 30 | */ 31 | public function validate() 32 | { 33 | return; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/RockSolidCustomElementsBundle.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements; 10 | 11 | use MadeYourDay\RockSolidCustomElements\DependencyInjection\RockSolidCustomElementsExtension; 12 | use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 13 | use Symfony\Component\HttpKernel\Bundle\Bundle; 14 | 15 | /** 16 | * Configures the RockSolid Custom Elements bundle. 17 | * 18 | * @author Martin Auswöger 19 | */ 20 | class RockSolidCustomElementsBundle extends Bundle 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getContainerExtension(): ?ExtensionInterface 26 | { 27 | return new RockSolidCustomElementsExtension(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Widget/ListItemStop.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\Widget; 12 | 13 | /** 14 | * List item stop widget 15 | * 16 | * @author Martin Auswöger 17 | */ 18 | class ListItemStop extends Widget 19 | { 20 | /** 21 | * @var boolean Submit user input 22 | */ 23 | protected $blnSubmitInput = false; 24 | 25 | /** 26 | * @var string Template 27 | */ 28 | protected $strTemplate = 'be_rsce_list'; 29 | 30 | /** 31 | * Generate the widget and return it as string 32 | * 33 | * @return string 34 | */ 35 | public function generate() 36 | { 37 | return 'arrAttributes['disabled'] ?? false) ? 'fieldset' : 'div').'>'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/EventListener/AddAssetsListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace MadeYourDay\RockSolidCustomElements\EventListener; 13 | 14 | use Contao\CoreBundle\Routing\ScopeMatcher; 15 | use Symfony\Component\HttpKernel\Event\RequestEvent; 16 | 17 | class AddAssetsListener 18 | { 19 | private ScopeMatcher $scopeMatcher; 20 | 21 | public function __construct(ScopeMatcher $scopeMatcher) 22 | { 23 | $this->scopeMatcher = $scopeMatcher; 24 | } 25 | 26 | public function __invoke(RequestEvent $event): void 27 | { 28 | if ($this->scopeMatcher->isBackendMainRequest($event)) { 29 | $GLOBALS['TL_JAVASCRIPT'][] = 'bundles/rocksolidcustomelements/js/be_main.js'; 30 | $GLOBALS['TL_CSS'][] = 'bundles/rocksolidcustomelements/css/be_main.css'; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Widget/ListStop.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\Widget; 12 | 13 | /** 14 | * List stop widget 15 | * 16 | * @author Martin Auswöger 17 | */ 18 | class ListStop extends Widget 19 | { 20 | /** 21 | * @var boolean Submit user input 22 | */ 23 | protected $blnSubmitInput = false; 24 | 25 | /** 26 | * @var string Template 27 | */ 28 | protected $strTemplate = 'be_rsce_list'; 29 | 30 | /** 31 | * Generate the widget and return it as string 32 | * 33 | * @return string 34 | */ 35 | public function generate() 36 | { 37 | return '' 40 | . '
'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Widget/Data.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\Widget; 12 | 13 | /** 14 | * Data widget 15 | * 16 | * @author Martin Auswöger 17 | */ 18 | class Data extends Widget 19 | { 20 | /** 21 | * @var boolean Submit user input 22 | */ 23 | protected $blnSubmitInput = true; 24 | 25 | /** 26 | * @var string Template 27 | */ 28 | protected $strTemplate = 'be_rsce_data'; 29 | 30 | /** 31 | * Generate the widget and return it as string 32 | * 33 | * @return string 34 | */ 35 | public function generate() 36 | { 37 | return '
' 38 | . '' 39 | . ($this->rsceScript ? '' : ''); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Migration/BasicEntitiesMigration.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements DCA 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_DCA']['tl_module']['config']['onload_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onloadCallback'); 16 | $GLOBALS['TL_DCA']['tl_module']['config']['onsubmit_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onsubmitCallback'); 17 | $GLOBALS['TL_DCA']['tl_module']['config']['onshow_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onshowCallback'); 18 | $GLOBALS['TL_DCA']['tl_module']['fields']['rsce_data'] = array( 19 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['rsce_data'], 20 | 'exclude' => true, 21 | 'inputType' => 'rsce_data', 22 | 'sql' => "mediumblob NULL", 23 | 'save_callback' => array( 24 | array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'saveDataCallback'), 25 | ), 26 | ); 27 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_content.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements DCA 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_DCA']['tl_content']['config']['onload_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onloadCallback'); 16 | $GLOBALS['TL_DCA']['tl_content']['config']['onsubmit_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onsubmitCallback'); 17 | $GLOBALS['TL_DCA']['tl_content']['config']['onshow_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onshowCallback'); 18 | $GLOBALS['TL_DCA']['tl_content']['fields']['rsce_data'] = array( 19 | 'label' => &$GLOBALS['TL_LANG']['tl_content']['rsce_data'], 20 | 'exclude' => true, 21 | 'inputType' => 'rsce_data', 22 | 'sql' => "mediumblob NULL", 23 | 'save_callback' => array( 24 | array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'saveDataCallback'), 25 | ), 26 | ); 27 | -------------------------------------------------------------------------------- /src/Resources/contao/dca/tl_form_field.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements DCA 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_DCA']['tl_form_field']['config']['onload_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onloadCallback'); 16 | $GLOBALS['TL_DCA']['tl_form_field']['config']['onsubmit_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onsubmitCallback'); 17 | $GLOBALS['TL_DCA']['tl_form_field']['config']['onshow_callback'][] = array('MadeYourDay\RockSolidCustomElements\CustomElements', 'onshowCallback'); 18 | $GLOBALS['TL_DCA']['tl_form_field']['fields']['rsce_data'] = array( 19 | 'label' => &$GLOBALS['TL_LANG']['tl_form_field']['rsce_data'], 20 | 'exclude' => true, 21 | 'inputType' => 'rsce_data', 22 | 'sql' => "mediumblob NULL", 23 | 'save_callback' => array( 24 | array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'saveDataCallback'), 25 | ), 26 | ); 27 | -------------------------------------------------------------------------------- /src/DependencyInjection/RockSolidCustomElementsExtension.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\DependencyInjection; 10 | 11 | use Symfony\Component\Config\FileLocator; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 14 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 15 | 16 | /** 17 | * RockSolid Custom Elements bundle extension. 18 | * 19 | * @author Martin Auswöger 20 | */ 21 | class RockSolidCustomElementsExtension extends Extension 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getAlias(): string 27 | { 28 | return 'rocksolid_custom_elements'; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function load(array $configs, ContainerBuilder $container) 35 | { 36 | $loader = new YamlFileLoader( 37 | $container, 38 | new FileLocator(__DIR__.'/../Resources/config') 39 | ); 40 | 41 | $loader->load('services.yaml'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/en/rocksolid_custom_elements.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['new_list_item'] = 'New element'; 16 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_up'] = 'Move one position up'; 17 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_down'] = 'Move one position down'; 18 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_delete'] = 'Delete element'; 19 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_new'] = 'Create new element'; 20 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_duplicate'] = 'Duplicate element'; 21 | 22 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_headline'] = 'RockSolid Custom Elements'; 23 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_description'] = 'Convert all RockSolid Custom Elements back into standard HTML.'; 24 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_submit'] = 'Convert elements to HTML'; 25 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_confirm'] = 'Do you really want to convert all Custom Elements?' . " \n" . 'This step cannot be reverted!'; 26 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_success'] = '%s elements successfully converted.'; 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"madeyourday/contao-rocksolid-custom-elements", 3 | "description":"Create your own, nestable content elements using a modular system. End the WYSIWYG chaos with your own content elements.", 4 | "keywords":["contao","custom-elements","content-elements"], 5 | "type":"contao-bundle", 6 | "homepage":"https://rocksolidthemes.com/de/contao/plugins/custom-content-elements", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"RockSolid Themes", 11 | "homepage":"https://rocksolidthemes.com/de/contao-themes", 12 | "role":"Developer" 13 | } 14 | ], 15 | "support":{ 16 | "forum":"http://help.rocksolidthemes.com/discussions/contao", 17 | "issues":"https://github.com/madeyourday/contao-rocksolid-custom-elements/issues", 18 | "source":"https://github.com/madeyourday/contao-rocksolid-custom-elements" 19 | }, 20 | "require":{ 21 | "php":">=7.4", 22 | "contao/core-bundle":"^4.13 || ^5.0" 23 | }, 24 | "require-dev": { 25 | "contao/manager-plugin": "^2.0" 26 | }, 27 | "suggest": { 28 | "madeyourday/contao-rocksolid-columns": "For grid column support. See documentation for more information." 29 | }, 30 | "conflict": { 31 | "contao/core": "*", 32 | "contao/core-bundle": "4.4.1", 33 | "contao/manager-plugin": "<2.0 || >=3.0", 34 | "madeyourday/contao-rocksolid-frontend-helper": "<=2.1.2" 35 | }, 36 | "autoload":{ 37 | "psr-4": { 38 | "MadeYourDay\\RockSolidCustomElements\\": "src/" 39 | } 40 | }, 41 | "replace":{ 42 | "contao-legacy/rocksolid-custom-elements":"self.version" 43 | }, 44 | "extra":{ 45 | "contao-manager-plugin": "MadeYourDay\\RockSolidCustomElements\\ContaoManagerPlugin" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Resources/contao/languages/de/rocksolid_custom_elements.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements translations 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['new_list_item'] = 'Neues Element'; 16 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_up'] = 'Eine Position nach oben verschieben'; 17 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_down'] = 'Eine Position nach unten verschieben'; 18 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_delete'] = 'Element löschen'; 19 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_new'] = 'Neues Element erstellen'; 20 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['list_item_duplicate'] = 'Element duplizieren'; 21 | 22 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_headline'] = 'RockSolid Custom Elements'; 23 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_description'] = 'Hier können Sie alle RockSolid Custom Elements in Standard-HTML-Module umwandeln.'; 24 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_submit'] = 'Elemente in HTML konvertieren'; 25 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_confirm'] = 'Wollen Sie wirklich alle Custom Elements umwandeln?' . " \n" . 'Dieser Schritt kann nicht rückgängig gemach werden!'; 26 | $GLOBALS['TL_LANG']['rocksolid_custom_elements']['convert_success'] = '%s Elemente wurden erfolgreich umgewandelt.'; 27 | -------------------------------------------------------------------------------- /src/Resources/contao/templates/be_rsce_convert.html5: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | isActive): ?> 6 | 7 | failedElements): ?> 8 |
9 | failedElements as $element): ?> 10 |

FAILED: -element, ID: , Template:

11 | 12 |
13 | 14 |
15 |

elementsCount) ?> 16 |

17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |

27 |
28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /src/Form/CustomWidget.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Form; 10 | 11 | use Contao\Widget; 12 | use MadeYourDay\RockSolidCustomElements\Element\CustomElement; 13 | use MadeYourDay\RockSolidCustomElements\Model\DummyModel; 14 | 15 | /** 16 | * Custom form widget 17 | * 18 | * @author Martin Auswöger 19 | */ 20 | class CustomWidget extends Widget 21 | { 22 | protected $blnSubmitInput = true; 23 | 24 | /** 25 | * @var string Template 26 | */ 27 | protected $strTemplate = 'form_rsce_plain'; 28 | 29 | /** 30 | * @var CustomElement 31 | */ 32 | protected $customElement; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function __construct($data = null) 38 | { 39 | if (!empty($data['class'])) { 40 | $data['cssID'] = serialize(array('', $data['class'])); 41 | } 42 | 43 | $this->customElement = new CustomElement(new DummyModel(null, (array) $data)); 44 | 45 | parent::__construct($data); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function generate() 52 | { 53 | $cssID = $this->customElement->cssID; 54 | $cssID[1] = (isset($cssID[1]) ? $cssID[1] . ' ' : '') . $this->class; 55 | $this->customElement->cssID = $cssID; 56 | 57 | $this->customElement->value = $this->value; 58 | 59 | foreach ([ 60 | 'hasErrors', 61 | 'getErrors', 62 | 'getErrorAsString', 63 | 'getErrorsAsString', 64 | 'getErrorAsHTML', 65 | 'getAttributes', 66 | 'getAttribute', 67 | ] as $methodName) { 68 | $this->customElement->$methodName = function() use($methodName) { 69 | return call_user_func_array([$this, $methodName], func_get_args()); 70 | }; 71 | } 72 | 73 | return $this->customElement->generate(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Resources/contao/config/config.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | /** 10 | * RockSolid Custom Elements configuration 11 | * 12 | * @author Martin Auswöger 13 | */ 14 | 15 | $GLOBALS['TL_HOOKS']['initializeSystem'][] = array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'loadConfig'); 16 | $GLOBALS['TL_HOOKS']['loadLanguageFile'][] = array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'loadLanguageFileHook'); 17 | $GLOBALS['TL_HOOKS']['exportTheme'][] = array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'exportThemeHook'); 18 | $GLOBALS['TL_HOOKS']['extractThemeFiles'][] = array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'extractThemeFilesHook'); 19 | 20 | $GLOBALS['BE_FFL']['rsce_list_start'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\ListStart'; 21 | $GLOBALS['BE_FFL']['rsce_list_stop'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\ListStop'; 22 | $GLOBALS['BE_FFL']['rsce_list_item_start'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\ListItemStart'; 23 | $GLOBALS['BE_FFL']['rsce_list_item_stop'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\ListItemStop'; 24 | $GLOBALS['BE_FFL']['rsce_group_start'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\GroupStart'; 25 | $GLOBALS['BE_FFL']['rsce_data'] = 'MadeYourDay\\RockSolidCustomElements\\Widget\\Data'; 26 | 27 | $GLOBALS['TL_MAINTENANCE'][] = 'MadeYourDay\\RockSolidCustomElements\\CustomElementsConvert'; 28 | 29 | $GLOBALS['TL_PURGE']['custom']['rocksolid_custom_elements'] = array( 30 | 'callback' => array('MadeYourDay\\RockSolidCustomElements\\CustomElements', 'purgeCache'), 31 | ); 32 | 33 | // Insert the custom_elements category 34 | Contao\ArrayUtil::arrayInsert($GLOBALS['TL_CTE'], 1, array('custom_elements' => array())); 35 | Contao\ArrayUtil::arrayInsert($GLOBALS['FE_MOD'], 0, array('custom_elements' => array())); 36 | -------------------------------------------------------------------------------- /src/Resources/public/css/be_main.css: -------------------------------------------------------------------------------- 1 | fieldset.collapsed fieldset, 2 | fieldset.collapsed p.rsce_list_description, 3 | fieldset.collapsed p.rsce_group_description { 4 | display: none !important; 5 | } 6 | fieldset.rsce_group_no_legend { 7 | padding-top: 0; 8 | border-top: 0; 9 | } 10 | fieldset.rsce_group_no_legend:last-of-type { 11 | padding-bottom: 0 12 | } 13 | .rsce_list_item fieldset.rsce_list, 14 | .rsce_list_item fieldset.rsce_group { 15 | margin: 0; 16 | } 17 | .rsce_list_item fieldset.rsce_group:last-of-type:not(.collapsed) { 18 | padding-bottom: 0; 19 | } 20 | fieldset.rsce_list { 21 | position: relative; 22 | } 23 | .rsce_list_inner { 24 | margin-bottom: -12px; 25 | } 26 | p.rsce_list_description, 27 | p.rsce_group_description { 28 | margin: 10px 15px 20px; 29 | } 30 | .rsce_list .rsce_list p.rsce_list_description { 31 | margin-right: 2%; 32 | margin-left: 2%; 33 | } 34 | .rsce_list_item { 35 | position: relative; 36 | display: inline-block; 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | box-sizing: border-box; 40 | width: calc(100% - 30px); 41 | margin: 0 15px 20px; 42 | padding: 0 0 20px 0; 43 | border: 1px solid var(--content-border, #ccc); 44 | background-color: #fafafa; 45 | background-color: rgba(0, 0, 0, 0.02); 46 | } 47 | html[data-color-scheme=dark] .rsce_list_item { 48 | background-color: rgba(255, 255, 255, 0.02); 49 | } 50 | .rsce_list_item .tl_text_trbl { 51 | background-color: var(--content-bg, #fff); 52 | } 53 | .rsce_list_item_dummy { 54 | display: none; 55 | } 56 | .rsce_list_item .rsce_list_item { 57 | width: 96%; 58 | margin: 0 2% 5px; 59 | border-color: var(--border, #e9e9e9); 60 | } 61 | .rsce_list_item_title { 62 | margin: 0; 63 | padding: 5px 15px; 64 | border-bottom: 1px solid var(--border, #e9e9e9); 65 | background: var(--content-bg, #fff); 66 | font-size: inherit; 67 | font-weight: inherit; 68 | } 69 | .rsce_list_toolbar { 70 | float: right; 71 | margin: 4px 2% 4px; 72 | text-align: right; 73 | } 74 | .rsce_list_item > .rsce_list_toolbar { 75 | position: absolute; 76 | top: 0; 77 | right: 0; 78 | margin: 4px; 79 | } 80 | fieldset.tl_box.rsce_list_stop { 81 | margin: -1px 0 0 0; 82 | padding: 0; 83 | } 84 | -------------------------------------------------------------------------------- /src/Widget/GroupStart.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\CoreBundle\ContaoCoreBundle; 12 | use Contao\System; 13 | use Contao\Widget; 14 | 15 | /** 16 | * Group start widget 17 | * 18 | * @author Martin Auswöger 19 | */ 20 | class GroupStart extends Widget 21 | { 22 | /** 23 | * @var boolean Submit user input 24 | */ 25 | protected $blnSubmitInput = false; 26 | 27 | /** 28 | * @var string Template 29 | */ 30 | protected $strTemplate = 'be_rsce_group'; 31 | 32 | /** 33 | * Generate the widget and return it as string 34 | * 35 | * @return string 36 | */ 37 | public function generate() 38 | { 39 | $this->loadLanguageFile('rocksolid_custom_elements'); 40 | 41 | $classes = [$this->arrConfiguration['tl_class'] ?? '', 'tl_box', 'rsce_group']; 42 | $fs = System::getContainer()->get('request_stack')->getSession()->getBag('contao_backend')->get('fieldset_states'); 43 | 44 | if ( 45 | (isset($fs[$this->strTable][$this->strId]) && !$fs[$this->strTable][$this->strId]) 46 | || (!isset($fs[$this->strTable][$this->strId]) && !empty($this->arrConfiguration['collapsed'])) 47 | ) { 48 | $classes[] = 'collapsed'; 49 | } 50 | 51 | if (version_compare(ContaoCoreBundle::getVersion(), '5.3', '>=')) { 52 | return '
' 53 | . '
' 54 | . '' 64 | . '' 65 | . '' . $this->strLabel 69 | . '' 70 | . '' 71 | . ($this->description ? '

' . $this->description . '

' : ''); 72 | } else { 73 | return '' 74 | . '
' 75 | . '' 79 | . '' . $this->strLabel 82 | . '' 83 | . ($this->description ? '

' . $this->description . '

' : ''); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Widget/ListStart.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\CoreBundle\ContaoCoreBundle; 12 | use Contao\System; 13 | use Contao\Widget; 14 | 15 | /** 16 | * List start widget 17 | * 18 | * @author Martin Auswöger 19 | */ 20 | class ListStart extends Widget 21 | { 22 | /** 23 | * @var boolean Submit user input 24 | */ 25 | protected $blnSubmitInput = false; 26 | 27 | /** 28 | * @var string Template 29 | */ 30 | protected $strTemplate = 'be_rsce_list'; 31 | 32 | /** 33 | * Generate the widget and return it as string 34 | * 35 | * @return string 36 | */ 37 | public function generate() 38 | { 39 | $this->loadLanguageFile('rocksolid_custom_elements'); 40 | 41 | $toolbar = ''; 44 | 45 | $classes = [$this->arrConfiguration['tl_class'] ?? '', 'tl_box', 'rsce_list']; 46 | $fs = System::getContainer()->get('request_stack')->getSession()->getBag('contao_backend')->get('fieldset_states'); 47 | 48 | if ( 49 | (isset($fs[$this->strTable][$this->strId]) && !$fs[$this->strTable][$this->strId]) 50 | || (!isset($fs[$this->strTable][$this->strId]) && !empty($this->arrConfiguration['collapsed'])) 51 | ) { 52 | $classes[] = 'collapsed'; 53 | } 54 | 55 | $config = array( 56 | 'minItems' => $this->minItems, 57 | 'maxItems' => $this->maxItems, 58 | ); 59 | 60 | if (version_compare(ContaoCoreBundle::getVersion(), '5.3', '>=')) { 61 | return '' 62 | . '
' 63 | . 'getAttributes() 74 | . '>' 75 | . '' 76 | . '' . $this->strLabel 80 | . '' 81 | . '' 82 | . $toolbar 83 | . ($this->description ? '

' . $this->description . '

' : ''); 84 | } else { 85 | return '' 86 | . '
' 87 | . 'getAttributes() 92 | . '>' 93 | . '' . $this->strLabel 96 | . '' 97 | . $toolbar 98 | . ($this->description ? '

' . $this->description . '

' : ''); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Template/CustomTemplate.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Template; 10 | 11 | use Contao\FrontendTemplate; 12 | use Contao\System; 13 | use Contao\ThemeModel; 14 | use Symfony\Component\Filesystem\Path; 15 | 16 | /** 17 | * Custom backend template 18 | * 19 | * @author Martin Auswöger 20 | */ 21 | class CustomTemplate extends FrontendTemplate 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function getTemplatePath($template, $format = 'html5', $default = false) 27 | { 28 | if ($default) { 29 | return parent::getTemplatePath($template, $format, $default); 30 | } 31 | 32 | return static::getTemplate($template, $format); 33 | } 34 | 35 | /** 36 | * Get the template path 37 | * 38 | * @param string $template Template name 39 | * @param string $format Format (xhtml or html5) 40 | * @return string Template path 41 | */ 42 | public static function getTemplate($template, $format = 'html5') 43 | { 44 | $templates = static::getTemplates($template, $format); 45 | 46 | return isset($templates[0]) ? $templates[0] : null; 47 | } 48 | 49 | /** 50 | * Get all found template paths 51 | * 52 | * @param string $template Template name 53 | * @param string $format Format (xhtml or html5) 54 | * @return array All template paths for the specified template 55 | */ 56 | public static function getTemplates($template, $format = 'html5') 57 | { 58 | $templates = array(); 59 | 60 | try { 61 | $theme = ThemeModel::findAll(array('order'=>'name')); 62 | } 63 | catch (\Exception $e) { 64 | $theme = null; 65 | } 66 | 67 | while ($theme && $theme->next()) { 68 | if ($theme->templates != '') { 69 | if (file_exists(System::getContainer()->getParameter('kernel.project_dir') . '/' . $theme->templates . '/' . $template . '.' . $format)) { 70 | $templates[] = System::getContainer()->getParameter('kernel.project_dir') . '/' . $theme->templates . '/' . $template . '.' . $format; 71 | } 72 | } 73 | } 74 | 75 | if (file_exists(System::getContainer()->getParameter('kernel.project_dir') . '/templates/' . $template . '.' . $format)) { 76 | $templates[] = System::getContainer()->getParameter('kernel.project_dir') . '/templates/' . $template . '.' . $format; 77 | } 78 | 79 | // Add templates of inactive themes to the bottom of the templates array 80 | $allFiles = glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/*/' . $template . '.' . $format) ?: array(); 81 | foreach ($allFiles as $file) { 82 | if (!in_array($file, $templates)) { 83 | $templates[] = $file; 84 | } 85 | } 86 | 87 | if (count($templates)) { 88 | return $templates; 89 | } 90 | 91 | return array(parent::getTemplate($template, $format)); 92 | } 93 | 94 | protected function renderTwigSurrogateIfExists(): ?string 95 | { 96 | $backupTemplate = $this->strTemplate; 97 | $templatesDir = Path::join(System::getContainer()->getParameter('kernel.project_dir'), 'templates'); 98 | 99 | try { 100 | $themeTemplate = static::getTemplate($this->strTemplate, 'html.twig'); 101 | 102 | if ($themeTemplate && Path::isBasePath($templatesDir, $themeTemplate)) { 103 | $this->strTemplate = substr(Path::makeRelative($themeTemplate, $templatesDir), 0, -10); 104 | } 105 | } 106 | catch (\Throwable $e) { 107 | // Ignore 108 | } 109 | 110 | try { 111 | return parent::renderTwigSurrogateIfExists(); 112 | } 113 | finally { 114 | $this->strTemplate = $backupTemplate; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Widget/ListItemStart.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Widget; 10 | 11 | use Contao\Image; 12 | use Contao\Widget; 13 | 14 | /** 15 | * List item start widget 16 | * 17 | * @author Martin Auswöger 18 | */ 19 | class ListItemStart extends Widget 20 | { 21 | /** 22 | * @var boolean Submit user input 23 | */ 24 | protected $blnSubmitInput = false; 25 | 26 | /** 27 | * @var string Template 28 | */ 29 | protected $strTemplate = 'be_rsce_list'; 30 | 31 | /** 32 | * Generate the widget and return it as string 33 | * 34 | * @return string 35 | */ 36 | public function generate() 37 | { 38 | $fieldName = substr($this->strId, 0, -21); 39 | $fieldIndex = explode('__', $fieldName); 40 | $fieldIndex = (int) $fieldIndex[count($fieldIndex) - 1]; 41 | 42 | $toolbar = ''; 50 | 51 | return '<' 52 | . (($this->arrAttributes['disabled'] ?? false) ? 'fieldset disabled' : 'div') 53 | . ' class="rsce_list_item' 54 | . (empty($this->arrConfiguration['tl_class']) ? '' : ' ' . $this->arrConfiguration['tl_class']) 55 | . '" data-rsce-name="' . $fieldName . '">' 56 | . ($this->strLabel ? '

' . $this->strLabel . '

' : '') 57 | . $toolbar 58 | . '
'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CustomElementsConvert.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements; 10 | 11 | use Contao\ArticleModel; 12 | use Contao\Backend; 13 | use Contao\BackendTemplate; 14 | use Contao\ContentModel; 15 | use Contao\CoreBundle\Monolog\ContaoContext; 16 | use Contao\Database; 17 | use Contao\Environment; 18 | use Contao\FormFieldModel; 19 | use Contao\Input; 20 | use Contao\MaintenanceModuleInterface; 21 | use Contao\ModuleModel; 22 | use Contao\StringUtil; 23 | use Contao\System; 24 | use Contao\Versions; 25 | use MadeYourDay\RockSolidCustomElements\Element\CustomElement; 26 | use Psr\Log\LogLevel; 27 | use Symfony\Component\Security\Csrf\CsrfToken; 28 | 29 | /** 30 | * RockSolid Custom Elements Convert 31 | * 32 | * This is used on the maintenance page in the backend 33 | * 34 | * @author Martin Auswöger 35 | */ 36 | class CustomElementsConvert extends Backend implements MaintenanceModuleInterface 37 | { 38 | /** 39 | * @return boolean True if the module is active 40 | */ 41 | public function isActive() 42 | { 43 | return Input::get('act') == 'rsce_convert'; 44 | } 45 | 46 | /** 47 | * Generate the module 48 | * 49 | * @return string 50 | */ 51 | public function run() 52 | { 53 | $objTemplate = new BackendTemplate('be_rsce_convert'); 54 | $objTemplate->isActive = $this->isActive(); 55 | $objTemplate->action = StringUtil::ampersand(Environment::get('request')); 56 | 57 | // Rebuild the index 58 | if (Input::get('act') === 'rsce_convert') { 59 | 60 | // Check the request token 61 | if (Input::get('rt') === null || !System::getContainer()->get('contao.csrf.token_manager')->isTokenValid(new CsrfToken(System::getContainer()->getParameter('contao.csrf_token_name'), Input::get('rt')))) 62 | { 63 | System::getContainer()->get('request_stack')->getSession()->getBag('contao_backend')->set('INVALID_TOKEN_URL', Environment::get('request')); 64 | $this->redirect('contao/confirm'); 65 | } 66 | 67 | $this->import(Database::class, 'Database'); 68 | 69 | $failedElements = array(); 70 | $elementsCount = 0; 71 | 72 | $contentElements = ContentModel::findBy(array(ContentModel::getTable() . '.type LIKE ?'), 'rsce_%'); 73 | 74 | while ($contentElements && $contentElements->next()) { 75 | 76 | $html = $this->getHtmlFromElement($contentElements); 77 | 78 | if (!$html) { 79 | $failedElements[] = array('content', $contentElements->id, $contentElements->type); 80 | } 81 | else { 82 | 83 | $GLOBALS['TL_DCA'][ContentModel::getTable()]['config']['ptable'] = $contentElements->ptable ?: ArticleModel::getTable(); 84 | 85 | $versions = new Versions(ContentModel::getTable(), $contentElements->id); 86 | 87 | $versions->initialize(); 88 | 89 | $this->Database 90 | ->prepare('UPDATE ' . ContentModel::getTable() . ' SET tstamp = ?, type = \'html\', html = ? WHERE id = ?') 91 | ->execute(time(), $html, $contentElements->id); 92 | $elementsCount++; 93 | 94 | $versions->create(); 95 | 96 | } 97 | 98 | } 99 | 100 | $moduleElements = ModuleModel::findBy(array(ModuleModel::getTable() . '.type LIKE ?'), 'rsce_%'); 101 | 102 | while ($moduleElements && $moduleElements->next()) { 103 | 104 | $html = $this->getHtmlFromElement($moduleElements); 105 | 106 | if (!$html) { 107 | $failedElements[] = array('module', $moduleElements->id, $moduleElements->type); 108 | } 109 | else { 110 | 111 | $versions = new Versions(ModuleModel::getTable(), $moduleElements->id); 112 | 113 | $versions->initialize(); 114 | 115 | $this->Database 116 | ->prepare('UPDATE ' . ModuleModel::getTable() . ' SET tstamp = ?, type = \'html\', html = ? WHERE id = ?') 117 | ->execute(time(), $html, $moduleElements->id); 118 | $elementsCount++; 119 | 120 | $versions->create(); 121 | 122 | } 123 | 124 | } 125 | 126 | $formElements = FormFieldModel::findBy(array(FormFieldModel::getTable() . '.type LIKE ?'), 'rsce_%'); 127 | 128 | while ($formElements && $formElements->next()) { 129 | 130 | $html = $this->getHtmlFromElement($formElements); 131 | 132 | if (!$html) { 133 | $failedElements[] = array('form', $formElements->id, $formElements->type); 134 | } 135 | else { 136 | 137 | $versions = new Versions(FormFieldModel::getTable(), $formElements->id); 138 | 139 | $versions->initialize(); 140 | 141 | $this->Database 142 | ->prepare('UPDATE ' . FormFieldModel::getTable() . ' SET tstamp = ?, type = \'html\', html = ? WHERE id = ?') 143 | ->execute(time(), $html, $formElements->id); 144 | $elementsCount++; 145 | 146 | $versions->create(); 147 | 148 | } 149 | 150 | } 151 | 152 | foreach ($failedElements as $element) { 153 | System::getContainer()->get('monolog.logger.contao')->log( 154 | LogLevel::ERROR, 155 | 'Failed to convert ' . $element[0] . ' element ID ' . $element[1] . ' (' . $element[2] . ') to a standard HTML element', 156 | array('contao' => new ContaoContext(__METHOD__, 'ERROR')), 157 | ); 158 | } 159 | 160 | System::getContainer()->get('monolog.logger.contao')->log( 161 | LogLevel::INFO, 162 | 'Converted ' . $elementsCount . ' RockSolid Custom Elements to standard HTML elements', 163 | array('contao' => new ContaoContext(__METHOD__, 'GENERAL')), 164 | ); 165 | 166 | $objTemplate->elementsCount = $elementsCount; 167 | $objTemplate->failedElements = $failedElements; 168 | 169 | } 170 | 171 | $this->loadLanguageFile('rocksolid_custom_elements'); 172 | 173 | return $objTemplate->parse(); 174 | } 175 | 176 | /** 177 | * Parse a custom element and return the resulting HTML code 178 | * 179 | * @param array $elementData The data to parse the template with 180 | * @return string HTML code 181 | */ 182 | public static function getHtmlFromElement($elementData) 183 | { 184 | try { 185 | $element = new CustomElement($elementData); 186 | return $element->generate(); 187 | } 188 | catch (\Exception $exception) { 189 | return ''; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Element/CustomElement.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements\Element; 10 | 11 | use Contao\ContentElement; 12 | use Contao\ContentModel; 13 | use Contao\Image\PictureConfiguration; 14 | use Contao\Input; 15 | use Contao\ModuleModel; 16 | use Contao\StringUtil; 17 | use Contao\System; 18 | use Contao\Validator; 19 | use MadeYourDay\RockSolidColumns\Element\ColumnsStart; 20 | use MadeYourDay\RockSolidCustomElements\Template\CustomTemplate; 21 | use MadeYourDay\RockSolidCustomElements\CustomElements; 22 | use Symfony\Component\HttpFoundation\Request; 23 | 24 | /** 25 | * Custom content element and frontend module 26 | * 27 | * @author Martin Auswöger 28 | */ 29 | class CustomElement extends ContentElement 30 | { 31 | /** 32 | * @var string Template 33 | */ 34 | protected $strTemplate = 'rsce_default'; 35 | 36 | /** 37 | * Find the correct template and parse it 38 | * 39 | * @return string Parsed template 40 | */ 41 | public function generate() 42 | { 43 | $this->strTemplate = $this->customTpl ?: $this->type; 44 | 45 | // Return output for the backend if in BE mode 46 | if (($output = $this->rsceGetBackendOutput()) !== null) { 47 | return $output; 48 | } 49 | 50 | try { 51 | return parent::generate(); 52 | } 53 | catch (\Exception $exception) { 54 | 55 | if (System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest(System::getContainer()->get('request_stack')->getCurrentRequest() ?? Request::create(''))) { 56 | 57 | $template = new CustomTemplate($this->strTemplate); 58 | $template->setData($this->Template->getData()); 59 | $this->Template = $template; 60 | 61 | return $this->Template->parse(); 62 | 63 | } 64 | 65 | throw $exception; 66 | } 67 | } 68 | 69 | /** 70 | * Generate backend output if TL_MODE is set to BE 71 | * 72 | * @return string|null Backend output or null 73 | */ 74 | public function rsceGetBackendOutput() 75 | { 76 | if (!System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest(System::getContainer()->get('request_stack')->getCurrentRequest() ?? Request::create(''))) { 77 | return null; 78 | } 79 | 80 | $config = CustomElements::getConfigByType($this->type) ?: array(); 81 | 82 | // Handle newsletter output the same way as the frontend 83 | if (!empty($config['isNewsletter'])) { 84 | 85 | if (Input::get('do') === 'newsletter') { 86 | return null; 87 | } 88 | 89 | foreach(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $entry) { 90 | $method = $entry['class'] . '::' . $entry['function']; 91 | if ( 92 | $entry['file'] === System::getContainer()->getParameter('kernel.project_dir') . '/system/modules/newsletter/classes/Newsletter.php' 93 | || $entry['file'] === System::getContainer()->getParameter('kernel.project_dir') . '/vendor/contao/newsletter-bundle/src/Resources/contao/classes/Newsletter.php' 94 | || $entry['file'] === System::getContainer()->getParameter('kernel.project_dir') . '/vendor/contao/newsletter-bundle/contao/classes/Newsletter.php' 95 | || $method === 'Contao\\Newsletter::send' 96 | || $method === 'tl_newsletter::listNewsletters' 97 | ) { 98 | return null; 99 | } 100 | } 101 | 102 | } 103 | 104 | if (!empty($config['beTemplate'])) { 105 | 106 | if (!isset($this->arrData['wildcard'])) { 107 | $label = CustomElements::getLabelTranslated($config['label']); 108 | $this->arrData['wildcard'] = '### ' . mb_strtoupper(is_array($label) ? $label[0] : $label) . ' ###'; 109 | } 110 | 111 | if (!isset($this->arrData['title'])) { 112 | $this->arrData['title'] = $this->headline; 113 | } 114 | 115 | if ( 116 | !isset($this->arrData['link']) 117 | && !isset($this->arrData['href']) 118 | && $this->objModel instanceof ModuleModel 119 | ) { 120 | $this->arrData['link'] = $this->name; 121 | $this->arrData['href'] = StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', ['do' => 'themes', 'table' => 'tl_module', 'act' => 'edit', 'id' => $this->id])); 122 | } 123 | 124 | $this->strTemplate = $config['beTemplate']; 125 | 126 | return null; 127 | } 128 | 129 | if ( 130 | in_array($this->type, $GLOBALS['TL_WRAPPERS']['start']) 131 | || in_array($this->type, $GLOBALS['TL_WRAPPERS']['stop']) 132 | || in_array($this->type, $GLOBALS['TL_WRAPPERS']['separator']) 133 | ) { 134 | return ''; 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /** 141 | * Parse the json data and pass it to the template 142 | * 143 | * @return void 144 | */ 145 | public function compile() 146 | { 147 | // Add an image 148 | if ($this->addImage && trim($this->singleSRC)) { 149 | $figure = System::getContainer() 150 | ->get('contao.image.studio') 151 | ->createFigureBuilder() 152 | ->from($this->singleSRC) 153 | ->setSize(StringUtil::deserialize($this->arrData['size'] ?? null) ?: null) 154 | ->enableLightbox((bool) ($this->arrData['fullsize'] ?? false)) 155 | ->setLightboxSize(StringUtil::deserialize($this->arrData['lightboxSize'] ?? null) ?: null) 156 | ->setMetadata((new ContentModel())->setRow($this->arrData)->getOverwriteMetadata()) 157 | ->buildIfResourceExists(); 158 | 159 | if ($figure) { 160 | $figure->applyLegacyTemplateData($this->Template, null, $this->arrData['floating'] ?? null); 161 | } 162 | } 163 | 164 | $data = array(); 165 | if ($this->rsce_data && substr($this->rsce_data, 0, 1) === '{') { 166 | $data = json_decode($this->rsce_data); 167 | } 168 | 169 | $data = $this->deserializeDataRecursive($data); 170 | 171 | foreach ($data as $key => $value) { 172 | $this->Template->$key = $value; 173 | } 174 | 175 | $self = $this; 176 | 177 | $this->Template->getImageObject = function() use($self) { 178 | return call_user_func_array(array($self, 'getImageObject'), func_get_args()); 179 | }; 180 | $this->Template->getColumnClassName = function() use($self) { 181 | return call_user_func_array(array($self, 'getColumnClassName'), func_get_args()); 182 | }; 183 | 184 | $this->addFragmentControllerDefaults(); 185 | } 186 | 187 | /** 188 | * Deserialize all data recursively 189 | * 190 | * @param array|object $data data array or object 191 | * @return array|object data passed in with deserialized values 192 | */ 193 | protected function deserializeDataRecursive($data) 194 | { 195 | foreach ($data as $key => $value) { 196 | if (is_string($value) && trim($value)) { 197 | if (is_object($data)) { 198 | $data->$key = StringUtil::deserialize($value); 199 | } 200 | else { 201 | $data[$key] = StringUtil::deserialize($value); 202 | } 203 | } 204 | else if (is_array($value) || is_object($value)) { 205 | if (is_object($data)) { 206 | $data->$key = $this->deserializeDataRecursive($value); 207 | } 208 | else { 209 | $data[$key] = $this->deserializeDataRecursive($value); 210 | } 211 | } 212 | } 213 | 214 | if ($data instanceof \stdClass) { 215 | $return = new class extends \stdClass{ 216 | public function __get($name) { 217 | return null; 218 | } 219 | }; 220 | foreach ($data as $key => $value) { 221 | $return->$key = $value; 222 | } 223 | 224 | $data = $return; 225 | } 226 | 227 | return $data; 228 | } 229 | 230 | /** 231 | * Get an image object from id/uuid and an optional size configuration 232 | * 233 | * @param int|string $id ID, UUID string or binary 234 | * @param string|array|PictureConfiguration $size [width, height, mode] optionally serialized or a config object 235 | * @param int $maxSize Gets passed to addImageToTemplate as $intMaxWidth 236 | * @param string $lightboxId Gets passed to addImageToTemplate as $strLightboxId 237 | * @param array $item Gets merged and passed to addImageToTemplate as $arrItem 238 | * @return object Image object (similar as addImageToTemplate) 239 | */ 240 | public function getImageObject($id, $size = null, $deprecated = null, $lightboxId = null, $item = array()) 241 | { 242 | if (!$id) { 243 | return null; 244 | } 245 | 246 | $figure = System::getContainer() 247 | ->get('contao.image.studio') 248 | ->createFigureBuilder() 249 | ->from($id) 250 | ->setSize($size) 251 | ->enableLightbox((bool) ($item['fullsize'] ?? false)) 252 | ->setLightboxGroupIdentifier($lightboxId) 253 | ->setLightboxSize(StringUtil::deserialize($item['lightboxSize'] ?? null) ?: null) 254 | ->setMetadata((new ContentModel())->setRow($item)->getOverwriteMetadata()) 255 | ->buildIfResourceExists(); 256 | 257 | if (null === $figure) { 258 | return null; 259 | } 260 | 261 | return (object) array_merge($figure->getLegacyTemplateData(), ['figure' => $figure]); 262 | } 263 | 264 | /** 265 | * Get the column class name for the specified index 266 | * 267 | * @param int $index Index of the column 268 | * @return string Class name(s) 269 | */ 270 | public function getColumnClassName($index) 271 | { 272 | if (!class_exists(ColumnsStart::class)) { 273 | return ''; 274 | } 275 | 276 | $config = ColumnsStart::getColumnsConfiguration($this->arrData); 277 | 278 | $classes = array('rs-column'); 279 | foreach ($config as $name => $media) { 280 | $classes = array_merge($classes, $media[$index % count($media)]); 281 | if ($index < count($media)) { 282 | $classes[] = '-' . $name . '-first-row'; 283 | } 284 | } 285 | 286 | return implode(' ', $classes); 287 | } 288 | 289 | private function addFragmentControllerDefaults() 290 | { 291 | $this->Template->template ??= $this->Template->getName(); 292 | $this->Template->as_editor_view ??= System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest(System::getContainer()->get('request_stack')->getCurrentRequest() ?? Request::create('')); 293 | $this->Template->data ??= $this->objModel ? $this->objModel->row() : $this->arrData; 294 | $this->Template->nested_fragments ??= []; 295 | $this->Template->section ??= $this->strColumn; 296 | $this->Template->properties ??= []; 297 | $this->Template->element_html_id ??= $this->Template->cssID[0] ?? null; 298 | $this->Template->element_css_classes ??= trim(($this->Template->cssID[1] ?? '') . ' ' . implode(' ', $this->objModel ? (array) $this->objModel->classes : [])); 299 | 300 | if ( 301 | (!\is_string($this->Template->headline) && $this->Template->headline !== null) 302 | || (!\is_string($this->Template->hl) && $this->Template->hl !== null) 303 | ) { 304 | return; 305 | } 306 | 307 | // Legacy templates access the text using `$this->headline`, twig templates use `headline.text` 308 | $this->Template->headline = new class($this->Template->headline, $this->Template->hl) implements \Stringable 309 | { 310 | public ?string $text; 311 | public ?string $tag_name; 312 | 313 | public function __construct(?string $text, ?string $tag_name) 314 | { 315 | $this->text = $text; 316 | $this->tag_name = $tag_name; 317 | } 318 | 319 | public function __toString(): string 320 | { 321 | return $this->text ?? ''; 322 | } 323 | 324 | public function __invoke(): string 325 | { 326 | return $this->text ?? ''; 327 | } 328 | }; 329 | 330 | // The parent::generate() method overwrites the template headline with $this->headline 331 | // so we need to set it to the same callable object here 332 | $this->headline = $this->Template->getData()['headline']; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/Resources/public/js/be_main.js: -------------------------------------------------------------------------------- 1 | ;(function($, window) { 2 | 3 | var renameElement = function(element) { 4 | 5 | element = $(element); 6 | 7 | var index = element.getAllPrevious('.rsce_list_item').length; 8 | var oldName = element.get('data-rsce-name'); 9 | 10 | var newName = oldName.split('__'); 11 | newName[newName.length - 1] = index; 12 | newName = newName.join('__'); 13 | 14 | var attributes = [ 15 | 'name', 16 | 'id', 17 | 'href', 18 | 'onclick', 19 | 'for', 20 | 'value', 21 | 'class', 22 | 'data-rsce-name' 23 | ]; 24 | 25 | element.set('data-rsce-name', newName); 26 | element.getChildren('[data-rsce-label]').each(function(el) { 27 | el.set( 28 | 'text', 29 | el.get('data-rsce-label').split('%s').join(index + 1) 30 | ); 31 | }); 32 | 33 | element.getElements( 34 | '[' + attributes.join('*="' + oldName + '"],[') + '*="' + oldName + '"]' 35 | ).each(function(el) { 36 | attributes.each(function(attribute) { 37 | if (el.get(attribute) && el.get(attribute).split(oldName).length > 1) { 38 | el.set(attribute, el.get(attribute).split(oldName).join(newName)); 39 | } 40 | }); 41 | }); 42 | 43 | element.getElements('script').each(function(el) { 44 | if (el.text && el.text.indexOf(oldName) !== -1) { 45 | el.text = el.text.split(oldName).join(newName); 46 | } 47 | }); 48 | 49 | }; 50 | 51 | var removeTinyMCEs = function(element) { 52 | 53 | element = $(element); 54 | 55 | var editors = window.tinymce ? window.tinymce.get() || [] : []; 56 | var textarea, textareas; 57 | for (var i = editors.length - 1; i >= 0; i--) { 58 | textarea = editors[i].getElement(); 59 | if (element.contains(textarea)) { 60 | textareas = element.retrieve('rsce_tinyMCE_textareas', []); 61 | var settings = editors[i].settings; 62 | if (!settings) { 63 | textarea.parentNode.querySelectorAll(':scope > script').forEach(function (script) { 64 | var match = /tinymce\.init\((\{.*\})\)/s.exec(script.innerHTML); 65 | if (match) { 66 | try { 67 | settings = eval('('+match[1]+')'); 68 | delete settings.selector; 69 | } catch (e) { 70 | } 71 | } 72 | }); 73 | } 74 | textareas.push({ 75 | textarea: textarea, 76 | isStimulus: !!textarea.tinymceConfig, 77 | settings: Object.append({}, settings) 78 | }); 79 | element.store('rsce_tinyMCE_textareas', textareas); 80 | editors[i].remove(); 81 | textarea.removeAttribute('data-controller'); 82 | textarea.removeAttribute('data-action'); 83 | } 84 | } 85 | 86 | }; 87 | 88 | var restoreTinyMCEs = function(element) { 89 | 90 | element = $(element); 91 | 92 | if (window.tinymce && window.tinymce.Editor) { 93 | element.retrieve('rsce_tinyMCE_textareas', []).each(function(data) { 94 | if (data.isStimulus) { 95 | setTimeout(() => { 96 | data.textarea.setAttribute('data-action', 'turbo:before-visit@window->contao--tinymce#leave'); 97 | data.textarea.setAttribute('data-controller', 'contao--tinymce'); 98 | }); 99 | return; 100 | } 101 | new window.tinymce.Editor( 102 | data.textarea.get('id'), 103 | data.settings, 104 | window.tinymce.EditorManager 105 | ).render(); 106 | }); 107 | element.store('rsce_tinyMCE_textareas', []); 108 | } 109 | 110 | }; 111 | 112 | var restoreChosens = function(element) { 113 | 114 | $(element).getElements('.chzn-container').each(function(container) { 115 | var select = container.getPrevious('select'); 116 | if (!select) { 117 | return; 118 | } 119 | select.setStyle('display', '').removeClass('chzn-done'); 120 | container.destroy(); 121 | $$([select]).chosen(); 122 | }); 123 | 124 | }; 125 | 126 | var removePicker = function(element) { 127 | $(element).getElements('script').each(function(script) { 128 | var match; 129 | if (match = script.get('html').match(/new MooRainbow\("(moo_rsce_field_[^"]+)"[^]*?id:\s*"([^"]+)"/)) { 130 | $(match[1]).removeEvents('click'); 131 | $(document.body).getChildren('#'+match[2]).destroy(); 132 | } 133 | else if (match = script.get('html').match(/\$\("((?:pp|ft)_rsce_field_[^"]+)"\)\.addEvent\("click"/)) { 134 | $(match[1]).removeEvents('click'); 135 | } 136 | }); 137 | }; 138 | 139 | var restorePicker = function(element) { 140 | $(element).getElements('script').each(function(script) { 141 | if (script.get('html').match(/new MooRainbow\("(moo_rsce_field_[^"]+)"[^]*?id:\s*"([^"]+)"/)) { 142 | Browser.exec(script.get('html')); 143 | } 144 | else if (script.get('html').match(/\$\("((?:pp|ft)_rsce_field_[^"]+)"\)\.addEvent\("click"/)) { 145 | Browser.exec(script.get('html')); 146 | } 147 | }); 148 | }; 149 | 150 | var removeACEs = function(element) { 151 | $(element).getElements('div.ace_editor').destroy(); 152 | } 153 | 154 | var restoreSelectorScripts = function(element) { 155 | 156 | $(element).getElements('.selector_container').each(function(container) { 157 | 158 | var input = container.getPrevious('input'); 159 | if (container.getElement('script') || !input) { 160 | return; 161 | } 162 | 163 | var name = input.name; 164 | var dummyName = name.replace(/__\d+__/g, '__rsce_dummy__'); 165 | var dummyScript = $(document.body).getElement('input[name="' + dummyName + '"] + .selector_container > script'); 166 | if (!dummyScript) { 167 | return; 168 | } 169 | 170 | var script = new Element('script', { 171 | html: dummyScript.get('html').split(dummyName).join(name), 172 | }).inject(container); 173 | 174 | }); 175 | 176 | } 177 | 178 | var persistSelects = function(element) { 179 | 180 | $(element).getElements('select').each(function(select) { 181 | 182 | var options = select.getElements('option:selected'); 183 | var oldOptions = select.getElements('option[selected]'); 184 | 185 | oldOptions.each(function (oldOption) { 186 | oldOption.removeAttribute('selected'); 187 | }); 188 | 189 | options.each(function (option) { 190 | option.setAttribute('selected', ''); 191 | }); 192 | 193 | }); 194 | 195 | } 196 | 197 | var preserveRadioButtons = function(element) { 198 | $(element).getElements('input[type=radio]:checked').each(function(input) { 199 | input.setAttribute('data-rsce-checked', ''); 200 | }); 201 | } 202 | 203 | var restoreRadioButtons = function(element) { 204 | $(element).getElements('input[type=radio][data-rsce-checked]').each(function(input) { 205 | input.checked = true; 206 | input.removeAttribute('data-rsce-checked'); 207 | }); 208 | } 209 | 210 | var updateListButtons = function(listElement) { 211 | 212 | listElement = $(listElement); 213 | 214 | var allItems = listElement.getChildren('.rsce_list_inner')[0].getChildren('.rsce_list_item'); 215 | var count = allItems.length; 216 | var config = listElement.retrieve('rsce_config', {}); 217 | var minReached = !!(config.minItems && count <= config.minItems); 218 | var maxReached = !!( 219 | typeof config.maxItems === 'number' 220 | && count >= config.maxItems 221 | ); 222 | 223 | listElement.getChildren('.rsce_list_toolbar')[0].getFirst('.header_new').setStyle( 224 | 'display', 225 | maxReached ? 'none' : '' 226 | ); 227 | 228 | allItems.each(function(el, index) { 229 | var toolbar = el.getChildren('.rsce_list_toolbar')[0]; 230 | toolbar.getFirst('.rsce_list_toolbar_up').setStyle( 231 | 'display', 232 | !index ? 'none' : '' 233 | ); 234 | toolbar.getFirst('.rsce_list_toolbar_down').setStyle( 235 | 'display', 236 | index === count - 1 ? 'none' : '' 237 | ); 238 | toolbar.getFirst('.rsce_list_toolbar_drag').setStyle( 239 | 'display', 240 | count < 2 ? 'none' : '' 241 | ); 242 | toolbar.getFirst('.rsce_list_toolbar_delete').setStyle( 243 | 'display', 244 | minReached ? 'none' : '' 245 | ); 246 | toolbar.getFirst('.rsce_list_toolbar_duplicate').setStyle( 247 | 'display', 248 | maxReached ? 'none' : '' 249 | ); 250 | toolbar.getFirst('.rsce_list_toolbar_new').setStyle( 251 | 'display', 252 | maxReached ? 'none' : '' 253 | ); 254 | }); 255 | 256 | }; 257 | 258 | var initListSort = function(listInner) { 259 | 260 | if (!listInner.getElements('.drag-handle').length) { 261 | return; 262 | } 263 | 264 | var ds = new Scroller(document.body, { 265 | onChange: function(x, y) { 266 | this.element.scrollTo(this.element.getScroll().x, y); 267 | } 268 | }); 269 | 270 | listInner.retrieve('listSort', new Sortables(listInner, { 271 | contstrain: true, 272 | opacity: 0.6, 273 | handle: '.drag-handle', 274 | onStart: function(element) { 275 | removeTinyMCEs(element); 276 | preserveRadioButtons(listInner); 277 | ds.start(); 278 | }, 279 | onComplete: function() { 280 | ds.stop(); 281 | removePicker(listInner); 282 | listInner.getChildren('.rsce_list_item').each(function(el) { 283 | removeTinyMCEs(el); 284 | }); 285 | listInner.getChildren('.rsce_list_item').each(function(el) { 286 | renameElement(el); 287 | }); 288 | listInner.getChildren('.rsce_list_item').each(function(el) { 289 | restoreTinyMCEs(el); 290 | }); 291 | updateListButtons(listInner.getParent('.rsce_list')); 292 | restorePicker(listInner); 293 | restoreChosens(listInner); 294 | restoreRadioButtons(listInner); 295 | } 296 | })); 297 | 298 | }; 299 | 300 | var newElementAtPosition = function(listElement, position) { 301 | 302 | listElement = $(listElement); 303 | var config = listElement.retrieve('rsce_config', {}); 304 | 305 | var dummyItem = listElement 306 | .getChildren('.rsce_list_item.rsce_list_item_dummy')[0]; 307 | var listInner = listElement.getChildren('.rsce_list_inner'); 308 | if (!listInner.length) { 309 | listInner = new Element('div', {'class': 'rsce_list_inner'}) 310 | .inject(listElement); 311 | } 312 | else { 313 | listInner = listInner[0]; 314 | } 315 | var allItems = listInner.getChildren('.rsce_list_item'); 316 | 317 | if ( 318 | typeof config.maxItems === 'number' 319 | && allItems.length >= config.maxItems 320 | ) { 321 | return; 322 | } 323 | 324 | var newItem = new Element('div', {'class': 'rsce_list_item'}); 325 | 326 | allItems.each(function(el) { 327 | removeTinyMCEs(el); 328 | removePicker(el); 329 | }); 330 | preserveRadioButtons(listElement); 331 | removeTinyMCEs(dummyItem); 332 | removeACEs(dummyItem); 333 | 334 | var key = dummyItem.get('data-rsce-name') 335 | .substr(0, dummyItem.get('data-rsce-name').length - 12); 336 | var newKey = key + '__' + position; 337 | var newFields = []; 338 | 339 | newItem.set('data-rsce-name', newKey); 340 | 341 | var newItemHtml = dummyItem.get('html') 342 | .split(' data-rsce-required="data-rsce-required"') 343 | .join(' required="required"') 344 | .split(key + '__rsce_dummy') 345 | .join(newKey); 346 | newItem.set('html', newItemHtml); 347 | 348 | copyTinyMceConfigs(dummyItem, key + '__rsce_dummy', newItem, newKey); 349 | 350 | newItem.getChildren('[data-rsce-label]').each(function(el) { 351 | el.set( 352 | 'text', 353 | el.get('data-rsce-label').split('%s').join(position + 1) 354 | ); 355 | }); 356 | 357 | newItem.getElements('.sortable.sortable-done').removeClass('sortable-done'); 358 | 359 | if (position) { 360 | newItem.inject(allItems[position - 1], 'after'); 361 | } 362 | else { 363 | newItem.inject(listInner, 'top'); 364 | } 365 | 366 | newItem.getElements('[name^="' + newKey + '"]').each(function(input) { 367 | if ( 368 | input.getParent('.rsce_list_item') === newItem && 369 | input.get('name').indexOf('__rsce_dummy') === -1 370 | ) { 371 | newFields.push(input.get('name').split('[')[0]); 372 | } 373 | }); 374 | 375 | newItem.getElements('[data-rsce-title]').each(function(el) { 376 | el.set('title', el.get('data-rsce-title')); 377 | }); 378 | 379 | restoreChosens(newItem); 380 | 381 | newItem.grab(new Element('input', { 382 | type: 'hidden', 383 | name: 'FORM_FIELDS[]', 384 | value: newFields.join(',') 385 | })); 386 | 387 | newItem.getAllNext('.rsce_list_item').each(function(el) { 388 | renameElement(el); 389 | restoreChosens(el); 390 | }); 391 | 392 | newItem.getElements('.rsce_list').each(function(el) { 393 | initList(el); 394 | }); 395 | 396 | allItems.each(function(el) { 397 | restorePicker(el); 398 | restoreTinyMCEs(el); 399 | }); 400 | restoreTinyMCEs(newItem); 401 | restoreRadioButtons(listElement); 402 | 403 | executeHtmlScripts(newItemHtml); 404 | 405 | if (listInner.retrieve('listSort')) { 406 | listInner.retrieve('listSort').addItems(newItem); 407 | } 408 | else { 409 | initListSort(listInner); 410 | } 411 | 412 | updateListButtons(listElement); 413 | updateDependingFields(newItem); 414 | 415 | try { 416 | window.fireEvent('subpalette'); 417 | } 418 | catch(e) {} 419 | 420 | try { 421 | window.fireEvent('ajax_change'); 422 | } 423 | catch(e) {} 424 | 425 | }; 426 | 427 | var copyTinyMceConfigs = function(origItem, origKey, newItem, newKey) { 428 | 429 | var textareas = []; 430 | 431 | origItem.retrieve('rsce_tinyMCE_textareas', []).each(function(data) { 432 | if (data.isStimulus) { 433 | return; 434 | } 435 | var newData = { 436 | settings: Object.append({}, data.settings) 437 | }; 438 | // Get textarea by id does not work here 439 | newItem.getElements('textarea').each(function(el) { 440 | if (el.get('id') === data.textarea.get('id').split(origKey).join(newKey)) { 441 | newData.textarea = el; 442 | } 443 | }); 444 | if (newData.textarea) { 445 | textareas.push(newData); 446 | } 447 | }); 448 | 449 | newItem.store('rsce_tinyMCE_textareas', textareas); 450 | 451 | }; 452 | 453 | var executeHtmlScripts = function(html) { 454 | 455 | html.replace(/]*>([\s\S]*?)<\/script>/gi, function(all, code){ 456 | 457 | code = code.replace(/||/g, ''); 458 | 459 | // Ignore tinyMCEs 460 | if (/^\s*window\.tinymce\s*&&\s*tinymce.init\s*\(/.test(code)) { 461 | return ''; 462 | } 463 | 464 | try { 465 | Browser.exec(code); 466 | } 467 | catch(e) {} 468 | 469 | return ''; 470 | 471 | }); 472 | 473 | } 474 | 475 | var newElement = function(linkElement) { 476 | 477 | var listElement = $(linkElement).getParent('.rsce_list'); 478 | 479 | return newElementAtPosition(listElement, 0); 480 | 481 | }; 482 | 483 | var newElementAfter = function(linkElement) { 484 | 485 | var listElement = $(linkElement).getParent('.rsce_list'); 486 | var position = $(linkElement).getParent('.rsce_list_item') 487 | .getAllPrevious('.rsce_list_item').length + 1; 488 | 489 | return newElementAtPosition(listElement, position); 490 | 491 | }; 492 | 493 | var duplicateElement = function(linkElement) { 494 | 495 | var element = $(linkElement).getParent('.rsce_list_item'); 496 | var listInner = element.getParent('.rsce_list_inner'); 497 | 498 | // The order is important to prevent id conflicts: 499 | // remove tinyMCEs => duplicate the element => rename => restoring tinyMCEs 500 | 501 | element.getAllNext('.rsce_list_item').each(function(el) { 502 | removeTinyMCEs(el); 503 | }); 504 | 505 | removeTinyMCEs(element); 506 | removePicker(listInner); 507 | restoreSelectorScripts(element); 508 | persistSelects(element); 509 | preserveRadioButtons(listInner); 510 | 511 | var newItem = element.cloneNode(true); 512 | 513 | copyTinyMceConfigs(element, element.get('data-rsce-name'), newItem, element.get('data-rsce-name')); 514 | removeACEs(newItem); 515 | 516 | newItem.inject(element, 'after'); 517 | 518 | renameElement(newItem); 519 | restoreChosens(newItem); 520 | 521 | newItem.getAllNext('.rsce_list_item').each(function(el) { 522 | renameElement(el); 523 | restoreChosens(el); 524 | }); 525 | 526 | newItem.getElements('.rsce_list').each(function(el) { 527 | initList(el); 528 | }); 529 | 530 | newItem.getElements('.rsce_list_inner').each(function(el) { 531 | initListSort(el); 532 | }); 533 | 534 | newItem.getAllNext('.rsce_list_item').each(function(el) { 535 | restoreTinyMCEs(el); 536 | }); 537 | restoreTinyMCEs(newItem); 538 | restoreTinyMCEs(element); 539 | 540 | executeHtmlScripts(newItem.get('html')); 541 | 542 | removePicker(newItem); 543 | restorePicker(listInner); 544 | 545 | restoreRadioButtons(listInner); 546 | 547 | if (listInner.retrieve('listSort')) { 548 | listInner.retrieve('listSort').addItems(newItem); 549 | } 550 | else { 551 | initListSort(listInner); 552 | } 553 | 554 | updateListButtons(element.getParent('.rsce_list')); 555 | updateDependingFields(newItem); 556 | 557 | }; 558 | 559 | var deleteElement = function(linkElement) { 560 | 561 | var element = $(linkElement).getParent('.rsce_list_item'); 562 | var listElement = element.getParent('.rsce_list'); 563 | var listInner = element.getParent('.rsce_list_inner'); 564 | var allItems = listInner.getChildren('.rsce_list_item'); 565 | var nextElements = element.getAllNext('.rsce_list_item'); 566 | 567 | var config = listElement.retrieve('rsce_config', {}); 568 | 569 | if (config.minItems && allItems.length <= config.minItems) { 570 | return; 571 | } 572 | 573 | removePicker(listInner); 574 | preserveRadioButtons(listInner); 575 | 576 | removeTinyMCEs(element); 577 | if (listInner.retrieve('listSort')) { 578 | listInner.retrieve('listSort').removeItems(element); 579 | } 580 | element.destroy(); 581 | nextElements.each(function(nextElement) { 582 | removeTinyMCEs(nextElement); 583 | }); 584 | nextElements.each(function(nextElement) { 585 | renameElement(nextElement); 586 | }); 587 | nextElements.each(function(nextElement) { 588 | restoreChosens(nextElement); 589 | restoreTinyMCEs(nextElement); 590 | }); 591 | 592 | restorePicker(listInner); 593 | restoreRadioButtons(listInner); 594 | 595 | updateListButtons(listElement); 596 | 597 | $(document.body).getChildren('.tip-wrap').each(function(el) { 598 | el.dispose(); 599 | }); 600 | setTimeout(function() { 601 | $(document.body).getChildren('.tip-wrap').each(function(el) { 602 | el.dispose(); 603 | }); 604 | }, 1000); 605 | 606 | }; 607 | 608 | var moveElement = function(linkElement, offset) { 609 | 610 | var element = $(linkElement).getParent('.rsce_list_item'); 611 | var swapElement; 612 | if (offset > 0) { 613 | swapElement = element.getNext('.rsce_list_item'); 614 | } 615 | else if (offset < 0) { 616 | swapElement = element.getPrevious('.rsce_list_item'); 617 | } 618 | if (!swapElement) { 619 | return; 620 | } 621 | 622 | // The order is important to prevent id conflicts: 623 | // remove tinyMCEs => move the element => rename => restoring tinyMCEs 624 | 625 | removeTinyMCEs(swapElement); 626 | removeTinyMCEs(element); 627 | 628 | removePicker(swapElement); 629 | removePicker(element); 630 | 631 | preserveRadioButtons(swapElement); 632 | preserveRadioButtons(element); 633 | 634 | element.inject(swapElement, offset > 0 ? 'after' : 'before'); 635 | 636 | renameElement(swapElement); 637 | renameElement(element); 638 | 639 | restoreChosens(swapElement); 640 | restoreChosens(element); 641 | 642 | restorePicker(swapElement); 643 | restorePicker(element); 644 | 645 | restoreRadioButtons(swapElement); 646 | restoreRadioButtons(element); 647 | 648 | restoreTinyMCEs(swapElement); 649 | restoreTinyMCEs(element); 650 | 651 | updateListButtons(element.getParent('.rsce_list')); 652 | 653 | }; 654 | 655 | var removeListFormFields = function(form) { 656 | form.getElements('input[name="FORM_FIELDS[]"]').each(function(input) { 657 | input = $(input); 658 | if (input.getParent().hasClass('rsce_list_item')) { 659 | return; 660 | } 661 | input.set('value', input.get('value').replace( 662 | /(?:^|,)rsce_[^,;]+?__[^,;]+/gi, 663 | '' 664 | )); 665 | }); 666 | }; 667 | 668 | var initList = function(listElement) { 669 | 670 | listElement = $(listElement); 671 | 672 | if (listElement.get('id').indexOf('__rsce_dummy__') !== -1) { 673 | return; 674 | } 675 | 676 | if (listElement.getChildren('.rsce_list_inner').length) { 677 | // Already initialized 678 | return; 679 | } 680 | 681 | if (listElement.get('data-config')) { 682 | listElement.store( 683 | 'rsce_config', 684 | JSON.decode(listElement.get('data-config')) 685 | ); 686 | } 687 | 688 | var listInner = new Element('div', {'class': 'rsce_list_inner'}) 689 | .inject( 690 | listElement.getChildren('.rsce_list_item.rsce_list_item_dummy')[0], 691 | 'after' 692 | ); 693 | 694 | listElement.getChildren('.rsce_list_item').each(function(element) { 695 | 696 | if (element.hasClass('rsce_list_item_dummy')) { 697 | return; 698 | } 699 | 700 | var key = element.get('data-rsce-name'); 701 | var fields = []; 702 | 703 | element.getElements('[name^="' + key + '"]').each(function(input) { 704 | if ( 705 | input.getParent('.rsce_list_item') === element && 706 | input.get('name').indexOf('__rsce_dummy') === -1 707 | ) { 708 | fields.push(input.get('name').split('[')[0]); 709 | } 710 | }); 711 | 712 | element.grab(new Element('input', { 713 | type: 'hidden', 714 | name: 'FORM_FIELDS[]', 715 | value: fields.join(',') 716 | })); 717 | 718 | element.inject(listInner); 719 | 720 | }); 721 | 722 | var dummyFields = []; 723 | listElement.getElements('[name*="__rsce_dummy__"]').each(function(input) { 724 | if (input.required) { 725 | input.required = false; 726 | input.setProperty('data-rsce-required', 'data-rsce-required'); 727 | } 728 | dummyFields.push(input.get('name').split('[')[0]); 729 | }); 730 | 731 | var parentForm = listElement.getParent('form'); 732 | removeListFormFields(parentForm); 733 | window.addEvent('ajax_change', function () { 734 | removeListFormFields(parentForm); 735 | }); 736 | 737 | initListSort(listInner); 738 | 739 | updateListButtons(listElement); 740 | 741 | }; 742 | 743 | var allDependingWidgets = []; 744 | 745 | var removeDependingFormFields = function(widget) { 746 | var fieldsToRemove = []; 747 | widget.getElements('input[name],textarea[name],select[name],button[name]').each(function(input) { 748 | fieldsToRemove.push(input.name.split('[')[0]); 749 | input.disabled = true; 750 | input.setAttribute('data-disabled-by-rsce', 'true'); 751 | }); 752 | if (!fieldsToRemove.length) { 753 | return; 754 | } 755 | var fieldsRegEx = new RegExp(fieldsToRemove.map(function(fieldName) { 756 | return '(?:^|,)'+(fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))+'(?=,|;|$)'; 757 | }).join('|'), 'g'); 758 | widget.getParent('form').getElements('input[name="FORM_FIELDS[]"]').each(function(input) { 759 | $(input).set('value', $(input).get('value').replace(fieldsRegEx, '')); 760 | }); 761 | }; 762 | 763 | var restoreDependingFormFields = function(widget) { 764 | var inputsToRestore = []; 765 | widget.getElements('[data-disabled-by-rsce]').each(function(input) { 766 | if ($(input).getParent().getStyle('display') === 'none') { 767 | return; 768 | } 769 | inputsToRestore.push(input); 770 | input.disabled = false; 771 | input.removeAttribute('data-disabled-by-rsce'); 772 | }); 773 | if (!inputsToRestore.length) { 774 | return; 775 | } 776 | inputsToRestore.each(function(input) { 777 | var parent = input.getParent('.rsce_list_item') || input.getParent('.tl_formbody_edit') || input.form; 778 | if (!parent) { 779 | return; 780 | } 781 | var fieldsInput = parent.getChildren('input[name="FORM_FIELDS[]"]')[0] || parent.getElement('input[name="FORM_FIELDS[]"]'); 782 | if (!fieldsInput) { 783 | return; 784 | } 785 | fieldsInput.set('value', fieldsInput.get('value')+','+(input.name.split('[')[0])); 786 | }); 787 | }; 788 | 789 | var updateDependingFields = function(formElement) { 790 | 791 | formElement.getElements('[class*=rsce-depends-on-]').each(function(dependentInput) { 792 | var widget = 793 | ( 794 | dependentInput.hasClass('rsce_list') 795 | || dependentInput.hasClass('rsce_group') 796 | || dependentInput.hasClass('widget') 797 | ) 798 | ? dependentInput 799 | : dependentInput.getParent('div.widget'); 800 | 801 | if (!widget || allDependingWidgets.indexOf(widget) !== -1) { 802 | return; 803 | } 804 | 805 | var dependsOnData = JSON.parse(decodeURIComponent(dependentInput.className.match(/rsce-depends-on-\S+/)[0].substr(16))); 806 | if (!dependsOnData || !dependsOnData.field) { 807 | return; 808 | } 809 | 810 | var inputs = document.body.getElements('[name="'+dependsOnData.field+'"][type!=hidden],[name^="'+dependsOnData.field+'["][type!=hidden]'); 811 | 812 | if (!inputs.length) { 813 | inputs = document.body.getElements('[name="'+dependsOnData.field+'"][type=hidden],[name^="'+dependsOnData.field+'["][type=hidden]'); 814 | 815 | if (!inputs.length) { 816 | return; 817 | } 818 | } 819 | 820 | allDependingWidgets.push(widget); 821 | 822 | if (inputs[0].getPrevious('[type=checkbox][id^=check_all_]')) { 823 | inputs[0].getPrevious('[type=checkbox][id^=check_all_]').addEvent('click', updateWidget); 824 | } 825 | 826 | // Handle widgets replaced via AJAX (e.g. fileTree) 827 | if (inputs[0].form) { 828 | inputs[0].form.addEvent('change', function (event) { 829 | if (inputs[0] && inputs[0].name !== dependsOnData.field) { 830 | dependsOnData.field = inputs[0].name.replace(/\[]$/, ''); 831 | } 832 | if ((event.target.name || '').substr(0, dependsOnData.field.length) === dependsOnData.field) { 833 | inputs = document.body.getElements('[name="'+dependsOnData.field+'"][type!=hidden],[name^="'+dependsOnData.field+'["][type!=hidden]'); 834 | if (!inputs.length) { 835 | inputs = document.body.getElements('[name="'+dependsOnData.field+'"][type=hidden],[name^="'+dependsOnData.field+'["][type=hidden]'); 836 | } 837 | updateWidget(); 838 | } 839 | }); 840 | } 841 | 842 | inputs.addEvent('input', updateWidget); 843 | inputs.addEvent('change', updateWidget); 844 | inputs.addEvent('click', updateWidget); 845 | updateWidget(); 846 | 847 | function updateWidget() { 848 | var input = inputs[0]; 849 | inputs.each(function(el) { 850 | if (el.checked) { 851 | input = el; 852 | } 853 | }); 854 | 855 | var value = input.get('value'); 856 | 857 | if (input.type === 'checkbox' && !input.checked) { 858 | value = ''; 859 | } 860 | 861 | if (input.name && input.form && new FormData(input.form).getAll(input.name).length) { 862 | value = new FormData(input.form).getAll(input.name).pop(); 863 | } 864 | 865 | if (input.type === 'checkbox' && input.name.substr(-2) === '[]') { 866 | value = []; 867 | inputs.each(function(el) { 868 | if (el.checked) { 869 | value.push(el.value); 870 | } 871 | }); 872 | } 873 | 874 | if (valueMatches(dependsOnData.value, value) && !input.disabled) { 875 | widget.style.display = 'block'; 876 | restoreDependingFormFields(widget); 877 | } 878 | else { 879 | widget.style.display = 'none'; 880 | removeDependingFormFields(widget); 881 | } 882 | 883 | // Handle nested depending widgets, e.g. groups 884 | widget.getElements('input,textarea,select').each(function(input) { 885 | input.fireEvent('change'); 886 | }); 887 | 888 | function valueMatches(dependingValue, actualValue) { 889 | if (Array.isArray(dependingValue)) { 890 | for (var i = 0; i < dependingValue.length; i++) { 891 | if (valueMatches(dependingValue[i], actualValue)) { 892 | return true; 893 | } 894 | } 895 | return false; 896 | } 897 | 898 | if (Array.isArray(actualValue)) { 899 | for (var i = 0; i < actualValue.length; i++) { 900 | if (valueMatches(dependingValue, actualValue[i])) { 901 | return true; 902 | } 903 | } 904 | return false; 905 | } 906 | 907 | if (dependingValue === true) { 908 | return !!actualValue; 909 | } 910 | 911 | if (dependingValue === false) { 912 | return !actualValue; 913 | } 914 | 915 | return dependingValue === actualValue; 916 | } 917 | } 918 | 919 | }); 920 | 921 | }; 922 | 923 | var init = function(formElement) { 924 | updateDependingFields($(formElement)); 925 | window.addEvent('domready', function() { 926 | updateDependingFields($(formElement)); 927 | }); 928 | }; 929 | 930 | // public objects 931 | window.rsceNewElement = newElement; 932 | window.rsceNewElementAfter = newElementAfter; 933 | window.rsceDuplicateElement = duplicateElement; 934 | window.rsceDeleteElement = deleteElement; 935 | window.rsceMoveElement = moveElement; 936 | window.rsceInitList = initList; 937 | window.rsceInit = init; 938 | 939 | })(document.id, window); 940 | -------------------------------------------------------------------------------- /src/CustomElements.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace MadeYourDay\RockSolidCustomElements; 10 | 11 | use Contao\Backend; 12 | use Contao\BackendUser; 13 | use Contao\Config; 14 | use Contao\Controller; 15 | use Contao\CoreBundle\ContaoCoreBundle; 16 | use Contao\CoreBundle\Monolog\ContaoContext; 17 | use Contao\Database; 18 | use Contao\DataContainer; 19 | use Contao\FilesModel; 20 | use Contao\Input; 21 | use Contao\ModuleModel; 22 | use Contao\StringUtil; 23 | use Contao\System; 24 | use Doctrine\DBAL\DBALException; 25 | use MadeYourDay\RockSolidCustomElements\Template\CustomTemplate; 26 | use Psr\Log\LogLevel; 27 | use Symfony\Component\Filesystem\Filesystem; 28 | use Symfony\Component\HttpFoundation\Request; 29 | 30 | /** 31 | * RockSolid Custom Elements DCA (tl_content, tl_module and tl_form_field) 32 | * 33 | * Provide miscellaneous methods that are used by the data configuration arrays. 34 | * 35 | * @author Martin Auswöger 36 | * @todo Create cache files with different names to be able to drop the 37 | * refreshOpcodeCache method 38 | */ 39 | class CustomElements 40 | { 41 | /** 42 | * @var array Currently loaded data 43 | */ 44 | protected $data = array(); 45 | 46 | /** 47 | * @var array Data prepared for saving 48 | */ 49 | protected $saveData = array(); 50 | 51 | /** 52 | * @var array Fields configuration 53 | */ 54 | protected $fieldsConfig = array(); 55 | 56 | /** 57 | * tl_content, tl_module and tl_form_field DCA onload callback 58 | * 59 | * Reloads config and creates the DCA fields 60 | * 61 | * @param DataContainer $dc Data container 62 | * @return void 63 | */ 64 | public function onloadCallback($dc) 65 | { 66 | if (Input::get('act') === 'create') { 67 | return; 68 | } 69 | 70 | if (Input::get('act') === 'edit') { 71 | $this->reloadConfig(); 72 | } 73 | 74 | if ($dc->table === 'tl_content' && class_exists('CeAccess')) { 75 | $ceAccess = new \CeAccess; 76 | $ceAccess->filterContentElements($dc); 77 | } 78 | 79 | if (Input::get('act') === 'editAll') { 80 | return $this->createDcaMultiEdit($dc); 81 | } 82 | 83 | $type = $this->getDcaFieldValue($dc, 'type'); 84 | if (!$type || substr($type, 0, 5) !== 'rsce_') { 85 | return; 86 | } 87 | 88 | $data = $this->getDcaFieldValue($dc, 'rsce_data', true); 89 | if ($data && substr($data, 0, 1) === '{') { 90 | $this->data = json_decode($data, true); 91 | } 92 | 93 | $createFromPost = Input::post('FORM_SUBMIT') === $dc->table; 94 | $tmpField = null; 95 | 96 | if (Input::get('field') && substr(Input::get('field'), 0, 11) === 'rsce_field_') { 97 | // Ensures that the fileTree oder pageTree field exists 98 | $tmpField = Input::get('field'); 99 | } 100 | elseif ( 101 | Input::get('target') 102 | && ($target = explode('.', Input::get('target'), 3)) 103 | && $target[0] === $dc->table 104 | && substr($target[1], 0, 11) === 'rsce_field_' 105 | ) { 106 | // Ensures that the fileTree oder pageTree field exists 107 | $tmpField = $target[1]; 108 | } 109 | elseif (Input::post('name') && substr(Input::post('name'), 0, 11) === 'rsce_field_') { 110 | // Ensures that the fileTree oder pageTree field exists 111 | $tmpField = Input::post('name'); 112 | } 113 | 114 | $this->createDca($dc, $type, $createFromPost, $tmpField); 115 | } 116 | 117 | /** 118 | * tl_content, tl_module and tl_form_field DCA onsubmit callback 119 | * 120 | * Creates empty arrays for empty lists if no data is available 121 | * (e.g. for new elements) 122 | * 123 | * @param DataContainer $dc Data container 124 | * @return void 125 | */ 126 | public function onsubmitCallback($dc) 127 | { 128 | $type = $this->getDcaFieldValue($dc, 'type'); 129 | if (!$type || substr($type, 0, 5) !== 'rsce_') { 130 | return; 131 | } 132 | 133 | $data = $this->getDcaFieldValue($dc, 'rsce_data', true); 134 | 135 | // Check if it is a new element with no data 136 | if ($data === null && !count($this->saveData)) { 137 | 138 | // Creates empty arrays for empty lists, see #4 139 | $data = $this->saveDataCallback(null, $dc); 140 | 141 | if ($data && substr($data, 0, 1) === '{') { 142 | Database::getInstance() 143 | ->prepare("UPDATE {$dc->table} SET rsce_data = ? WHERE id = ?") 144 | ->execute($data, $dc->id); 145 | } 146 | 147 | } 148 | } 149 | 150 | /** 151 | * tl_content, tl_module and tl_form_field DCA onshow callback 152 | * 153 | * @param array $data 154 | * @param array $row 155 | * @param DataContainer $dc Data container 156 | * 157 | * @return array 158 | */ 159 | public static function onshowCallback($data, $row, $dc) 160 | { 161 | if ( 162 | empty($row['type']) 163 | || substr($row['type'], 0, 5) !== 'rsce_' 164 | || !$data 165 | || empty($row['rsce_data']) 166 | || $row['rsce_data'] === '{}' 167 | || !$rsceData = json_decode($row['rsce_data'], true) 168 | ) { 169 | return $data; 170 | } 171 | 172 | $config = static::getConfigByType($row['type']); 173 | 174 | foreach ($data as $table => $rows) { 175 | foreach ($rows as $rowIndex => $rowValues) { 176 | foreach ($rowValues as $label => $value) { 177 | if (substr($label, -24) === 'rsce_data') { 178 | unset($data[$table][$rowIndex][$label]); 179 | } 180 | } 181 | } 182 | } 183 | 184 | if (!isset($data['tl_content.rsce_data'])) { 185 | $data = array_merge(['tl_content.rsce_data' => []], $data); 186 | } 187 | 188 | $data['tl_content.rsce_data'][] = self::formatShowData($rsceData, $config['fields'] ?? []); 189 | 190 | return $data; 191 | } 192 | 193 | private static function formatShowData(array $rsceData, array $fieldsConfig, string $prefix = '', string $keyPrefix = ''): array 194 | { 195 | $data = []; 196 | foreach ($rsceData as $key => $value) { 197 | if (is_array($value)) { 198 | foreach ($value as $valueIndex => $valueRow) { 199 | $prefixLabel = static::getLabelTranslated($fieldsConfig[$key]['elementLabel'] ?? null) ?: $key . ' %s'; 200 | $data = array_merge( 201 | $data, 202 | self::formatShowData( 203 | $valueRow, 204 | $fieldsConfig[$key]['fields'] ?? [], 205 | $prefix . sprintf($prefixLabel, $valueIndex + 1) . ' › ', 206 | $keyPrefix . $key . '[' . $valueIndex . ']->' 207 | ) 208 | ); 209 | } 210 | } 211 | else { 212 | $label = $prefix; 213 | $label .= (static::getLabelTranslated($fieldsConfig[$key]['label'] ?? null) ?: [$key])[0]; 214 | $label .= '' . $keyPrefix . $key . ''; 215 | $value = StringUtil::deserialize($value); 216 | if (is_array($value)) { 217 | $value = self::formatShowArray($value); 218 | } 219 | $data[$label] = (string) $value; 220 | } 221 | } 222 | 223 | return $data; 224 | } 225 | 226 | private static function formatShowArray(array $data): string 227 | { 228 | $data = array_map(function($value) { 229 | if (is_array($value)) { 230 | return '(' . self::formatShowArray($value) . ')'; 231 | } 232 | return (string) $value; 233 | }, $data); 234 | 235 | if ($data && array_keys($data) !== range(0, count($data) - 1)) { 236 | $data = array_map(function($key, $value) { 237 | return $key.': '.$value; 238 | }, array_keys($data), $data); 239 | } 240 | 241 | return implode(', ', $data); 242 | } 243 | 244 | /** 245 | * Field load callback 246 | * 247 | * Finds the current value for the field 248 | * 249 | * @param string $value Current value 250 | * @param DataContainer $dc Data container 251 | * @return string Current value for the field 252 | */ 253 | public function loadCallback($value, $dc) 254 | { 255 | if ($value !== null) { 256 | return $value; 257 | } 258 | 259 | $value = $this->getNestedValue($dc->field); 260 | 261 | if ($value === null && isset($GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['default'])) { 262 | $value = $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['default']; 263 | } 264 | 265 | if ($value && ( 266 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fileTree' 267 | || $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fineUploader' 268 | )) { 269 | $value = $this->uuidTextToBin($value); 270 | } 271 | 272 | return $value; 273 | } 274 | 275 | /** 276 | * Field load callback multi edit 277 | * 278 | * Finds the current value for the field directly from the database 279 | * 280 | * @param string $value Current value 281 | * @param DataContainer $dc Data container 282 | * @return string Current value for the field 283 | */ 284 | public function loadCallbackMultiEdit($value, $dc) 285 | { 286 | if ($value !== null) { 287 | return $value; 288 | } 289 | 290 | $field = substr($dc->field, strlen($dc->activeRecord->type . '_field_')); 291 | 292 | $data = Database::getInstance() 293 | ->prepare("SELECT rsce_data FROM {$dc->table} WHERE id=?") 294 | ->execute($dc->id) 295 | ->rsce_data; 296 | $data = $data ? json_decode($data, true) : []; 297 | 298 | if (isset($data[$field])) { 299 | $value = $data[$field]; 300 | } 301 | 302 | if ($value === null && isset($GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['default'])) { 303 | $value = $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['default']; 304 | } 305 | 306 | if ($value && ( 307 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fileTree' 308 | || $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fineUploader' 309 | )) { 310 | $value = $this->uuidTextToBin($value); 311 | } 312 | 313 | return $value; 314 | } 315 | 316 | /** 317 | * Get the value of the nested data array $this->data from field name 318 | * 319 | * @param string $field Field name 320 | * @param bool $fromSaveData True to retrieve the value from $this->saveData instead of $this->data 321 | * @return mixed Value from $this->data or $this->saveData 322 | */ 323 | protected function getNestedValue($field, $fromSaveData = false) 324 | { 325 | $field = preg_split('(__([0-9]+)__)', substr($field, 11), -1, PREG_SPLIT_DELIM_CAPTURE); 326 | 327 | if ($fromSaveData) { 328 | if (!isset($this->saveData[$field[0]])) { 329 | return null; 330 | } 331 | $data =& $this->saveData[$field[0]]; 332 | } 333 | else { 334 | if (!isset($this->data[$field[0]])) { 335 | return null; 336 | } 337 | $data =& $this->data[$field[0]]; 338 | } 339 | 340 | for ($i = 0; isset($field[$i]); $i += 2) { 341 | 342 | if (isset($field[$i + 1])) { 343 | if (!isset($data[$field[$i + 1]])) { 344 | return null; 345 | } 346 | if (!isset($data[$field[$i + 1]][$field[$i + 2]])) { 347 | return null; 348 | } 349 | $data =& $data[$field[$i + 1]][$field[$i + 2]]; 350 | } 351 | else { 352 | return $data; 353 | } 354 | 355 | } 356 | } 357 | 358 | /** 359 | * Get the reference to a value of the nested data array $this->saveData from field name 360 | * 361 | * @param string $field Field name 362 | * @return mixed Value from $this->saveData as reference 363 | */ 364 | protected function &getNestedValueReference($field) 365 | { 366 | $field = preg_split('(__([0-9]+)__)', substr($field, 11), -1, PREG_SPLIT_DELIM_CAPTURE); 367 | 368 | if (!isset($this->saveData[$field[0]])) { 369 | $this->saveData[$field[0]] = array(); 370 | } 371 | 372 | $data =& $this->saveData[$field[0]]; 373 | 374 | for ($i = 0; isset($field[$i]); $i += 2) { 375 | 376 | if (isset($field[$i + 1])) { 377 | if (!isset($data[$field[$i + 1]])) { 378 | $data[$field[$i + 1]] = array(); 379 | } 380 | if (!isset($data[$field[$i + 1]][$field[$i + 2]])) { 381 | $data[$field[$i + 1]][$field[$i + 2]] = array(); 382 | } 383 | $data =& $data[$field[$i + 1]][$field[$i + 2]]; 384 | } 385 | else { 386 | return $data; 387 | } 388 | 389 | } 390 | } 391 | 392 | /** 393 | * Unset a value of the nested data array $this->saveData from field name 394 | * 395 | * @param string $field Field name 396 | * @return void 397 | */ 398 | protected function unsetNestedValue($field) 399 | { 400 | $field = preg_split('(__([0-9]+)__)', substr($field, 11), -1, PREG_SPLIT_DELIM_CAPTURE); 401 | 402 | if (!isset($this->saveData[$field[0]])) { 403 | return; 404 | } 405 | 406 | if (\count($field) === 1) { 407 | unset($this->saveData[$field[0]]); 408 | 409 | return; 410 | } 411 | 412 | $data =& $this->saveData[$field[0]]; 413 | 414 | for ($i = 0; isset($field[$i + 1]); $i += 2) { 415 | if (!isset($data[$field[$i + 1]])) { 416 | return; 417 | } 418 | if (!isset($data[$field[$i + 1]][$field[$i + 2]])) { 419 | return; 420 | } 421 | if (isset($field[$i + 3])) { 422 | $data =& $data[$field[$i + 1]][$field[$i + 2]]; 423 | } 424 | else { 425 | unset($data[$field[$i + 1]][$field[$i + 2]]); 426 | return; 427 | } 428 | } 429 | } 430 | 431 | /** 432 | * Get the config from field name 433 | * 434 | * @param string $field Field name 435 | * @return mixed Configuration of the field 436 | */ 437 | protected function getNestedConfig($field, $config) 438 | { 439 | $field = preg_split('(__([0-9]+)__)', substr($field, 11), -1, PREG_SPLIT_DELIM_CAPTURE); 440 | 441 | if (!isset($config[$field[0]])) { 442 | return null; 443 | } 444 | 445 | $fieldConfig =& $config[$field[0]]; 446 | 447 | for ($i = 0; isset($field[$i]); $i += 2) { 448 | 449 | if (isset($field[$i + 1])) { 450 | if (!isset($fieldConfig['fields'])) { 451 | return null; 452 | } 453 | if (!isset($fieldConfig['fields'][$field[$i + 2]])) { 454 | return null; 455 | } 456 | $fieldConfig =& $fieldConfig['fields'][$field[$i + 2]]; 457 | } 458 | else { 459 | return $fieldConfig; 460 | } 461 | 462 | } 463 | } 464 | 465 | /** 466 | * Field save callback 467 | * 468 | * Saves the field data to $this->saveData 469 | * 470 | * @param string $value Field value 471 | * @param DataContainer $dc Data container 472 | * @return void 473 | */ 474 | public function saveCallback($value, $dc) 475 | { 476 | if (strpos($dc->field, '__rsce_dummy__') !== false) { 477 | return; 478 | } 479 | 480 | if ( 481 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fileTree' 482 | || $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fineUploader' 483 | ) { 484 | $value = $this->uuidBinToText($value); 485 | } 486 | 487 | $field = preg_split('(__([0-9]+)__)', substr($dc->field, 11), -1, PREG_SPLIT_DELIM_CAPTURE); 488 | 489 | $data =& $this->saveData[$field[0]]; 490 | 491 | for ($i = 0; isset($field[$i]); $i += 2) { 492 | 493 | if (isset($field[$i + 1])) { 494 | if (!isset($data[$field[$i + 1]])) { 495 | $data[$field[$i + 1]] = array(); 496 | } 497 | if (!isset($data[$field[$i + 1]][$field[$i + 2]])) { 498 | if ($field[$i + 2] === 'rsce_empty' && !isset($field[$i + 3]) && !$value) { 499 | // do not save the empty field 500 | break; 501 | } 502 | $data[$field[$i + 1]][$field[$i + 2]] = array(); 503 | } 504 | $data =& $data[$field[$i + 1]][$field[$i + 2]]; 505 | } 506 | else { 507 | $data = $value; 508 | } 509 | } 510 | 511 | return ''; 512 | } 513 | 514 | /** 515 | * Field save callback multi edit 516 | * 517 | * Saves the field data directly into the database field rsce_data 518 | * 519 | * @param string $value Field value 520 | * @param DataContainer $dc Data container 521 | * @return void 522 | */ 523 | public function saveCallbackMultiEdit($value, $dc) 524 | { 525 | if ( 526 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fileTree' 527 | || $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['inputType'] === 'fineUploader' 528 | ) { 529 | $value = $this->uuidBinToText($value); 530 | } 531 | 532 | $field = substr($dc->field, strlen($dc->activeRecord->type . '_field_')); 533 | 534 | $data = Database::getInstance() 535 | ->prepare("SELECT rsce_data FROM {$dc->table} WHERE id=?") 536 | ->execute($dc->id) 537 | ->rsce_data; 538 | $data = $data ? json_decode($data, true) : []; 539 | 540 | $data[$field] = $value; 541 | 542 | $data = json_encode($data); 543 | 544 | Database::getInstance() 545 | ->prepare("UPDATE {$dc->table} SET rsce_data = ? WHERE id = ?") 546 | ->execute($data, $dc->id); 547 | 548 | return ''; 549 | } 550 | 551 | /** 552 | * rsce_data field save callback 553 | * 554 | * Returns the JSON encoded $this->saveData 555 | * 556 | * @param string $value Current field value 557 | * @param DataContainer $dc Data container 558 | * @return string JSON encoded $this->saveData 559 | */ 560 | public function saveDataCallback($value, $dc) 561 | { 562 | $this->prepareSaveData('rsce_field_', $this->fieldsConfig, $dc); 563 | 564 | $data = json_encode($this->saveData); 565 | 566 | if ($data === '[]') { 567 | $data = '{}'; 568 | } 569 | 570 | return $data; 571 | } 572 | 573 | /** 574 | * prepare the data to save and create empty arrays for empty lists 575 | * 576 | * @param string $fieldPrefix field prefix 577 | * @param array $fieldsConfig fields configuration 578 | * @param DataContainer $dc Data container 579 | * @return void 580 | */ 581 | protected function prepareSaveData($fieldPrefix, $fieldsConfig, $dc) 582 | { 583 | foreach ($fieldsConfig as $fieldName => $fieldConfig) { 584 | 585 | if (!empty($fieldConfig['dependsOn'])) { 586 | if (\is_string($fieldConfig['dependsOn'])) { 587 | $fieldConfig['dependsOn'] = ['field' => $fieldConfig['dependsOn']]; 588 | } 589 | if (\is_array($fieldConfig['dependsOn'])) { 590 | $dependingFieldName = $this->getDependingFieldName($fieldConfig['dependsOn'], $fieldPrefix); 591 | if (substr($dependingFieldName, 0, 11) === 'rsce_field_') { 592 | $actualValue = $this->getNestedValue($dependingFieldName, true); 593 | } 594 | else { 595 | $actualValue = $dc->activeRecord ? $dc->activeRecord->$dependingFieldName : null; 596 | } 597 | if (!$this->dependingValueMatches($fieldConfig['dependsOn']['value'] ?? true, StringUtil::deserialize($actualValue))) { 598 | $this->unsetNestedValue($fieldPrefix . $fieldName); 599 | } 600 | } 601 | } 602 | 603 | if (isset($fieldConfig['inputType']) && $fieldConfig['inputType'] === 'list') { 604 | 605 | // creates an empty array for a empty lists 606 | $fieldData = $this->getNestedValueReference($fieldPrefix . $fieldName); 607 | 608 | for ($dataKey = 0; isset($fieldData[$dataKey]); $dataKey++) { 609 | $this->prepareSaveData($fieldPrefix . $fieldName . '__' . $dataKey . '__', $fieldConfig['fields'], $dc); 610 | } 611 | 612 | } 613 | 614 | } 615 | } 616 | 617 | /** 618 | * Create all DCA fields for the specified type 619 | * 620 | * @param DataContainer $dc Data container 621 | * @param string $type The template name 622 | * @param boolean $createFromPost Whether to create the field structure from post data or not 623 | * @param string $tmpField Field name to create temporarily for page or file tree widget ajax calls 624 | * @return void 625 | */ 626 | protected function createDca($dc, $type, $createFromPost = false, $tmpField = null) 627 | { 628 | $config = static::getConfigByType($type); 629 | 630 | if (!$config) { 631 | return; 632 | } 633 | 634 | $paletteFields = array(); 635 | $standardFields = is_array($config['standardFields'] ?? null) ? $config['standardFields'] : array(); 636 | $this->fieldsConfig = $config['fields']; 637 | 638 | foreach ($this->fieldsConfig as $fieldName => $fieldConfig) { 639 | $this->createDcaItem('rsce_field_', $fieldName, $fieldConfig, $paletteFields, $dc, $createFromPost); 640 | } 641 | if ($tmpField && !in_array($tmpField, $paletteFields)) { 642 | $fieldConfig = $this->getNestedConfig($tmpField, $this->fieldsConfig); 643 | if ($fieldConfig) { 644 | $this->createDcaItem($tmpField, '', $fieldConfig, $paletteFields, $dc, false); 645 | } 646 | } 647 | 648 | $GLOBALS['TL_DCA'][$dc->table]['fields']['rsce_data']['eval']['rsceScript'] = 'window.rsceInit(document.currentScript.closest(".tl_formbody_edit"));'; 649 | 650 | $paletteFields[] = 'rsce_data'; 651 | 652 | $GLOBALS['TL_DCA'][$dc->table]['palettes'][$type] = static::generatePalette( 653 | $dc->table, 654 | $paletteFields, 655 | $standardFields 656 | ); 657 | 658 | $GLOBALS['TL_DCA'][$dc->table]['fields']['customTpl']['options_callback'] = function($dc) { 659 | $templates = Controller::getTemplateGroup($dc->activeRecord->type.'_', [], $dc->activeRecord->type); 660 | foreach ($templates as $key => $label) { 661 | if (substr($key, -7) === '_config' || $key === $dc->activeRecord->type) { 662 | unset($templates[$key]); 663 | } 664 | } 665 | return $templates; 666 | }; 667 | 668 | $GLOBALS['TL_LANG'][$dc->table]['rsce_legend'] = $GLOBALS['TL_LANG'][$dc->table === 'tl_content' ? 'CTE' : ($dc->table === 'tl_module' ? 'FMD' : 'FFL')][$type][0]; 669 | 670 | if (!empty($config['onloadCallback']) && is_array($config['onloadCallback'])) { 671 | foreach ($config['onloadCallback'] as $callback) { 672 | if (is_array($callback)) { 673 | System::importStatic($callback[0])->{$callback[1]}($dc); 674 | } 675 | else if (is_callable($callback)) { 676 | $callback($dc); 677 | } 678 | } 679 | } 680 | } 681 | 682 | /** 683 | * Create one DCA field with the specified parameters 684 | * 685 | * This function calls itself recursively for nested data structures 686 | * 687 | * @param string $fieldPrefix Field prefix, e.g. "rsce_field_" 688 | * @param string $fieldName Field name 689 | * @param array $fieldConfig Field configuration array 690 | * @param array $paletteFields Reference to the list of all fields 691 | * @param DataContainer $dc Data container 692 | * @param boolean $createFromPost Whether to create the field structure from post data or not 693 | * @param boolean $multiEdit Whether to create the field for the multi edit view 694 | * @return void 695 | */ 696 | protected function createDcaItem($fieldPrefix, $fieldName, $fieldConfig, &$paletteFields, $dc, $createFromPost, $multiEdit = false) 697 | { 698 | if (!is_string($fieldConfig) && !is_array($fieldConfig)) { 699 | throw new \Exception('Field config must be of type array or string.'); 700 | } 701 | if (strpos($fieldName, '__') !== false) { 702 | throw new \Exception('Field name must not include "__" (' . $this->getDcaFieldValue($dc, 'type') . ': ' . $fieldName . ').'); 703 | } 704 | if (strpos($fieldName, 'rsce_field_') !== false) { 705 | throw new \Exception('Field name must not include "rsce_field_" (' . $this->getDcaFieldValue($dc, 'type') . ': ' . $fieldName . ').'); 706 | } 707 | if (substr($fieldName, 0, 1) === '_' || substr($fieldName, -1) === '_') { 708 | throw new \Exception('Field name must not start or end with "_" (' . $this->getDcaFieldValue($dc, 'type') . ': ' . $fieldName . ').'); 709 | } 710 | 711 | if (!is_string($fieldName)) { 712 | $fieldName = 'unnamed_' . $fieldName; 713 | } 714 | 715 | if (is_string($fieldConfig)) { 716 | $fieldConfig = array( 717 | 'inputType' => 'group', 718 | 'label' => array($fieldConfig, ''), 719 | ); 720 | } 721 | 722 | if ( 723 | !BackendUser::getInstance()->hasAccess($dc->table . '::rsce_data', 'alexf') 724 | && $fieldConfig['inputType'] !== 'standardField' 725 | ) { 726 | return; 727 | } 728 | 729 | if (isset($fieldConfig['label'])) { 730 | $translatedLabel = static::getLabelTranslated($fieldConfig['label']); 731 | // Don’t overwrite referenced variable 732 | unset($fieldConfig['label']); 733 | $fieldConfig['label'] = $translatedLabel; 734 | } 735 | 736 | if ( 737 | isset($fieldConfig['reference']) 738 | && is_array($fieldConfig['reference']) 739 | && count(array_filter($fieldConfig['reference'], 'is_array')) 740 | ) { 741 | $translatedReference = array_map(function($label) { 742 | return \MadeYourDay\RockSolidCustomElements\CustomElements::getLabelTranslated($label); 743 | }, $fieldConfig['reference']); 744 | // Don’t overwrite referenced variable 745 | unset($fieldConfig['reference']); 746 | $fieldConfig['reference'] = $translatedReference; 747 | } 748 | 749 | if (isset($fieldConfig['dependsOn'])) { 750 | if (\is_string($fieldConfig['dependsOn'])) { 751 | $fieldConfig['dependsOn'] = ['field' => $fieldConfig['dependsOn']]; 752 | } 753 | if (\is_array($fieldConfig['dependsOn'])) { 754 | if (empty($fieldConfig['eval']['tl_class'])) { 755 | $fieldConfig['eval']['tl_class'] = ''; 756 | } 757 | $fieldConfig['eval']['tl_class'] .= ' rsce-depends-on-'.rawurlencode(json_encode([ 758 | 'field' => $this->getDependingFieldName($fieldConfig['dependsOn'], $fieldPrefix), 759 | 'value' => $fieldConfig['dependsOn']['value'] ?? true, 760 | ])); 761 | } 762 | } 763 | 764 | if ($fieldConfig['inputType'] === 'list') { 765 | 766 | if (isset($fieldConfig['elementLabel'])) { 767 | $translatedLabel = static::getLabelTranslated($fieldConfig['elementLabel']); 768 | // Don’t overwrite referenced variable 769 | unset($fieldConfig['elementLabel']); 770 | $fieldConfig['elementLabel'] = $translatedLabel; 771 | } else { 772 | $fieldConfig['elementLabel'] = "%s"; 773 | } 774 | 775 | $fieldConfig['minItems'] = isset($fieldConfig['minItems']) ? (int)$fieldConfig['minItems'] : 0; 776 | $fieldConfig['maxItems'] = isset($fieldConfig['maxItems']) ? (int)$fieldConfig['maxItems'] : null; 777 | 778 | if ($fieldConfig['maxItems'] && $fieldConfig['maxItems'] < $fieldConfig['minItems']) { 779 | throw new \Exception('maxItems must not be higher than minItems (' . $this->getDcaFieldValue($dc, 'type') . ': ' . $fieldName . ').'); 780 | } 781 | 782 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '_rsce_list_start'] = array( 783 | 'exclude' => false, 784 | 'label' => $fieldConfig['label'], 785 | 'inputType' => 'rsce_list_start', 786 | 'eval' => array_merge(isset($fieldConfig['eval']) ? $fieldConfig['eval'] : array(), array( 787 | 'minItems' => $fieldConfig['minItems'], 788 | 'maxItems' => $fieldConfig['maxItems'], 789 | )), 790 | ); 791 | $paletteFields[] = $fieldPrefix . $fieldName . '_rsce_list_start'; 792 | 793 | $hasFields = false; 794 | foreach ($fieldConfig['fields'] as $fieldConfig2) { 795 | if (isset($fieldConfig2['inputType']) && $fieldConfig2['inputType'] !== 'list') { 796 | $hasFields = true; 797 | } 798 | } 799 | if (!$hasFields) { 800 | // add an empty field 801 | $fieldConfig['fields']['rsce_empty'] = array( 802 | 'inputType' => 'text', 803 | 'eval' => array('tl_class' => 'hidden'), 804 | ); 805 | } 806 | 807 | $this->createDcaItemListDummy($fieldPrefix, $fieldName, $fieldConfig, $paletteFields, $dc, $createFromPost); 808 | 809 | $fieldData = $this->getNestedValue($fieldPrefix . $fieldName); 810 | 811 | for ( 812 | $dataKey = 0; 813 | $dataKey < $fieldConfig['minItems'] || ($createFromPost ? $this->wasListFieldSubmitted($fieldPrefix . $fieldName, $dataKey) : isset($fieldData[$dataKey])); 814 | $dataKey++ 815 | ) { 816 | 817 | if (is_int($fieldConfig['maxItems']) && $dataKey > $fieldConfig['maxItems'] - 1) { 818 | break; 819 | } 820 | 821 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_start'] = array( 822 | 'exclude' => false, 823 | 'inputType' => 'rsce_list_item_start', 824 | 'label' => array(sprintf($fieldConfig['elementLabel'], $dataKey + 1)), 825 | 'eval' => array( 826 | 'label_template' => $fieldConfig['elementLabel'], 827 | ), 828 | ); 829 | $paletteFields[] = $fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_start'; 830 | 831 | foreach ($fieldConfig['fields'] as $fieldName2 => $fieldConfig2) { 832 | $this->createDcaItem($fieldPrefix . $fieldName . '__' . $dataKey . '__', $fieldName2, $fieldConfig2, $paletteFields, $dc, $createFromPost); 833 | } 834 | 835 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_stop'] = array( 836 | 'exclude' => false, 837 | 'inputType' => 'rsce_list_item_stop', 838 | ); 839 | $paletteFields[] = $fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_stop'; 840 | 841 | } 842 | 843 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '_rsce_list_stop'] = array( 844 | 'exclude' => false, 845 | 'inputType' => 'rsce_list_stop', 846 | ); 847 | $paletteFields[] = $fieldPrefix . $fieldName . '_rsce_list_stop'; 848 | 849 | } 850 | else if ($fieldConfig['inputType'] === 'standardField') { 851 | 852 | if (strpos($fieldPrefix, '__') !== false) { 853 | throw new \Exception('Input type "standardField" is not allowed inside lists.'); 854 | } 855 | 856 | if (isset($GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName])) { 857 | 858 | if ( 859 | isset($GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName]['eval']) 860 | && is_array($GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName]['eval']) 861 | && isset($fieldConfig['eval']) 862 | && is_array($fieldConfig['eval']) 863 | ) { 864 | $fieldConfig['eval'] = array_merge( 865 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName]['eval'], 866 | $fieldConfig['eval'] 867 | ); 868 | } 869 | 870 | unset($fieldConfig['inputType']); 871 | 872 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName] = array_merge( 873 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldName], 874 | $fieldConfig 875 | ); 876 | 877 | $paletteFields[] = $fieldName; 878 | 879 | } 880 | 881 | } 882 | else if ($fieldConfig['inputType'] === 'group') { 883 | 884 | $fieldConfig['exclude'] = false; 885 | $fieldConfig['inputType'] = 'rsce_group_start'; 886 | 887 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName] = $fieldConfig; 888 | $paletteFields[] = $fieldPrefix . $fieldName; 889 | 890 | } 891 | else { 892 | 893 | if ($fieldConfig['inputType'] === 'url') { 894 | $fieldConfig['inputType'] = 'text'; 895 | $fieldConfig['eval'] = array_merge(array( 896 | 'rgxp' => 'url', 897 | 'decodeEntities' => true, 898 | 'dcaPicker' => true, 899 | 'addWizardClass' => false, 900 | ), isset($fieldConfig['eval']) ? $fieldConfig['eval'] : []); 901 | } 902 | 903 | if ($fieldConfig['inputType'] === 'pageTree' || $fieldConfig['inputType'] === 'fileTree') { 904 | if (!isset($fieldConfig['eval']['fieldType'])) { 905 | $fieldConfig['eval']['fieldType'] = 'radio'; 906 | } 907 | } 908 | 909 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName] = $fieldConfig; 910 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['exclude'] = false; 911 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['eval']['alwaysSave'] = true; 912 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['eval']['doNotSaveEmpty'] = true; 913 | if (!isset($GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['load_callback'])) { 914 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['load_callback'] = array(); 915 | } 916 | array_unshift( 917 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['load_callback'], 918 | array('MadeYourDay\\RockSolidCustomElements\\CustomElements', $multiEdit ? 'loadCallbackMultiEdit' : 'loadCallback') 919 | ); 920 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName]['save_callback'][] = 921 | array('MadeYourDay\\RockSolidCustomElements\\CustomElements', $multiEdit ? 'saveCallbackMultiEdit' : 'saveCallback'); 922 | 923 | $paletteFields[] = $fieldPrefix . $fieldName; 924 | 925 | } 926 | } 927 | 928 | /** 929 | * Get depending field name from dependsOn config resoving relative ../ parts 930 | * 931 | * @param array $config 932 | * @param string $prefix 933 | * @return string 934 | */ 935 | protected function getDependingFieldName(array $config, string $prefix): string 936 | { 937 | if (empty($config['field'])) { 938 | return ''; 939 | } 940 | 941 | $field = $config['field']; 942 | $prefixParts = explode('__', substr($prefix, 11)); 943 | 944 | if ($field[0] === '/') { 945 | return substr($field, 1); 946 | } 947 | 948 | while(substr($field, 0, 3) === '../') { 949 | $field = substr($field, 3); 950 | 951 | if (!\count($prefixParts)) { 952 | throw new \RuntimeException(sprintf('Invalid field path "%s" for prefix "%s".', $config['field'], $prefix)); 953 | } 954 | 955 | array_splice($prefixParts, -2); 956 | } 957 | 958 | if (!\count($prefixParts)) { 959 | return $field; 960 | } 961 | 962 | $prefixParts[\count($prefixParts) - 1] = ""; 963 | 964 | return 'rsce_field_' . implode('__', $prefixParts) . $field; 965 | } 966 | 967 | /** 968 | * Compare depending value from config with actual value 969 | * 970 | * @param bool|string|array $dependingValue 971 | * @param string|array $actualValue 972 | * @return bool 973 | */ 974 | protected function dependingValueMatches($dependingValue, $actualValue): bool 975 | { 976 | if (\is_array($dependingValue)) { 977 | foreach ($dependingValue as $value) { 978 | if ($this->dependingValueMatches($value, $actualValue)) { 979 | return true; 980 | } 981 | } 982 | 983 | return false; 984 | } 985 | 986 | if (\is_array($actualValue)) { 987 | foreach ($actualValue as $value) { 988 | if ($this->dependingValueMatches($dependingValue, $value)) { 989 | return true; 990 | } 991 | } 992 | 993 | return false; 994 | } 995 | 996 | if ($dependingValue === true) { 997 | return $actualValue || $actualValue === '0'; 998 | } 999 | 1000 | if ($dependingValue === false) { 1001 | return !$actualValue && $actualValue !== '0'; 1002 | } 1003 | 1004 | return $dependingValue === $actualValue; 1005 | } 1006 | 1007 | /** 1008 | * Page picker wizard for url fields 1009 | * 1010 | * @param DataContainer $dc Data container 1011 | * @return string Page picker button html code 1012 | */ 1013 | public function pagePicker($dc) 1014 | { 1015 | @trigger_error('Using pagePicker() has been deprecated and will be removed in a future version. Set the "dcaPicker" eval attribute instead.', E_USER_DEPRECATED); 1016 | 1017 | return Backend::getDcaPickerWizard(true, $dc->table, $dc->field, $dc->inputName); 1018 | } 1019 | 1020 | /** 1021 | * Check if a field was sumitted via POST 1022 | * 1023 | * @param string $fieldName field name to check 1024 | * @param int $dataKey data index 1025 | * @return boolean true if the field was sumitted via POST 1026 | */ 1027 | protected function wasListFieldSubmitted($fieldName, $dataKey) 1028 | { 1029 | if (!is_array(Input::post('FORM_FIELDS'))) { 1030 | return false; 1031 | } 1032 | 1033 | if (strpos($fieldName, '__rsce_dummy__') !== false) { 1034 | return false; 1035 | } 1036 | 1037 | $formFields = array_unique(StringUtil::trimsplit( 1038 | '[,;]', 1039 | implode(',', Input::post('FORM_FIELDS')) 1040 | )); 1041 | 1042 | $fieldPrefix = $fieldName . '__' . $dataKey . '__'; 1043 | 1044 | foreach ($formFields as $field) { 1045 | if (substr($field, 0, strlen($fieldPrefix)) === $fieldPrefix) { 1046 | return true; 1047 | } 1048 | } 1049 | 1050 | return false; 1051 | } 1052 | 1053 | /** 1054 | * Create one list item dummy with the specified parameters 1055 | * 1056 | * @param string $fieldPrefix Field prefix, e.g. "rsce_field_" 1057 | * @param string $fieldName Field name 1058 | * @param array $fieldConfig Field configuration array 1059 | * @param array $paletteFields Reference to the list of all fields 1060 | * @param DataContainer $dc Data container 1061 | * @param boolean $createFromPost Whether to create the field structure from post data or not 1062 | * @return void 1063 | */ 1064 | protected function createDcaItemListDummy($fieldPrefix, $fieldName, $fieldConfig, &$paletteFields, $dc, $createFromPost) 1065 | { 1066 | $dataKey = 'rsce_dummy'; 1067 | 1068 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_start'] = array( 1069 | 'exclude' => false, 1070 | 'inputType' => 'rsce_list_item_start', 1071 | 'label' => array($fieldConfig['elementLabel']), 1072 | 'eval' => array( 1073 | 'tl_class' => 'rsce_list_item_dummy', 1074 | 'disabled' => true, 1075 | 'label_template' => $fieldConfig['elementLabel'], 1076 | ), 1077 | ); 1078 | $paletteFields[] = $fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_start'; 1079 | 1080 | foreach ($fieldConfig['fields'] as $fieldName2 => $fieldConfig2) { 1081 | $this->createDcaItem($fieldPrefix . $fieldName . '__' . $dataKey . '__', $fieldName2, $fieldConfig2, $paletteFields, $dc, $createFromPost); 1082 | } 1083 | 1084 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_stop'] = array( 1085 | 'exclude' => false, 1086 | 'inputType' => 'rsce_list_item_stop', 1087 | 'eval' => array( 1088 | 'disabled' => true, 1089 | ), 1090 | ); 1091 | $paletteFields[] = $fieldPrefix . $fieldName . '__' . $dataKey . '_rsce_list_item_stop'; 1092 | } 1093 | 1094 | /** 1095 | * Create all DCA standard fields for multi edit mode 1096 | * 1097 | * @param DataContainer $dc Data container 1098 | * @return void 1099 | */ 1100 | protected function createDcaMultiEdit($dc) 1101 | { 1102 | $session = System::getContainer()->get('request_stack')->getSession()->all(); 1103 | if (empty($session['CURRENT']['IDS']) || !is_array($session['CURRENT']['IDS'])) { 1104 | return; 1105 | } 1106 | $ids = $session['CURRENT']['IDS']; 1107 | 1108 | $types = Database::getInstance() 1109 | ->prepare(' 1110 | SELECT type 1111 | FROM ' . $dc->table . ' 1112 | WHERE id IN (' . implode(',', $ids) . ') 1113 | AND type LIKE \'rsce_%\' 1114 | GROUP BY type 1115 | ') 1116 | ->execute() 1117 | ->fetchEach('type'); 1118 | 1119 | if (!$types) { 1120 | return; 1121 | } 1122 | 1123 | foreach ($types as $type) { 1124 | 1125 | $paletteFields = array(); 1126 | $config = static::getConfigByType($type); 1127 | 1128 | if (!$config) { 1129 | continue; 1130 | } 1131 | 1132 | $standardFields = is_array($config['standardFields'] ?? null) ? $config['standardFields'] : array(); 1133 | 1134 | foreach ($config['fields'] as $fieldName => $fieldConfig) { 1135 | if (isset($fieldConfig['inputType']) && $fieldConfig['inputType'] !== 'list') { 1136 | $this->createDcaItem($type . '_field_', $fieldName, $fieldConfig, $paletteFields, $dc, false, true); 1137 | } 1138 | } 1139 | 1140 | $GLOBALS['TL_DCA'][$dc->table]['palettes'][$type] = static::generatePalette( 1141 | $dc->table, 1142 | $paletteFields, 1143 | $standardFields 1144 | ); 1145 | 1146 | } 1147 | } 1148 | 1149 | /** 1150 | * Get configuration array for the specified type 1151 | * 1152 | * @param string $type Element type beginning with "rsce_" 1153 | * @return array|null Configuration array 1154 | */ 1155 | public static function getConfigByType($type) 1156 | { 1157 | $configPath = null; 1158 | 1159 | try { 1160 | $templatePaths = CustomTemplate::getTemplates($type); 1161 | if (!empty($templatePaths[0])) { 1162 | $configPath = substr($templatePaths[0], 0, -6) . '_config.php'; 1163 | } 1164 | } 1165 | catch (\Exception $e) { 1166 | $configPath = null; 1167 | } 1168 | 1169 | if ($configPath === null) { 1170 | try { 1171 | $twigLoader = System::getContainer()->get('contao.twig.filesystem_loader'); 1172 | $configPath = substr($twigLoader->getSourceContext($twigLoader->getFirst($type))->getPath(), 0, -10) . '_config.php'; 1173 | } 1174 | catch (\Exception $e) { 1175 | $configPath = null; 1176 | } 1177 | } 1178 | 1179 | if ($configPath === null || !file_exists($configPath)) { 1180 | $allConfigs = array_merge( 1181 | glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/' . $type . '_config.php') ?: array(), 1182 | glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/*/' . $type . '_config.php') ?: array() 1183 | ); 1184 | if (count($allConfigs)) { 1185 | $configPath = $allConfigs[0]; 1186 | } 1187 | else { 1188 | return; 1189 | } 1190 | } 1191 | 1192 | $config = include $configPath; 1193 | 1194 | if ($config) { 1195 | $config['fields'] = is_array($config['fields'] ?? null) ? $config['fields'] : array(); 1196 | } 1197 | 1198 | return $config; 1199 | } 1200 | 1201 | /** 1202 | * Generates the palette definition 1203 | * 1204 | * @param string $table "tl_content", "tl_module" or "tl_form_field" 1205 | * @param array $paletteFields Palette fields 1206 | * @param array $standardFields Standard fields 1207 | * @return string Palette definition 1208 | */ 1209 | protected static function generatePalette($table, array $paletteFields = array(), array $standardFields = array()) 1210 | { 1211 | $palette = ''; 1212 | 1213 | if ($table === 'tl_module') { 1214 | $palette .= '{title_legend},name'; 1215 | if (in_array('headline', $standardFields)) { 1216 | $palette .= ',headline'; 1217 | } 1218 | $palette .= ',type'; 1219 | } 1220 | else { 1221 | $palette .= '{type_legend}'; 1222 | if (version_compare(ContaoCoreBundle::getVersion(), '5.6', '>=')) { 1223 | if ($table === 'tl_content') { 1224 | $palette .= ',title'; 1225 | } 1226 | } 1227 | else { 1228 | $palette .= ',type'; 1229 | } 1230 | if ($table === 'tl_content' && in_array('headline', $standardFields)) { 1231 | $palette .= ',headline'; 1232 | } 1233 | if (version_compare(ContaoCoreBundle::getVersion(), '5.6', '>=')) { 1234 | $palette .= ',type'; 1235 | } 1236 | if (in_array('columns', $standardFields)) { 1237 | $palette .= ';{rs_columns_legend},' . explode(';', explode('{rs_columns_legend},', $GLOBALS['TL_DCA']['tl_content']['palettes']['rs_columns_start'] ?? '')[1] ?? '')[0]; 1238 | } 1239 | if (in_array('text', $standardFields)) { 1240 | $palette .= ';{text_legend},text'; 1241 | } 1242 | } 1243 | 1244 | if ( 1245 | isset($paletteFields[0]) 1246 | && $paletteFields[0] !== 'rsce_data' 1247 | && isset($GLOBALS['TL_DCA'][$table]['fields'][$paletteFields[0]]['inputType']) 1248 | && $GLOBALS['TL_DCA'][$table]['fields'][$paletteFields[0]]['inputType'] !== 'rsce_group_start' 1249 | && $GLOBALS['TL_DCA'][$table]['fields'][$paletteFields[0]]['inputType'] !== 'rsce_list_start' 1250 | ) { 1251 | $palette .= ';{rsce_legend}'; 1252 | } 1253 | 1254 | $palette .= ',' . implode(',', $paletteFields); 1255 | 1256 | if ($table === 'tl_content' && in_array('image', $standardFields)) { 1257 | $palette .= ';{image_legend},addImage'; 1258 | } 1259 | 1260 | if ($table === 'tl_form_field') { 1261 | $palette .= ';{expert_legend:hide},class'; 1262 | } 1263 | 1264 | $palette .= ';{template_legend:hide},customTpl'; 1265 | 1266 | if ($table !== 'tl_form_field') { 1267 | 1268 | $palette .= ';{protected_legend:hide},protected;{expert_legend:hide},guests'; 1269 | 1270 | if (in_array('cssID', $standardFields)) { 1271 | $palette .= ',cssID'; 1272 | } 1273 | 1274 | } 1275 | 1276 | if ($table === 'tl_content') { 1277 | $palette .= ';{invisible_legend:hide},invisible,start,stop'; 1278 | } 1279 | 1280 | return $palette; 1281 | } 1282 | 1283 | /** 1284 | * Get the value of a field (from POST data, active record or the database) 1285 | * 1286 | * @param DataContainer $dc Data container 1287 | * @param string $fieldName Field name 1288 | * @param boolean $fromDb True to ignore POST data 1289 | * @return string The value 1290 | */ 1291 | protected static function getDcaFieldValue($dc, $fieldName, $fromDb = false) 1292 | { 1293 | $value = null; 1294 | 1295 | if (Input::post('FORM_SUBMIT') === $dc->table && !$fromDb) { 1296 | $value = Input::post($fieldName); 1297 | if ($value !== null) { 1298 | return $value; 1299 | } 1300 | } 1301 | 1302 | if ($dc->activeRecord) { 1303 | $value = $dc->activeRecord->$fieldName; 1304 | } 1305 | else { 1306 | 1307 | $table = $dc->table; 1308 | $id = $dc->id; 1309 | 1310 | if (Input::get('target')) { 1311 | $table = explode('.', Input::get('target'), 2)[0]; 1312 | $id = (int) explode('.', Input::get('target'), 3)[2]; 1313 | } 1314 | 1315 | if ($table && $id) { 1316 | $record = Database::getInstance() 1317 | ->prepare("SELECT * FROM {$table} WHERE id=?") 1318 | ->execute($id); 1319 | if ($record->next()) { 1320 | $value = $record->$fieldName; 1321 | } 1322 | } 1323 | 1324 | } 1325 | 1326 | return $value; 1327 | } 1328 | 1329 | /** 1330 | * Purge cache file rocksolid_custom_elements_config.php 1331 | * 1332 | * @return void 1333 | */ 1334 | public static function purgeCache() 1335 | { 1336 | $filePaths = static::getCacheFilePaths(); 1337 | 1338 | if (file_exists($filePaths['fullPath'])) { 1339 | (new Filesystem())->dumpFile($filePaths['fullPath'], ''); 1340 | static::refreshOpcodeCache($filePaths['fullPath']); 1341 | } 1342 | } 1343 | 1344 | /** 1345 | * Get path and fullPath to the cache file 1346 | * 1347 | * @return string 1348 | */ 1349 | public static function getCacheFilePaths() 1350 | { 1351 | $cacheDir = System::getContainer()->getParameter('kernel.cache_dir') . '/contao'; 1352 | 1353 | $filePath = $cacheDir . '/rocksolid_custom_elements_config.php'; 1354 | 1355 | return array( 1356 | 'path' => StringUtil::stripRootDir($filePath), 1357 | 'fullPath' => $filePath, 1358 | ); 1359 | } 1360 | 1361 | /** 1362 | * Load the TL_CTE, FE_MOD and TL_FFL configuration and use caching if possible 1363 | * 1364 | * @param bool $bypassCache 1365 | * @return void 1366 | */ 1367 | public static function loadConfig($bypassCache = false) 1368 | { 1369 | $filePaths = static::getCacheFilePaths(); 1370 | 1371 | $cacheHash = md5(implode(',', array_merge( 1372 | glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/rsce_*') ?: array(), 1373 | glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/*/rsce_*') ?: array() 1374 | ))); 1375 | 1376 | if (!$bypassCache && file_exists($filePaths['fullPath'])) { 1377 | $fileCacheHash = null; 1378 | include $filePaths['fullPath']; 1379 | if ($fileCacheHash === $cacheHash) { 1380 | // the cache file is valid and loaded 1381 | return; 1382 | } 1383 | } 1384 | 1385 | System::loadLanguageFile('default'); 1386 | System::loadLanguageFile('tl_content'); 1387 | System::loadLanguageFile('tl_module'); 1388 | 1389 | $contents = array(); 1390 | $contents[] = 'getParameter('kernel.project_dir') . '/templates/rsce_*_config.php') ?: array(), 1397 | glob(System::getContainer()->getParameter('kernel.project_dir') . '/templates/*/rsce_*_config.php') ?: array() 1398 | ); 1399 | $fallbackConfigPaths = array(); 1400 | 1401 | $duplicateConfigs = array_filter( 1402 | array_count_values(array_map( 1403 | function($configPath) { 1404 | return basename($configPath, '_config.php'); 1405 | }, 1406 | $allConfigs 1407 | )), 1408 | function ($count) { 1409 | return $count > 1; 1410 | } 1411 | ); 1412 | if (count($duplicateConfigs)) { 1413 | System::getContainer()->get('monolog.logger.contao')->log(LogLevel::ERROR, 'Duplicate Custom Elements found: ' . implode(', ', array_keys($duplicateConfigs)), array('contao' => new ContaoContext(__METHOD__, 'ERROR'))); 1414 | } 1415 | 1416 | foreach ($allConfigs as $configPath) { 1417 | $templateName = basename($configPath, '_config.php'); 1418 | if ( 1419 | file_exists(substr($configPath, 0, -11) . '.html5') 1420 | || file_exists(substr($configPath, 0, -11) . '.html.twig') 1421 | ) { 1422 | if (!isset($templates[$templateName])) { 1423 | $templates[$templateName] = $templateName; 1424 | } 1425 | if (!isset($fallbackConfigPaths[$templateName])) { 1426 | $fallbackConfigPaths[$templateName] = $configPath; 1427 | } 1428 | } 1429 | } 1430 | 1431 | try { 1432 | $themes = Database::getInstance() 1433 | ->prepare('SELECT name, templates FROM tl_theme') 1434 | ->execute() 1435 | ->fetchAllAssoc(); 1436 | } 1437 | catch (DBALException $e) { 1438 | $themes = array(); 1439 | } 1440 | catch (\Doctrine\DBAL\Exception $e) { 1441 | $themes = array(); 1442 | } 1443 | $themeNamesByTemplateDir = array(); 1444 | foreach ($themes as $theme) { 1445 | if ($theme['templates']) { 1446 | $themeNamesByTemplateDir[$theme['templates']] = $theme['name']; 1447 | } 1448 | } 1449 | 1450 | $twigLoader = System::getContainer()->get('contao.twig.filesystem_loader'); 1451 | $saveToCache = true; 1452 | $elements = array(); 1453 | 1454 | foreach ($templates as $template => $label) { 1455 | 1456 | if (substr($template, -7) === '_config') { 1457 | continue; 1458 | } 1459 | 1460 | $configPath = null; 1461 | 1462 | try { 1463 | $templatePaths = CustomTemplate::getTemplates($template); 1464 | if (!empty($templatePaths[0])) { 1465 | $configPath = substr($templatePaths[0], 0, -6) . '_config.php'; 1466 | } 1467 | } 1468 | catch (\Exception $e) { 1469 | $configPath = null; 1470 | } 1471 | 1472 | if ($configPath === null) { 1473 | try { 1474 | $configPath = substr($twigLoader->getSourceContext($twigLoader->getFirst($template))->getPath(), 0, -10) . '_config.php'; 1475 | } 1476 | catch (\Exception $e) { 1477 | $configPath = null; 1478 | } 1479 | } 1480 | 1481 | if ($configPath === null || !file_exists($configPath)) { 1482 | if (isset($fallbackConfigPaths[$template])) { 1483 | $configPath = $fallbackConfigPaths[$template]; 1484 | } 1485 | else { 1486 | continue; 1487 | } 1488 | } 1489 | 1490 | try { 1491 | $config = include $configPath; 1492 | } 1493 | catch (\Throwable $exception) { 1494 | $request = System::getContainer()->get('request_stack')->getCurrentRequest(); 1495 | 1496 | // Swallow exceptions in install tool and on CLI 1497 | if (!$request || $request->get('_route') === 'contao_install') { 1498 | $saveToCache = false; 1499 | continue; 1500 | } 1501 | 1502 | throw $exception; 1503 | } 1504 | 1505 | $element = array( 1506 | 'config' => $config, 1507 | 'label' => isset($config['label']) ? $config['label'] : array(implode(' ', array_map('ucfirst', explode('_', substr($template, 5)))), ''), 1508 | 'labelPrefix' => '', 1509 | 'types' => isset($config['types']) ? $config['types'] : array('content', 'module', 'form'), 1510 | 'contentCategory' => isset($config['contentCategory']) ? $config['contentCategory'] : 'custom_elements', 1511 | 'moduleCategory' => isset($config['moduleCategory']) ? $config['moduleCategory'] : 'custom_elements', 1512 | 'template' => $template, 1513 | 'path' => substr(dirname($configPath), strlen(System::getContainer()->getParameter('kernel.project_dir') . '/')), 1514 | ); 1515 | 1516 | if ($element['path'] && substr($element['path'], 0, 10) === 'templates/') { 1517 | if (isset($themeNamesByTemplateDir[$element['path']])) { 1518 | $element['labelPrefix'] = $themeNamesByTemplateDir[$element['path']] . ': '; 1519 | } 1520 | else { 1521 | $element['labelPrefix'] = implode(' ', array_map('ucfirst', preg_split('(\\W)', substr($element['path'], 10)))) . ': '; 1522 | } 1523 | } 1524 | 1525 | $elements[] = $element; 1526 | 1527 | } 1528 | 1529 | usort($elements, function($a, $b) { 1530 | if ($a['path'] !== $b['path']) { 1531 | if ($a['path'] === 'templates') { 1532 | return -1; 1533 | } 1534 | if ($b['path'] === 'templates') { 1535 | return 1; 1536 | } 1537 | return strcmp($a['labelPrefix'], $b['labelPrefix']); 1538 | } 1539 | return strcmp($a['template'], $b['template']); 1540 | }); 1541 | 1542 | $addLabelPrefix = count(array_unique(array_map(function($element) { 1543 | return $element['path']; 1544 | }, $elements))) > 1; 1545 | 1546 | foreach ($elements as $element) { 1547 | 1548 | if (in_array('content', $element['types'])) { 1549 | 1550 | $GLOBALS['TL_CTE'][$element['contentCategory']][$element['template']] = 'MadeYourDay\\RockSolidCustomElements\\Element\\CustomElement'; 1551 | $contents[] = '$GLOBALS[\'TL_CTE\'][\'' . $element['contentCategory'] . '\'][\'' . $element['template'] . '\'] = \'MadeYourDay\\\\RockSolidCustomElements\\\\Element\\\\CustomElement\';'; 1552 | 1553 | $GLOBALS['TL_LANG']['CTE'][$element['template']] = static::getLabelTranslated($element['label']); 1554 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'CTE\'][\'' . $element['template'] . '\'] = \\MadeYourDay\\RockSolidCustomElements\\CustomElements::getLabelTranslated(' . var_export($element['label'], true) . ');'; 1555 | 1556 | if ($addLabelPrefix && $element['labelPrefix']) { 1557 | $GLOBALS['TL_LANG']['CTE'][$element['template']][0] = $element['labelPrefix'] . $GLOBALS['TL_LANG']['CTE'][$element['template']][0]; 1558 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'CTE\'][\'' . $element['template'] . '\'][0] = ' . var_export($element['labelPrefix'], true) . ' . $GLOBALS[\'TL_LANG\'][\'CTE\'][\'' . $element['template'] . '\'][0];'; 1559 | } 1560 | 1561 | if (!isset($GLOBALS['TL_LANG']['CTE'][$element['contentCategory']])) { 1562 | $GLOBALS['TL_LANG']['CTE'][$element['contentCategory']] = $element['contentCategory']; 1563 | } 1564 | $contents[] = 'if (!isset($GLOBALS[\'TL_LANG\'][\'CTE\'][' . var_export($element['contentCategory'], true) . '])) {'; 1565 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'CTE\'][' . var_export($element['contentCategory'], true) . '] = ' . var_export($element['contentCategory'], true) . ';'; 1566 | $contents[] = '}'; 1567 | 1568 | } 1569 | 1570 | if (in_array('module', $element['types'])) { 1571 | 1572 | $GLOBALS['FE_MOD'][$element['moduleCategory']][$element['template']] = 'MadeYourDay\\RockSolidCustomElements\\Element\\CustomElement'; 1573 | $contents[] = '$GLOBALS[\'FE_MOD\'][\'' . $element['moduleCategory'] . '\'][\'' . $element['template'] . '\'] = \'MadeYourDay\\\\RockSolidCustomElements\\\\Element\\\\CustomElement\';'; 1574 | 1575 | $GLOBALS['TL_LANG']['FMD'][$element['template']] = static::getLabelTranslated($element['label']); 1576 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'FMD\'][\'' . $element['template'] . '\'] = \\MadeYourDay\\RockSolidCustomElements\\CustomElements::getLabelTranslated(' . var_export($element['label'], true) . ');'; 1577 | 1578 | if ($addLabelPrefix && $element['labelPrefix']) { 1579 | $GLOBALS['TL_LANG']['FMD'][$element['template']][0] = $element['labelPrefix'] . $GLOBALS['TL_LANG']['FMD'][$element['template']][0]; 1580 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'FMD\'][\'' . $element['template'] . '\'][0] = ' . var_export($element['labelPrefix'], true) . ' . $GLOBALS[\'TL_LANG\'][\'FMD\'][\'' . $element['template'] . '\'][0];'; 1581 | } 1582 | 1583 | if (!isset($GLOBALS['TL_LANG']['FMD'][$element['moduleCategory']])) { 1584 | $GLOBALS['TL_LANG']['FMD'][$element['moduleCategory']] = $element['moduleCategory']; 1585 | } 1586 | $contents[] = 'if (!isset($GLOBALS[\'TL_LANG\'][\'FMD\'][' . var_export($element['moduleCategory'], true) . '])) {'; 1587 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'FMD\'][' . var_export($element['moduleCategory'], true) . '] = ' . var_export($element['moduleCategory'], true) . ';'; 1588 | $contents[] = '}'; 1589 | 1590 | } 1591 | 1592 | if (in_array('form', $element['types'])) { 1593 | 1594 | $hasInput = isset($element['config']['fields']['name']['inputType']) && $element['config']['fields']['name']['inputType'] === 'standardField'; 1595 | 1596 | $GLOBALS['TL_FFL'][$element['template']] = 'MadeYourDay\\RockSolidCustomElements\\Form\\CustomWidget'.($hasInput ? '' : 'NoInput'); 1597 | $contents[] = '$GLOBALS[\'TL_FFL\'][\'' . $element['template'] . '\'] = \'MadeYourDay\\\\RockSolidCustomElements\\\\Form\\\\CustomWidget'.($hasInput ? '' : 'NoInput').'\';'; 1598 | 1599 | $GLOBALS['TL_LANG']['FFL'][$element['template']] = static::getLabelTranslated($element['label']); 1600 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'FFL\'][\'' . $element['template'] . '\'] = \\MadeYourDay\\RockSolidCustomElements\\CustomElements::getLabelTranslated(' . var_export($element['label'], true) . ');'; 1601 | 1602 | if ($addLabelPrefix && $element['labelPrefix']) { 1603 | $GLOBALS['TL_LANG']['FFL'][$element['template']][0] = $element['labelPrefix'] . $GLOBALS['TL_LANG']['FFL'][$element['template']][0]; 1604 | $contents[] = '$GLOBALS[\'TL_LANG\'][\'FFL\'][\'' . $element['template'] . '\'][0] = ' . var_export($element['labelPrefix'], true) . ' . $GLOBALS[\'TL_LANG\'][\'FFL\'][\'' . $element['template'] . '\'][0];'; 1605 | } 1606 | 1607 | } 1608 | 1609 | if (!empty($element['config']['wrapper']['type'])) { 1610 | $GLOBALS['TL_WRAPPERS'][$element['config']['wrapper']['type']][] = $element['template']; 1611 | $contents[] = '$GLOBALS[\'TL_WRAPPERS\'][' . var_export($element['config']['wrapper']['type'], true) . '][] = ' . var_export($element['template'], true) . ';'; 1612 | } 1613 | 1614 | } 1615 | 1616 | if (!$saveToCache) { 1617 | return; 1618 | } 1619 | 1620 | (new Filesystem())->dumpFile($filePaths['fullPath'], implode("\n", $contents)); 1621 | static::refreshOpcodeCache($filePaths['fullPath']); 1622 | } 1623 | 1624 | /** 1625 | * Call loadConfig and bypass the cache 1626 | * 1627 | * @return void 1628 | */ 1629 | public static function reloadConfig() 1630 | { 1631 | return static::loadConfig(true); 1632 | } 1633 | 1634 | /** 1635 | * Refreshes all active opcode caches for the specified file 1636 | * 1637 | * @param string $path Path to the file 1638 | * @return boolean True on success, false on failure 1639 | */ 1640 | protected static function refreshOpcodeCache($path) 1641 | { 1642 | try { 1643 | 1644 | // Zend OPcache 1645 | if (function_exists('opcache_invalidate')) { 1646 | opcache_invalidate($path, true); 1647 | } 1648 | 1649 | // Zend Optimizer+ 1650 | if (function_exists('accelerator_reset')) { 1651 | accelerator_reset(); 1652 | } 1653 | 1654 | // APC 1655 | if (function_exists('apc_compile_file') && !ini_get('apc.stat')) { 1656 | apc_compile_file($path); 1657 | } 1658 | 1659 | // eAccelerator 1660 | if (function_exists('eaccelerator_purge') && !ini_get('eaccelerator.check_mtime')) { 1661 | @eaccelerator_purge(); 1662 | } 1663 | 1664 | // XCache 1665 | if (function_exists('xcache_count') && !ini_get('xcache.stat')) { 1666 | if (($count = xcache_count(XC_TYPE_PHP)) > 0) { 1667 | for ($id = 0; $id < $count; $id++) { 1668 | xcache_clear_cache(XC_TYPE_PHP, $id); 1669 | } 1670 | } 1671 | } 1672 | 1673 | // WinCache 1674 | if (function_exists('wincache_refresh_if_changed')) { 1675 | wincache_refresh_if_changed(array($path)); 1676 | } 1677 | 1678 | } 1679 | catch(\Exception $exception) { 1680 | return false; 1681 | } 1682 | 1683 | return true; 1684 | } 1685 | 1686 | /** 1687 | * Reload translated labels if default language file gets loaded 1688 | * 1689 | * @param string $name 1690 | * @param string $language 1691 | * @return void 1692 | */ 1693 | public function loadLanguageFileHook($name, $language) 1694 | { 1695 | if ($name === 'default') { 1696 | static::loadConfig(); 1697 | } 1698 | } 1699 | 1700 | /** 1701 | * Return translated label if label configuration contains language keys 1702 | * 1703 | * @param array $labelConfig 1704 | * @return mixed Translated label if exists, otherwise $labelConfig 1705 | */ 1706 | public static function getLabelTranslated($labelConfig) 1707 | { 1708 | if (!is_array($labelConfig)) { 1709 | return $labelConfig; 1710 | } 1711 | 1712 | // Return if it isn't an associative array 1713 | if (!count(array_filter(array_keys($labelConfig), 'is_string'))) { 1714 | return $labelConfig; 1715 | } 1716 | 1717 | $language = str_replace('-', '_', $GLOBALS['TL_LANGUAGE']); 1718 | if (isset($labelConfig[$language])) { 1719 | return $labelConfig[$language]; 1720 | } 1721 | 1722 | // Try the short language code 1723 | $language = substr($language, 0, 2); 1724 | if (isset($labelConfig[$language])) { 1725 | return $labelConfig[$language]; 1726 | } 1727 | 1728 | // Fall back to english 1729 | $language = 'en'; 1730 | if (isset($labelConfig[$language])) { 1731 | return $labelConfig[$language]; 1732 | } 1733 | 1734 | // Return the first item that seems to be a language key 1735 | foreach ($labelConfig as $key => $label) { 1736 | if (strlen($key) === 2 || substr($key, 2, 1) === '_') { 1737 | return $label; 1738 | } 1739 | } 1740 | 1741 | return $labelConfig; 1742 | } 1743 | 1744 | /** 1745 | * Convert IDs for theme export 1746 | * 1747 | * @param \DOMDocument $xml theme.xml 1748 | * @param \ZipWriter $zipArchive CTO file 1749 | * @param int $themeId 1750 | * @return void 1751 | */ 1752 | public function exportThemeHook($xml, $zipArchive, $themeId) 1753 | { 1754 | $xpath = new \DOMXPath($xml); 1755 | $tlModule = $xpath->query('/tables/table[@name = \'tl_module\']')->item(0); 1756 | 1757 | if (!$tlModule) { 1758 | return; 1759 | } 1760 | 1761 | static::reloadConfig(); 1762 | 1763 | foreach ($tlModule->childNodes as $row) { 1764 | 1765 | if (strtolower($row->nodeName) !== 'row') { 1766 | continue; 1767 | } 1768 | 1769 | $type = $xpath->query('field[@name = \'type\']', $row)->item(0); 1770 | if (!$type || substr($type->nodeValue, 0, 5) !== 'rsce_') { 1771 | continue; 1772 | } 1773 | 1774 | $rsceData = $xpath->query('field[@name = \'rsce_data\']', $row)->item(0); 1775 | if (!$rsceData) { 1776 | continue; 1777 | } 1778 | 1779 | $rsceDataConverted = $this->convertDataForImportExport( 1780 | false, 1781 | $type->nodeValue, 1782 | $rsceData->nodeValue 1783 | ); 1784 | 1785 | if ( 1786 | !$rsceDataConverted 1787 | || strtolower($rsceDataConverted) === 'null' 1788 | || $rsceDataConverted === '{}' 1789 | || $rsceDataConverted === '[]' 1790 | || $rsceDataConverted === $rsceData->nodeValue 1791 | ) { 1792 | continue; 1793 | } 1794 | 1795 | $rsceData->nodeValue = $rsceDataConverted; 1796 | 1797 | } 1798 | } 1799 | 1800 | /** 1801 | * Convert IDs for theme import 1802 | * 1803 | * @param \DOMDocument $xml theme.xml 1804 | * @param \ZipWriter $zipArchive CTO file 1805 | * @param int $themeId 1806 | * @param array $idMappingData ID mapping for imported database rows 1807 | * @return void 1808 | */ 1809 | public function extractThemeFilesHook($xml, $zipArchive, $themeId, $idMappingData) 1810 | { 1811 | $modules = ModuleModel::findBy( 1812 | array('tl_module.pid = ? AND tl_module.type LIKE \'rsce_%\''), 1813 | $themeId 1814 | ); 1815 | 1816 | if (!$modules || !count($modules)) { 1817 | return; 1818 | } 1819 | 1820 | foreach ($modules as $module) { 1821 | 1822 | if (substr($module->type, 0, 5) !== 'rsce_') { 1823 | continue; 1824 | } 1825 | 1826 | $rsceDataConverted = $this->convertDataForImportExport( 1827 | true, 1828 | $module->type, 1829 | $module->rsce_data, 1830 | $idMappingData 1831 | ); 1832 | 1833 | if ( 1834 | !$rsceDataConverted 1835 | || strtolower($rsceDataConverted) === 'null' 1836 | || $rsceDataConverted === '{}' 1837 | || $rsceDataConverted === '[]' 1838 | || $rsceDataConverted === $module->rsce_data 1839 | ) { 1840 | continue; 1841 | } 1842 | 1843 | $module->rsce_data = $rsceDataConverted; 1844 | 1845 | $module->save(); 1846 | 1847 | } 1848 | } 1849 | 1850 | /** 1851 | * @param bool $import True for import, false for export 1852 | * @param string $type Element type 1853 | * @param string $jsonData JSON-encoded data 1854 | * @param array $idMappingData ID mapping for imported database rows 1855 | * @return string Converted $jsonData 1856 | */ 1857 | protected function convertDataForImportExport($import, $type, $jsonData, $idMappingData = array()) 1858 | { 1859 | $data = json_decode($jsonData, true); 1860 | $config = static::getConfigByType($type); 1861 | 1862 | if (!$config || !$data) { 1863 | return $jsonData; 1864 | } 1865 | 1866 | $data = $this->convertDataForImportExportParseFields( 1867 | $import, 1868 | $data, 1869 | $config['fields'], 1870 | $idMappingData 1871 | ); 1872 | 1873 | return json_encode($data); 1874 | } 1875 | 1876 | /** 1877 | * @param bool $import True for import, false for export 1878 | * @param array $data Data of element or parent list item 1879 | * @param array $config Fields configuration 1880 | * @param array $idMappingData ID mapping for imported database rows 1881 | * @param string $fieldPrefix 1882 | * @return array Converted $data 1883 | */ 1884 | protected function convertDataForImportExportParseFields($import, $data, $config, $idMappingData, $fieldPrefix = 'rsce_field_') 1885 | { 1886 | foreach ($data as $fieldName => $value) { 1887 | 1888 | $fieldConfig = $this->getNestedConfig($fieldPrefix . $fieldName, $config); 1889 | 1890 | if (empty($fieldConfig['inputType'])) { 1891 | continue; 1892 | } 1893 | 1894 | if ($fieldConfig['inputType'] === 'list') { 1895 | 1896 | for ($dataKey = 0; isset($value[$dataKey]); $dataKey++) { 1897 | $data[$fieldName][$dataKey] = $this->convertDataForImportExportParseFields( 1898 | $import, 1899 | $value[$dataKey], 1900 | $config, 1901 | $idMappingData, 1902 | $fieldPrefix . $fieldName . '__' . $dataKey . '__' 1903 | ); 1904 | } 1905 | 1906 | } 1907 | 1908 | // UUIDs to paths and vice versa 1909 | else if ($value && ( 1910 | $fieldConfig['inputType'] === 'fileTree' 1911 | || $fieldConfig['inputType'] === 'fineUploader' 1912 | )) { 1913 | 1914 | if (empty($fieldConfig['eval']['multiple'])) { 1915 | 1916 | if ($import) { 1917 | $file = FilesModel::findByPath(System::getContainer()->getParameter('contao.upload_path') . '/' . preg_replace('(^files/)', '', $value)); 1918 | if ($file) { 1919 | $data[$fieldName] = StringUtil::binToUuid($file->uuid); 1920 | } 1921 | } 1922 | else { 1923 | $file = FilesModel::findById($value); 1924 | if ($file) { 1925 | $data[$fieldName] = 'files/' . preg_replace('(^' . preg_quote(System::getContainer()->getParameter('contao.upload_path')) . '/)', '', $file->path); 1926 | } 1927 | } 1928 | 1929 | } 1930 | else { 1931 | 1932 | $data[$fieldName] = serialize(array_map( 1933 | function($value) use($import) { 1934 | if ($import) { 1935 | $file = FilesModel::findByPath(System::getContainer()->getParameter('contao.upload_path') . '/' . preg_replace('(^files/)', '', $value)); 1936 | if ($file) { 1937 | return StringUtil::binToUuid($file->uuid); 1938 | } 1939 | } 1940 | else { 1941 | $file = FilesModel::findById($value); 1942 | if ($file) { 1943 | return 'files/' . preg_replace('(^' . preg_quote(System::getContainer()->getParameter('contao.upload_path')) . '/)', '', $file->path); 1944 | } 1945 | } 1946 | return $value; 1947 | }, 1948 | StringUtil::deserialize($value, true) 1949 | )); 1950 | 1951 | } 1952 | 1953 | } 1954 | 1955 | // tl_image_size IDs 1956 | else if ($fieldConfig['inputType'] === 'imageSize' && $value && $import) { 1957 | 1958 | $value = StringUtil::deserialize($value, true); 1959 | 1960 | if ( 1961 | !empty($value[2]) 1962 | && is_numeric($value[2]) 1963 | && !empty($idMappingData['tl_image_size'][$value[2]]) 1964 | ) { 1965 | $value[2] = $idMappingData['tl_image_size'][$value[2]]; 1966 | $data[$fieldName] = serialize($value); 1967 | } 1968 | 1969 | } 1970 | 1971 | } 1972 | 1973 | return $data; 1974 | } 1975 | 1976 | /** 1977 | * Convert binary UUIDs to text 1978 | * 1979 | * @param string $value 1980 | * 1981 | * @return string 1982 | */ 1983 | private function uuidBinToText($value) 1984 | { 1985 | if (trim($value) && $value !== 'a:1:{i:0;s:0:"";}') { 1986 | if (strlen($value) === 16) { 1987 | return StringUtil::binToUuid($value); 1988 | } 1989 | return serialize(array_map('Contao\StringUtil::binToUuid', StringUtil::deserialize($value))); 1990 | } 1991 | return ''; 1992 | } 1993 | 1994 | /** 1995 | * Convert text UUIDs to binary 1996 | * 1997 | * @param string $value 1998 | * 1999 | * @return string 2000 | */ 2001 | private function uuidTextToBin($value) 2002 | { 2003 | // Multiple files 2004 | if (substr($value, 0, 2) === 'a:') { 2005 | return serialize(array_map(function($value) { 2006 | if (strlen($value) === 36) { 2007 | $value = StringUtil::uuidToBin($value); 2008 | } 2009 | return $value; 2010 | }, StringUtil::deserialize($value))); 2011 | } 2012 | // Single file 2013 | if (strlen($value) === 36) { 2014 | return StringUtil::uuidToBin($value); 2015 | } 2016 | 2017 | return $value; 2018 | } 2019 | } 2020 | --------------------------------------------------------------------------------