[Drupal] Perfect views block configuration for multilingual node support

Objectives

  • We want to add on a node page layout a block which uses a field from the currently rendered content (node).
  • We want this block to be translated the same as the currently rendered entity.
  • As a anonymous user, we want it to respect the published settings of the currently rendered entity.
  • As an admin, we want to be able to see this block if the content is unpublished.
  • We want to support cases when a content is not translated. The default Drupal behaviour is to use the default translation.

Configuration

Screenshot
Configuration screenshot.
  • Do not use Published (= Yes) setting.

    Views will remove itself results on which you don’t have permission—unless you have disabled access checks in the Query settings parameter of the view. Also, as an admin, you will still be able to see the block if the content is unpublished.

  • Set Default translation (= True).

    Views will retrieve the default translation, which is the default result when you attempt to go on a node page which has no translation.

  • Set Rendering Language to Interface text language selected for page.

    Views will attempt to translate the content. If no content translation is found, default translation will be used.

  • Contextual filter set to Content: ID with Content ID from URL as default value.
  • Hide block if the view output is empty.

    No need to keep this block if there is no output (or keep it if you need it anyway).

Additional information

  • Tested on Drupal 8.5.5 on simplytest.me.

[Drupal] Custom scrollbars with Overlay Scrollbars library

The road to custom scrollbars

Reminiscing.

I am currently working on a website for a video game reviews project I have had with an old friend of mine (FuuDoh). Obviously, I have switched to Drupal to get a result quicker than what I would get with the PHP only solution I have been working on a few years ago (it was fun though).

One of the main problems I have came across while theming was to give a consistent look across various web browsers (and now devices—mobile and tablets were not as much a concern as nowadays). Bootstrap was not as popular as it is now and it saved a lot of my time for my current project. But it did not fix one of the other problems I am still experiencing today: scrollbars.

I hate how they cannot be themed natively. In an overflow: auto setting, they push all the content by their width when they appear, potentially changing the height of the content; which is not very elegant in my opinion. You cannot change their size, nor their color.

With the big grow in the recent years in the JS scene, we have now good enough JS libraries which gives developers means to achieve better, cross-browser, cross-platform scrollbars.

I have checked a Drupal 8 module, scrollbar, which unfortunately does not work well (or was misconfigured). The bars were not refreshing when the browser window is resized. Maybe I’ll post an issue on the Drupal module tracker, but it does not seem to be the fault of the module. But there was one big drawback with the library used by this module (jScrollPane): the scroll behaviour is not the native browser’s scroll behaviour. You will not get any ease in the scroll animation (or maybe you could have it by configuring the library).

Then I have searched a little and found two candidates:

Both are JS libraries which use native browser’s capabilities to scroll the content; you will not feel anything different between normal scrollbars and those custom ones, which is perfect! The first library is currently (May 2018) used on Twitch.tv. I have not tested it, but it claims to be cross browser. But it also claims that there may be some performance impact when used on the body element. That is why I chose OverlayScrollbars, which is recommended by simplebar in its documentation by the way.

Relieved.

After having spent some time trying to figure out why it did not work, I have finally got it working nicely with the bootstrap+sass theme! The implementation is in fact trivial for a Drupal themer but here are the steps to achieve it.

Steps

  1. Download the OverlayScrollbars library (link to releases) and extract the files to the themes/custom/THEMENAME/libraries/OverlayScrollbars directory (create necessary directories). This OverlayScrollbars folder must contain the css and js directories which came from the zip (remove any wrapping directory).
  2. Create a file named trigger.js at this location: themes/custom/THEMENAME/js/overlayscrollbars/trigger.js
  3. Add this to the file:
    (function ($) {
        //The passed argument has to be at least a empty object or a object with your desired options
        $('body').overlayScrollbars({ });
        // .overlayScrollbars({ }) must be appended on any single element you want a scrollbar.
        $('nav.article-list').overlayScrollbars({ });
    }(jQuery));
    
    
  4. The THEMENAME.libraries.yml of your subtheme may look like this (the overlayscrollbars is what we care about):
    framework:
      css:
        theme:
          css/style.css: {}
      js:
        bootstrap/assets/javascripts/bootstrap/affix.js: {}
        bootstrap/assets/javascripts/bootstrap/alert.js: {}
        bootstrap/assets/javascripts/bootstrap/button.js: {}
        bootstrap/assets/javascripts/bootstrap/carousel.js: {}
        bootstrap/assets/javascripts/bootstrap/collapse.js: {}
        bootstrap/assets/javascripts/bootstrap/dropdown.js: {}
        bootstrap/assets/javascripts/bootstrap/modal.js: {}
        bootstrap/assets/javascripts/bootstrap/tooltip.js: {}
        bootstrap/assets/javascripts/bootstrap/popover.js: {}
        bootstrap/assets/javascripts/bootstrap/scrollspy.js: {}
        bootstrap/assets/javascripts/bootstrap/tab.js: {}
        bootstrap/assets/javascripts/bootstrap/transition.js: {}
    overlayscrollbars:
      js:
        libraries/OverlayScrollbars/js/jquery.overlayScrollbars.js: {}
        js/overlayscrollbars/trigger.js: {}
      dependencies:
        - core/jquery
    
  5. Add an import directive in the _overrides.scss, right after the other imports (remember, I am using sass; if you use less, you have to adapt it):
    // Custom scrollbars library.
    @import "../libraries/OverlayScrollbars/css/OverlayScrollbars.css";
    
  6. Compile the scss.
  7. Add this hook in your THEMENAME.theme:
    /**
     * Implements hook_page_attachments_alter().
     *
     * {@inherit}
     */
    function THEMENAME_page_attachments_alter(array &$page) {
      $page['#attached']['library'][] = 'THEMENAME/overlayscrollbars';
    }
    
    
  8. Rebuild the Drupal cache: drush cr.

