FriendsOfREDAXO Tricks

website·Indexed 30 days ago

https://friendsofredaxo.github.io/tricks/

A collection of technical guides, snippets, and custom tools for the REDAXO CMS. Includes implementation details for AddOns such as FOR calendar, MForm, and Consent Manager, as well as tutorials on YForm, accessibility (a11y), and development workflows using GitHub and PLESK.

Tokens
93.6K
Snippets
344
Records
416
Agent score
75%

What's inside FriendsOfREDAXO Tricks

  1. Overview of YDeploy for REDAXO

    YDeploy is an addon based on Deployer used to synchronize data from a local development system to a live system (or staging/other developers).

    What is synchronized:

    • All database tables (structure and fields).
    • Specific database table data if configured (e.g., rex_media_manager or rex_redactor_profile).

    What is NOT synchronized:

    • Records in other tables (e.g., slices, articles).
    • Files in the media folder.
    • Data directories.
    • Cache directories.
  2. Create a searchable icon overview page in REDAXO backend

    This utility allows developers to browse and search for icons provided by the be_style addon (including FontAwesome, Glyphicons, and .rex-icon). It parses style.css to generate a visual gallery where users can filter by icon group, search by name, and double-click an icon name to copy its HTML tag to the clipboard.

    Important Caveat: Since the transition to FontAwesome 6, this method has very limited functionality because icon information is now distributed across multiple files. A revised version is not currently planned.

    // Save this script as «domain»/redaxo/src/addons/project/pages/icons.php
    // (Create the 'pages' folder if it does not exist)
    
    $types = ['FontAwesome'=>'fa','Glyphicons'=>'glyphicon','REX'=>'rex-icon'];
    $filename = $this->getCachePath('icons.html');
    $stylename = rex_path::addonAssets('be_style','css/styles.css');
    
    if( !is_readable($stylename) ) return;
    
    $html = false;
    if( is_readable($filename) && filemtime($filename) < filemtime($stylename) ){
        $html = @file_get_contents($filename);
    }
    $html = false;
    
    if( !$html ) {
        // Extract icons from be_style CSS
        $css = file_get_contents( $stylename );
        $typeset = implode('|',$types);
        $p = preg_match_all(
            '/((\.('.$typeset.')\-[a-z0-9_-]+)((\:before,\.('.$typeset.')\-[a-z0-9_-]+))*)\:before{content:"(?<marker>(.*?))"}/m',
            $css,
            $match,
        );
    
        $icons = [];
        foreach( $match['marker'] as $k=>$code ) {
            $code = sprintf("\\%04X",mb_ord($code));
            $unicode = $result[$code] ?? [];
            preg_match_all( '/(?<class>\.(?<type>('.$typeset.'))-(?<name>[a-z0-9_-]+))/',$match[1][$k],$data );
            foreach( $data['class'] as $i => $class ){
                $type = $data['type'][$i];
                $name = $data['name'][$i];
                $unicode['class'][$class] = $class;
                $unicode['name'][$name] = $name;
                $unicode['type'][$type] = 'x-'.$type;
                if( !isset($unicode['html']) ) $unicode['html'] = "<i class=\"$type $type-$name\"></i>";
            }
            $icons[$code] = $unicode;
        }
        unset( $match );
        ksort($icons);
    
        // Build UI using rex_fragment
        $umfang = new rex_fragment();
        $elements = [];
        foreach( $types as $k=>$v ) {
            $elements[] = [
                'label' => '<label class="control-label">' . $k . '</label>',
                'field' => '<input onchange="f_select(this);" type="checkbox" id="scope-' . $v . '" value="x-' . $v . '" checked />',
            ];
        }
        $umfang->setVar('elements', $elements, false);
        $umfang->setVar('grouped', true);
        $umfang->setVar('inline', true);
    
        $search = new rex_fragment();
        $elements = [
                'field' => '<input oninput="f_search(this.value)" type="text" class="form-control" placeholder="Search for..." id="search">',
                'right' => '<button onclick="f_reset()"class="btn btn-default" type="button"><i class="fa fa-close"></i></button>',
            ];
        $search->setVar('elements', [$elements], false);
    
        $row1 = new rex_fragment();
        $row1->setVar('content', [$umfang->parse('core/form/checkbox.php'),$search->parse('core/form/input_group.php')], false);
        
        $row2 = new rex_fragment();
        $row2->setVar('content', array_map(function($v){return "&lt;i class=\"$v $v-«name»\"&gt;&lt;/i&gt;";},$types), false);
    
        $section = new rex_fragment();
        $content = '<div ondblclick="f_copy(event)" class="'.implode(' ',array_map(function($v){return "x-$v";},$types)).'" id="iconlist">';
        foreach( $icons as $k=>$v ) {
            $type = implode(' ',$v['type']);
            $name = implode(',',$v['name']);
            $class = implode('</span><br><span>',$v['class']);
            $content .= "<div class=\"panel panel-default $type\" data-search=\"$name\"><div class=\"panel-heading\"><small>$k</small>{$v['html']}</div><div class=\"panel-body\"><span>$class</span></div></div>";
        }
        $content .= '</div>';
        $section->setVar('heading', $row1->parse('core/page/grid.php').$row2->parse('core/page/grid.php'), false);
        $section->setVar('body',$content,false);
        $html = $section->parse('core/page/section.php');
    
        rex_dir::create($this->getCachePath());
        file_put_contents($filename, $html);
    }
    echo $html;
    ?>
    <style>
        #iconlist { display: flex; flex-wrap: wrap; justify-content: center;}
        #iconlist .panel { display: none; margin: 1em; width: 14em; font-size: 1em; }
        #iconlist .panel-heading { font-size: 2rem; text-align: center; position: relative; }
        #iconlist .panel-body { padding: 0.1rem 0.2rem; }
        #iconlist small { padding: 0.1rem; position: absolute; top:0; left:0; font-size: 1rem; }
        <?= implode(',',array_map(function($v){return "#iconlist.x-$v .panel.x-$v";},$types)) ?> { display: block; }
    </style>
    <script>
    var iconlist = document.getElementById('iconlist');
    var search = document.getElementById('search');
    var typeList = ['<?= implode('\',\',$types) ?>'];
    function f_select( node ) { iconlist.classList.toggle(node.value,node.checked); }
    function f_reset() { search.value = ''; f_search( search.value ); }
    function f_search( pattern ) {
        if( pattern ) {
            for( var node of iconlist.children ) {
                var hidden = !node.dataset.search.includes(pattern.toLowerCase());
                if( node.classList.contains('hidden') != hidden ) node.classList.toggle('hidden',hidden);
            }
            return;
        }
        for( var node of iconlist.children ) { if( node.classList.contains('hidden') ) node.classList.remove('hidden'); }
    }
    function f_copy( event ) {
        var target = event.target;
        if( target.tagName.toLowerCase() != 'span' ) return;
        for( var type of typeList ) {
            if( target.innerText.startsWith('.'+type) ) {
                var node = document.createElement('textarea');
                node.value = '<i class="'+type+' '+target.innerText+'"></i>';
                node.style = {position: 'absolute', left: '-9999px'};
                document.body.appendChild(node);
                node.select();
                document.execCommand('copy');
                alert('HTML for "'+target.innerText+'" copied to clipboard');
                document.body.removeChild(node);
                event.preventDefault();
                return;
            }
        }
    }
    </script>
  3. Modify YForm Table Manager column content for display

    You can manipulate the value of a column before it is displayed in the Table Manager overview using setColumnFormat with a 'custom' callback. This allows combining the current column value with values from other columns in the same row.
    if (rex::isBackend()) {
        rex_extension::register('YFORM_DATA_LIST', function( $ep ) {
            if ($ep->getParam('table')->getTableName() == "gewuenschte_tabelle") {
                $list = $ep->getSubject();
                $list->setColumnFormat(
                    'title', // Column to target
                    'custom', // Keyword for custom function
                    function($a) {
                        // $a['value'] is the current column value
                        // $a['list']->getValue('xyz') gets value of another column
                        return $a['value'] . " " . $a['list']->getValue('xyz');
                    }
                );
            }
        });
    }
  4. Implement accessibility compliance documents

    To maintain transparency and trust, provide a dedicated Accessibility Statement (Barrierefreiheitserklärung) and a Feedback Mechanism. The statement should inform users about the current state of accessibility and be updated regularly to disclose known issues. The feedback mechanism should provide a simple way for users to report accessibility barriers.
    <footer
      <a href="/barrierefreiheitserklaerung">Barrierefreiheitserklärung</a>
      <a href="/feedback">Feedback zur Barrierefreiheit geben</a>
    </footer>
  5. Structure REDAXO categories for multi-domain best practices

    To maintain a clean multi-domain architecture in REDAXO, use root categories for each domain. Follow these structural guidelines:

    1. Use the start article of the root category as the Mountpoint article (the entry point for the domain).
    2. The start article of the root category can also serve as the homepage, though it is not mandatory.
    3. Create any number of categories and articles beneath the Mountpoint article.
    4. To create a specific landing page for a different domain within a website's structure, you can place Mountpoint articles from other domains beneath a root category.
  6. Access created record details after YForm submission

    To perform actions or display information about a record immediately after it is created in the frontend, check for the send object parameter and the absence of warning after calling getForm().
    $yform = new rex_yform();
    
    // Must call getForm() first
    echo $yform->getForm();
    
    if ($yform->getObjectparams('send') && !$yform->getObjectparams('warning')) {
        // Access the value pool of the created record
        dump($yform->getObjectparams('value_pool'));
    }
  7. Integrate the Icon Overview page into a REDAXO Service Addon

    To integrate the icon overview script into a service addon (e.g., a 'project' addon), follow these steps:

    1. Save the script: Place the icons.php file in «domain»/redaxo/src/addons/project/pages/icons.php.
    2. Register in index.php: Ensure the index.php in the pages directory includes the current page subpath.
    3. Configure package.yml: Add the page definition to your addon's package.yml to make it visible in the backend menu.
    // In «domain»/redaxo/src/addons/project/pages/index.php
    <?php  
    echo rex_view::title('AddOn: project');  
    rex_be_controller::includeCurrentPageSubPath();  
    ?>
    # In package.yml
    page:
      title: 'Projekt-Addon'
      perm: project[]
        subpages:
            icons:
               title: 'Icons'
               icon: fa fa-table
  8. Establish a logical heading hierarchy

    Use heading tags <h1> through <h6> in a strict logical order to define the information architecture of the page. Avoid skipping levels to ensure screen reader users can understand the relationship between sections.
    <h1>Page Main Title</h1>
    <h2>Section Subtitle</h2>
    <h3>Subsection Title</h3>
  9. Display FOR calendar events in a REDaxo fragment

    To display the retrieved calendar entries, instantiate the data retrieval class, pass the results to a rex_fragment, and parse a PHP template file (e.g., located at theme/private/fragments/forcal_terminliste.php).
    // In the module
    $my_forcal = new my_forcal();
    $res = $my_forcal->get_entries();
    
    $fragment = new rex_fragment();
    $fragment->setVar('termine', $res);
    echo $fragment->parse('forcal_terminliste.php');
    <!-- In theme/private/fragments/forcal_terminliste.php -->
    <ul>
    <?php foreach ($this->termine as $i => $item) : ?>
        <li>
            <a href="event-detail.html">
                <div class="event__date"><?= $item->get_formatted_date_and_time() ?></div>
                <div class="event__category"><?= $item->sec_name ?></div>
                <div class="event__title" style="color: <?= $item->color ?>"><?= $item->{'name_'.rex_clang::getCurrentId()} ?></div>
                <div class="event__subtitle"><?= $item->get_val('subtitle') ?></div>
                <div class="event__description"><?= $item->get_val('subline') ?></div>
            </a>
            <?= $item->get_val('description') ?>
            <dl class="uk-description-list">
               <?php if ($item->get_val('info_meetingpoint')) : ?>
                 <dt></dt><dd><?= $item->get_val('info_meetingpoint') ?></dd>
               <?php endif ?>
               <!-- Repeat for info_duration, info_price, info_registration, info_contact -->
               <?php if ($item->get_val('info_1_value')) : ?>
                 <dt><?= $item->get_val('info_1_label') ?></dt>
                 <dd><?= $item->get_val('info_1_value') ?></dd>
               <?php endif ?>
             </dl>
             <img src="/images/content/<?= $item->get_img() ?>" alt="">
        </li>
    <?php endforeach ?>
    </ul>
  10. Embed YForm tables and forms into a custom addon

    To integrate YForm table management into a custom addon, create a wrapper script (e.g., pages/yform.php) that includes the YForm data_edit.php page. This allows you to define YForm management pages within your addon's package.yml and control visibility and styling via configuration properties.
    <?php
    /**
     * Wrapper to call YForm Table Manager pages as native addon pages.
     * Save this file as pages/yform.php in your addon.
     */
    
    $yform = $this->getProperty('yform', []);
    $yform = $yform[rex_be_controller::getCurrentPage()] ?? [];
    
    if (isset($yform['table_name'])) {
        $table_name = $yform['table_name'];
        if (is_subclass_of($table_name, rex_yform_manager_dataset::class)) {
            $table_name = $table_name::table()->getTableName();
        } elseif (!str_starts_with($table_name, rex::getTablePrefix())) {
            $table_name = rex::getTable($table_name);
        }
    } else {
        $table_name = '';
    }
    
    $table_name = rex_request('table_name', 'string', $table_name);
    $show_title = true === ($yform['show_title'] ?? false);
    $wrapper_class = $yform['wrapper_class'] ?? '';
    
    if ('' !== $table_name) {
        $_REQUEST['table_name'] = $table_name;
    }
    
    if (!$show_title) {
        rex_extension::register(
            'YFORM_MANAGER_DATA_PAGE_HEADER',
            static function (rex_extension_point $ep) {
                if ($ep->getParam('yform')->table->getTableName() === $ep->getParam('table_name')) {
                    return '';
                }
            },
            rex_extension::EARLY, ['table_name' => $table_name],
        );
    }
    
    if ('' !== $wrapper_class) {
        echo '<div class="',$wrapper_class,'">';
    }
    
    if (is_file(rex_path::addon('yform', 'pages/manager.data_edit.php'))) {
        include rex_path::addon('yform', 'pages/manager.data_edit.php'); // YForm 5
    } else {
        include rex_path::plugin('yform', 'manager', 'pages/data_edit.php'); // YForm 4
    }
    
    if ('' !== $wrapper_class) {
        echo '</div>';
    }