Additional information

  • Tested on Drupal 8.5.3.
  • Tested on official standard Bootstrap theme, sass flavor.
  • The original install guide is here: https://kingsora.github.io/OverlayScrollbars/#!overview. You will also get a list of options on the original guide.
  • You can also put the content of the trigger.js file directly into the twig (not tested though); html.html.twig would be a good candidate.

[Drupal] Change CKEditor behaviour for line breaks (br or p)

Here is a little code snippet to be added in a .module file.

/**
 * Implements hook_editor_js_settings_alter().
 *
 * {@inheritdoc}
 */
function MODULE_editor_js_settings_alter(array &$settings) {
  foreach ($settings['editor']['formats'] as $name => $value) {
    // Use 
instead of

on enter. // See: https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR.html#property-ENTER_BR. // CKEditor natively converts double
into

on rendering. $settings['editor']['formats'][$name]['editorSettings']['enterMode'] = '2'; } }

Additional Information

  • Instructions made on Drupal 8.5.x.
  • Will break text align buttons in the CKEditor toolbar: Issue on Drupal.org
  • The default behaviour of CKEditor is the following:
    • Enter: inserts a paragraph directly in the WYSIWYG,
    • Shift + Enter: inserts a line break.
  • The new behaviour of CKEditor with the hook will be:
    • Enter: inserts a single line break (br),
    • 2 × Enter: inserts two line breaks which will be converted into a paragraph when rendered.

[Drupal] Make the Search API Page search block appear in Bootstrap markup

When using Search API Page, I needed to theme the search form in the bootstrap structure. The cleanest answer I have found is to add some variables to trigger the bootstrap overrides on the search form.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function MYMODULE_form_search_api_page_block_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // We need this to make our search_api_page_block_form rendered in the bootstrap fashion
  // This code was inspired by \Drupal\bootstrap\Plugin\Form\SearchBlockForm
  $form['actions']['submit']['#icon_only'] = true;
  $form['keys']['#input_group_button'] = true;
}

Additional Information

  • Instructions made on Drupal 8.3.x (probably, haha).

[Drupal] VM + XDebug + Netbeans

Hey! Long time no see! Heehee.

I am just dropping a note here about the basic steps of how to install XDebug for a specific setup.

Here is my configuration:

  • host:
    • OS: Windows 7 64 bits
    • IP: 192.168.0.3
    • IDE: Netbeans 8.2
  • guest:
    • OS: Debian Jessie 64 bits in VirtualBox
    • IP: 192.168.0.4 (bridged network in VirtualBox settings)
    • standard Apache2, PHP 5, MySQL from official repositories

The idea is: I want to use the stepper on a website located inside a virtual machine from an IDE on the host machine.

Step by step

XDebug installation

  1. On the guest machine, get the phpinfo() output. If you have no idea how to do this quickly:
    1. From the command line, run php --info > ~/tempphpinfo.txt (you can safely delete this file later).
    2. Open the file: gedit ~/tempphpinfo.txt.
    3. Copy all the contents.
  2. Go to https://xdebug.org/wizard.php and follow the instructions: you will have to paste the contents of the phpinfo obtained earlier. The wizard will tell you exactly how to build xdebug. Make sure you are in the correct folder when running the phpize command.
  3. Locate the xdebug.ini files (named 20-xdebug.ini on my Debian) on the guest machine:
    1. cd /etc/php5
    2. find | grep xdebug
  4. Edit one of the files listed by the last command and add those lines:
    ;zend_extension=[path to xdebug .so/.dll]
    zend_extension=/usr/lib/php5/20131226/xdebug.so
    xdebug.remote_enable=1
    xdebug.remote_handler=dbgp
    xdebug.remote_mode=req
    xdebug.remote_host=127.0.0.1
    xdebug.remote_port=9000
      
  5. sudo service apache2 restart, just in case.

Testing XDebug

We will create a small php file which will react to XDebug calls.

  1. Create a php file:
    • touch ~/testxdebug.php
    • gedit ~/testxdebug.php
    • Insert the following:
      
      
    • Save and close.
  2. Start the command line debugger: php ~/testxdebug.php. You may have a warning about the xdebug extension already started: you can ignore it or fix this later by commenting/removing the zend_extension=path/to/xdebug.so line you have added to the /etc/php5/cli/php.ini file (not the xdebug.ini files!).
  3. In the web browser, go to your website location, a Drupal 8 in my case and add an attribute at the end of the URL, e.g.: http://localhost/DrupalXDebug/web/index.php?XDEBUG_SESSION_START=mysession.
  4. In the terminal where you have run the php ~/testxdebug.php command, you should see a message like this one:

    connection established: Resource id #5

    If not, the configuration is wrong and check the steps again or the resources links at the end of this article.

Setup remote debugging

Until now, we have only ran the debugger (the little php script we have created earlier, which you can safely delete now) from inside the guest machine. We will configure xdebug to work with remote machines, in my case, the Windows host.

  1. Edit one of the xdebug.ini files you have edited earlier: replace the xdebug.remote_host value by the IP of the host, 192.168.0.3 in my case.
  2. sudo service apache2 restart, just in case.

Netbeans configuration

Now we can configure Netbeans to run the debugger.

  1. Open the website project.
  2. Right click on the project in the Projects list and click on Properties.
  3. Enter the URL of your website in Run Configuration > Project URL. In my case, it was like this:
    Screenshot from netbeans
    Where to set the website URL
  4. Click on the Advanced button.
  5. Set the path mappings. In my case:
    Screenshot of path mapping on Netbeans
    How to set path mappings

You should now be set for debugging with the stepper!

Run the debug

  1. Click on the Debug Project button in the top toolbar of Netbeans and it should launch automatically a browser with a session id already set!
  2. Put a breakpoint somewhere in the code.
  3. Each time a request is sent to the website, Netbeans will control the page generation.

Additional Information

[Drupal] Use admin_block and admin_block_content templates

This article is about the default admin_page, admin_block and admin_block_content templates which serves as the base of the content display on some system configuration pages such as the one on this image:

Screenshot of the configuration page on a Drupal 8 website
Screenshot of the configuration page on a Drupal 8 website

The code

 'admin_page',
      '#blocks' => [
        // Array of admin_block
        [
          'title' => $this->t('A block, on the left'),
          'position' => 'left',
          'content' => [
            '#theme' => 'admin_block_content',
            '#content' => [
              [
                'title' => 'Do something',
                'url' => Url::fromRoute('your_module.some.example.route'),
                'description' => $this->t('This will link to a route.')
              ],
              [
                'title' => 'Navigate to a nice blog',
                'url' => Url::fromUri('https://blog.dakwamine.fr'),
                'description' => $this->t('This will link to a URI, external in this case.')
              ],
              [
                'title' => 'Go to contents page',
                'url' => Url::fromUri('internal:/admin/content'),
                'description' => $this->t('Also works for internal URI.')
              ]
            ]
          ]
        ],
        [
          'title' => $this->t('Another block, on the right'),
          'position' => 'right',
          'content' => [
            '#theme' => 'admin_block_content',
            '#content' => [
              [
                'title' => 'Another link to a route',
                'url' => Url::fromRoute('your_module.another.route'),
                'description' => $this->t('Yup.')
              ],
              [
                'title' => 'Website example link',
                'url' => Url::fromUri('https://website.example'),
                'description' => $this->t('Goes on website.example.')
              ],
            ]
          ]
        ]
      ],
    ];

    // admin_block style (full width)
    $mainRenderArray[] = [
      '#theme' => 'admin_block',
      '#block' => [
        'title' => $this->t('Full width block'),
        'content' => [
          '#theme' => 'admin_block_content',
          '#content' => [
            [
              'title' => 'Another link',
              'url' => Url::fromUri('internal:/admin/people'),
              'description' => $this->t('A link to the people page.')
            ],
            [
              'title' => 'Another link again',
              'url' => Url::fromUri('https://website.example'),
              'description' => $this->t('Goes on website.example.')
            ],
          ]
        ],
      ],
    ];

    return $mainRenderArray;
  }

}

Explanation

If you use admin_page, you will be able to define the column in which the blocks dwell: left or right.

If you use directly admin_block, there is only a single block and it will span over the full width of the region.

So all in all, here is a pseudo hierarchy to better understand how it works:

(render array base)
│
├── admin_page
│   └── admin_block  <== Half width
│       └── admin_block_content
│
└── admin_block  <== Full width
    └── admin_block_content

Replace image URLs in WordPress posts from http to https

I just switched my website to https for testing purpose. But changing the website’s URL to https:// is not sufficient to enable a fully secure browsing experience, as images in posts are still loaded from http://. Or more precisely, most modern browsers (FF or Opera for instance) are now clever enough to guess that the current website is on https:// so its contents should also be loaded with the same protocol, but there may be a possibility that other less advanced browsers are not doing this smart guess.

There are two possibilities:

  • install and activate a plugin such as the SSL Insecure Content Fixer. It will replace on the fly the protocols to https. The advantage is that your contents are not edited in the database. The drawback is that your website may be a little slower. (sorry I don’t have metrics)
  • the other method is to replace the src="http:// references in the post_content field from the wp_posts table by src="https:// by using SQL. For example, in phpMyAdmin:
    1. make a backup of the wp_posts table (or even better, your database)
    2. execute this query:
      UPDATE `wp_posts` SET post_content = REPLACE( post_content,  ' src="http://', ' src="https://' ) WHERE 1;

[Drupal] Base hook schema example to put in .install

Introduction

Delighted

hook_schema is one of those methods where you are happy to have a usable documentation on the api website. It is pretty much self-explanatory and you don’t have to look for hours on third parties websites to get some usable information. 🙂

  • hook_schema on api.drupal.org and don’t forget to select your Drupal version!
  • $DRUPAL_ROOT\core\lib\Drupal\Core\Database\database.api.php: another file which describes the Database API array structure!
  • Schema Reference on drupal.org for D7. Yeah, there is no D8 version of the docs… Hopefully, nothing has changed between D7 and D8.

After some research, I found the files which are responsible for schema to SQL statement conversion:

File location Method name
$DRUPAL_ROOT\core\includes\schema.inc drupal_install_schema (8.2.x)
$DRUPAL_ROOT\core\includes\database.inc (deprecated) db_create_table (8.2.x) (deprecated)
$DRUPAL_ROOT\core\lib\Drupal\Core\Database\Schema.php protected createTable (8.2.x)
$DRUPAL_ROOT\core\lib\Drupal\Core\Database\Driver\{mysql,sqlite,pgsql}\Schema.php
$DRUPAL_ROOT\core\lib\Drupal\Core\Database\Driver\{mysql,sqlite,pgsql}\Schema.php

The code

 'Stores the history of content views blablabla.',
    'fields' => [
      'uid' => [
        'description' => 'User id.',
        'type' => 'int',
        'length' => 10,
        'unsigned' => true,
        'not null' => true
      ],
      'content_id'=> [
        'description' => 'Id of the content.',
        'type' => 'int',
        'length' => 11,
        'not null' => true
      ],
      'access_time'=> [
        'description' => 'Access timestamp.',
        'type' => 'int',
        'length' => 10,
        'unsigned' => true,
        'not null' => true
      ]
    ]
  ];
  
  return $schema;
}

[Drupal] Table render array example structure

Result

Rendered table from a render array
Rendered table from a render array

The code

  public function controllerCallbackExample() {
    return [
      '#type' => 'table',
      '#caption' => 'This is a caption. Use a translated string here.',
      '#header' => [
        'Header 1', 'Header 2', 'Header 3', 'Header 4'
      ],
      '#rows' => [
        [
          'Row 1 Cell 1', 'Row 1 Cell 2', 'Row 1 Cell 3', ['data' => 'Row 1 Cell 4']
        ],
        [
          'class' => ['second-row-class', 'may contain spaces but dont use them'],
          'data' => [
            'Row 2 Cell 1',
            [
              'data' => 'Row 2 Cell 2'
            ],
            [
              'colspan' => 2,
              'data' => 'Row 3 Cell 3 & 4',
              'style' => 'background-color: pink; text-align: center'
            ]
          ],
          'style' => 'font-weight: bold'
        ],
        [
          'data' => [
            'Row 3 Cell 1', 'Row 3 Cell 2', 'Row 3 Cell 3', 'Row 3 Cell 4'
          ],
          'no_striping' => FALSE // Not working in 8.2.x, may be fixed later
        ]
      ],
      '#footer' => [
        [
          'data' => [
            'Footer 1',
            [
              'colspan' => 2,
              'data' => 'Footer 2 with colspan = 2',
              'style' => 'text-align: inherit'
            ],
            [
              'data' => 'Footer 4',
              'style' => 'text-align: inherit'
            ]
          ],
          'style' => 'text-align: center'
        ],
        [
          [
            'colspan' => 4,
            'class' => 'second-footer-row',
            'data' => 'Another Footer Line',
            'style' => ['background-color: purple;', 'text-align: center;', 'color: white;']
          ]
        ]
      ]
    ];
  }

Additional Information

[Drupal] Create an event subscriber

Instructions

I will be creating a dedicated module using Drupal Console for this event subscriber. But feel free to use your own if you already have one! (don’t forget to backup)

  1. Access the drupal root folder in command line.
  2. Generate a new module:
    dakwamine@debian-drupal:/var/www/html/drupal$ drupal generate:module
    
     Enter the new module name:
     > Test Module          
    
     Enter the module machine name [test_module]:
     > 
    
     Enter the module Path [/modules/custom]:
     > 
    
     Enter module description [My Awesome Module]:
     > 
    
     Enter package name [Custom]:
     > 
    
     Enter Drupal Core version [8.x]:
     > 
    
     Do you want to generate a .module file (yes/no) [yes]:
     > no
    
     Define module as feature (yes/no) [no]:
     > 
    
     Do you want to add a composer.json file to your module (yes/no) [yes]:
     > no
    
     Would you like to add module dependencies (yes/no) [no]:
     > 
    
    
     Do you confirm generation? (yes/no) [yes]:
     > 
    
    Generated or updated files
     Site path: /var/www/html/drupal
     1 - modules/custom/test_module/test_module.info.yml
    
  3. Generate the event subscriber. We will subscribe to the kernel.request event:
    dakwamine@debian-drupal:/var/www/html/drupal$ drupal generate:event:subscriber
     Enter the module name [admin_toolbar]:
     > test_module
    
     Enter the service name [test_module.default]:
     > test_module.event_subscriber_example
    
     Class name [DefaultSubscriber]:
     > TestModuleExampleSubscriber
    
     
    Type the event name or use keyup or keydown.
    This is optional, press enter to continue
    
     Enter event name [ ]:
     > kernel.request
    
     Callback function name to handle event [kernel_request]:
     > 
    
     Enter event name [ ]:
     > 
     Do you want to load services from the container (yes/no) [no]:
     > 
    
    
     Do you confirm generation? (yes/no) [yes]:
     > 
    
    Generated or updated files
     Site path: /var/www/html/drupal
     1 - modules/custom/test_module/src/EventSubscriber/TestModuleExampleSubscriber.php
     2 - modules/custom/test_module/test_module.services.yml
     cache:rebuild
    
     Rebuilding cache(s), wait a moment please.
    
    
     [OK] Done clearing cache(s).
    
    
    
  4. Current state:
    • .
      ├── src
      │   └── EventSubscriber
      │       └── TestModuleExampleSubscriber.php
      ├── test_module.info.yml
      └── test_module.services.yml
      
    • name: Test Module
      type: module
      description: My Awesome Module
      core: 8.x
      package: Custom
      
      
    • services:
        test_module.event_subscriber_example:
          class: Drupal\test_module\EventSubscriber\TestModuleExampleSubscriber
          arguments: []
          tags:
            - { name: event_subscriber }
      
      
    • 
      
  5. Install the module using either drupal module:install or drush pm-enable:
    dakwamine@debian-drupal:/var/www/html/drupal$ drupal module:install test_module
     Installing module(s) test_module
    
    
     [OK] The following module(s) were installed successfully: test_module
    
    
     cache:rebuild
    
     Rebuilding cache(s), wait a moment please.
    
    
     [OK] Done clearing cache(s).
    
    
  6. Now, refresh your web page and on each request received server side, a message will tell you the event occurred!

Additional Information

  • Instructions made on Drupal 8.2.x.
  • Using Drupal Console, you can easily inject services such as the current_user one by answering yes when it asks if you want to load services from the container. Please check this article about service dependency injection: [Drupal] Service dependency injection in a service type class.
  • After editing your files, remember to run a cache rebuild either with drupal cache:rebuild or drush cache-rebuild.
  • Events documentation entry point: Events on Drupal 8. You will find a few core events here.
  • The callback function’s name can be changed to whatever you want.