So I have been extracting a lot of code recently from several projects that I\’ve done over 2020 and I am converting those independent solutions into plugins so that I can reuse them over time. Usually, my solutions allow developers to easily modify the styling and functionality.
In order to also understand the plugin I will go over it step by step, it will also help you to build your own plugin solutions.
Requirements
- SCSS installed with npm
- Some icons for toggle up and down. I usually grab them from fontawesome.
This is a quick overview on how this setup works
- Create a plugin file and folder structure
In order for WordPress to read the plugin, you\’ll need some info. Plus we need SCSS/CSS and Javascript files.
- Create Custom Post Type & Taxonomy for the FAQs
In order to modify the data in WordPress, and also structure the content.
- Query & Display Custom Post Type & Taxonomy
WP_Query as well as shortcode options in order to handle the display
1. Create Plugin Files
First let\’s create a folder in your plugins folder. Name it faq. Then create inside this folder a file called faq.php. Start by adding the following content. Here you can specify some information about the plugin and how it looks inside the admin plugin\’s interface.
/** * @wordpress-plugin * Plugin Name: WP FAQ Playground * Plugin URI: https://hkvlaanderen.com * Description: Easy to modify plugin for displaying FAQs * Version: 1.0.0 * Author: Hendrik Vlaanderen * Author URI: https://blog.hkvlaanderen.com * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Text Domain: wp-faq-playground */ defined( \'ABSPATH\' ) or die( \'Direct script access disallowed.\' );
Then, let\’s create the folder structure as in the image. This helps with easy file management.
2. Create Custom Post Type and Taxonomy
In the faq.php file add the following code to initialise the custom post type and the taxonomy that belongs to it.
if ( ! function_exists(\'register_playground_faq\') ) { // Register Custom Post Type function register_playground_faq() { $labels = array( \'name\' => _x( \'FAQ\', \'Post Type General Name\', \'faq-playground\' ), \'singular_name\' => _x( \'FAQ\', \'Post Type Singular Name\', \'faq-playground\' ), \'menu_name\' => __( \'FAQ\', \'faq-playground\' ), \'name_admin_bar\' => __( \'FAQ\', \'faq-playground\' ), \'archives\' => __( \'FAQ\', \'faq-playground\' ), \'attributes\' => __( \'Item Attributes\', \'faq-playground\' ), \'parent_item_colon\' => __( \'Parent Item:\', \'faq-playground\' ), \'all_items\' => __( \'All FAQ Items\', \'faq-playground\' ), \'add_new_item\' => __( \'Add New FAQ Item\', \'faq-playground\' ), \'add_new\' => __( \'Add FAQ Item\', \'faq-playground\' ), \'new_item\' => __( \'New FAQ Item\', \'faq-playground\' ), \'edit_item\' => __( \'Edit FAQ Item\', \'faq-playground\' ), \'update_item\' => __( \'Update FAQ Item\', \'faq-playground\' ), \'view_item\' => __( \'View FAQ Item\', \'faq-playground\' ), \'view_items\' => __( \'View FAQ Items\', \'faq-playground\' ), \'search_items\' => __( \'Search FAQ Items\', \'faq-playground\' ), \'not_found\' => __( \'Not found\', \'faq-playground\' ), \'not_found_in_trash\' => __( \'Not found in Trash\', \'faq-playground\' ), \'featured_image\' => __( \'Featured Image\', \'faq-playground\' ), \'set_featured_image\' => __( \'Set featured image\', \'faq-playground\' ), \'remove_featured_image\' => __( \'Remove featured image\', \'faq-playground\' ), \'use_featured_image\' => __( \'Use as featured image\', \'faq-playground\' ), \'insert_into_item\' => __( \'Insert into item\', \'faq-playground\' ), \'uploaded_to_this_item\' => __( \'Uploaded to this item\', \'faq-playground\' ), \'items_list\' => __( \'Items list\', \'faq-playground\' ), \'items_list_navigation\' => __( \'Items list navigation\', \'faq-playground\' ), \'filter_items_list\' => __( \'Filter items list\', \'faq-playground\' ), ); $args = array( \'label\' => __( \'FAQ\', \'faq-playground\' ), \'labels\' => $labels, \'supports\' => array( \'title\', \'editor\', \'excerpt\' ), \'hierarchical\' => false, \'public\' => true, \'show_ui\' => true, \'show_in_menu\' => true, \'menu_position\' => 5, \'menu_icon\' => \'dashicons-businessman\', \'show_in_admin_bar\' => true, \'show_in_nav_menus\' => true, \'can_export\' => true, \'has_archive\' => true, \'exclude_from_search\' => true, \'publicly_queryable\' => false, \'query_var\' => true, \'capability_type\' => \'page\', \'rewrite\' => array(\'slug\' => \'faq\') ); register_post_type( \'faq\', $args ); } add_action( \'init\', \'register_playground_faq\', 0 ); } // Register Custom Taxonomy function register_faq_playground_category() { $labels = array( \'name\' => _x( \'FAQ Categories\', \'Taxonomy General Name\', \'sanatio\' ), \'singular_name\' => _x( \'FAQ Category\', \'Taxonomy Singular Name\', \'sanatio\' ), \'menu_name\' => __( \'FAQ Category\', \'sanatio\' ), \'all_items\' => __( \'All Categories\', \'sanatio\' ), \'parent_item\' => __( \'Parent Item\', \'sanatio\' ), \'parent_item_colon\' => __( \'Parent Item:\', \'sanatio\' ), \'new_item_name\' => __( \'New Item Name\', \'sanatio\' ), \'add_new_item\' => __( \'Add New Item\', \'sanatio\' ), \'edit_item\' => __( \'Edit Item\', \'sanatio\' ), \'update_item\' => __( \'Update Item\', \'sanatio\' ), \'view_item\' => __( \'View Item\', \'sanatio\' ), \'separate_items_with_commas\' => __( \'Separate items with commas\', \'sanatio\' ), \'add_or_remove_items\' => __( \'Add or remove items\', \'sanatio\' ), \'choose_from_most_used\' => __( \'Choose from the most used\', \'sanatio\' ), \'popular_items\' => __( \'Popular Items\', \'sanatio\' ), \'search_items\' => __( \'Search Items\', \'sanatio\' ), \'not_found\' => __( \'Not Found\', \'sanatio\' ), \'no_terms\' => __( \'No items\', \'sanatio\' ), \'items_list\' => __( \'Items list\', \'sanatio\' ), \'items_list_navigation\' => __( \'Items list navigation\', \'sanatio\' ), ); $args = array( \'labels\' => $labels, \'hierarchical\' => true, \'public\' => false, \'show_ui\' => true, \'has_archive\' => false, \'show_admin_column\' => true, \'show_in_nav_menus\' => true, \'show_tagcloud\' => true, \'rewrite\' => false, ); register_taxonomy( \'faq_category\', array( \'faq\' ), $args ); } add_action( \'init\', \'register_faq_playground_category\', 0 );
3. Query and Display Custom Post Type and Taxonomy
Next up, we should see the FAQ and Taxonomy in the wordpress admin panel. Let\’s add a few so we can display them later.
Now, let\’s make a shortcode to display them on a page that you want to showcase them on. Here you go, add this to your faq.php, and also add one file in the templates folder. I called it list.php.
require_once \'templates/list.php\'; // references the get_faq_loop function below add_shortcode(\'faq_items\', \'faq_show_loop\'); function faq_show_loop(){ ob_start(); get_faq_loop(); $html = ob_get_contents(); ob_end_clean(); return $html; }
Add this to list.php
<?php function get_faq_loop(){ ?> <div class=\"faq-blocks\"> <?php $taxonomy = \'faq_category\'; $orderby = \'name\'; $show_count = 0; // 1 for yes, 0 for no $pad_counts = 0; // 1 for yes, 0 for no $hierarchical = 1; // 1 for yes, 0 for no $title = \'\'; $empty = 0; $args = array( \'taxonomy\' => $taxonomy, \'orderby\' => $orderby, \'show_count\' => $show_count, \'pad_counts\' => $pad_counts, \'hierarchical\' => $hierarchical, \'title_li\' => $title, \'hide_empty\' => $empty ); $all_categories = get_categories( $args ); ?> <?php foreach($all_categories as $cat) : if(!$cat->category_parent == 0) { // if the parent isnt zero it must be a child. // we dont do anything } else { // if the parent == 0 it must be a parent // output the parent posts faq_category_loop($cat, \'parent\'); } endforeach; ?> </div> <?php } function faq_category_loop($cat){ ?> <div class=\"faq-block\" id=\"<?= $cat->slug ?>\"> <div class=\"category-block\"> <h2><?php echo $cat->name ?></h2> </div> <?php $args = array( \'post_type\' => \'faq\', \'post_status\' => \'publish\', \'posts_per_page\' => -1, \'posts_per_archive_page\' => -1, \'orderby\' => \'menu_order\', \'order\' => \'ASC\', \'tax_query\' => array( array ( \'taxonomy\' => \'faq_category\', \'field\' => \'slug\', \'terms\' => $cat->slug, \'include_children\' => false ) ), ); $query = new WP_Query($args); ?> <div class=\"faq-items\"> <?php while ($query->have_posts()) : $query->the_post(); ?> <div class=\"faq-item\"> <button class=\"toggle-button\" data-toggle=\"collapse\" data-target=\".collapse.collapse-<?= get_the_ID() ?>\" data-text=\"Collapse\" > <?php the_title() ?> </button> <div class=\"block collapse collapse-<?= get_the_ID() ?>\"> <div class=\"block__content\"> <?php the_content() ?> </div> </div> </div> <?php endwhile; ?> </div> </div> <?php wp_reset_postdata(); }
Also we can start adding the enqueue methods for including the js and css files. Add this also to faq.php.
function faq_playground_include_scripts(){ $version = 1.0; wp_enqueue_style( \'faq-styles\', plugins_url(\'faq\') .\'/css/faq.css\', array(), $version ); wp_enqueue_script(\'faq-script\', plugins_url(\'faq\') . \'/js/faq.js\', array(\'jquery\'), $version, true); } add_action( \'wp_enqueue_scripts\', \'faq_playground_include_scripts\', 10);
Start styling & Interactivity
I\’ve added in the scss folder a file called faq.scss with the following content. Note the following, there are several variables on the top deciding on the color and font. Also, I create a mixin for the breakpoints. This is used in several points in the file to style differently for mobile. Note, also that the icons are referenced here as SVGs in the icons folder. You\’ll need to run sass for this SCSS to turn it into css. Here is the sass command. It watches the scss folder and exports it into css.
sass --watch scss:css
$bebas-bold:\'sans-serif\'; $dark-forest-green:#333; $deep-aqua-new:blue; $light-green:green; @mixin bp($point) { $bp-phone: \"(max-width: 768px)\"; $bp-tablet: \"(min-width: 768px) and (max-width:1024px)\"; $bp-desktop: \"(min-width: 1024px)\"; @if $point == phone { @media #{$bp-phone} { @content; } } @else if $point == tablet { @media #{$bp-tablet} { @content; } } @else if $point == desktop { @media #{$bp-desktop} { @content; } } } .faq { margin-top:30px; position: relative; @include bp(desktop){ margin-top:95px; } } #faq { .background__headline { top:180px; font-size: 330px; } } $category-block-width:340/1080*100%; $faq-items-width:524/1080*100%; .block.collapse { display: block; max-height: 0px; overflow: hidden; transition: max-height 1.5s cubic-bezier(0, 1, 0, 1); &.show { max-height: 99em; transition: max-height 1.5s ease-in-out; } } .toggle-button { padding-left:0; padding-bottom:10px; width: 100%; border: 0; background: transparent; text-align: left; font-family: $bebas-bold; font-size: 20px; font-weight: bold; font-stretch: normal; font-style: normal; line-height: 0.9; letter-spacing: normal; color:$dark-forest-green; text-transform:uppercase; padding-right:20px; position: relative; &.active { &:after { transform:rotateX(180deg); background-image:url(\"../icons/chevron-down-aqua.svg\") } } &.active, &:hover { color: $deep-aqua-new; border-bottom:1px solid $deep-aqua-new; } &:after { content:\"\"; position:absolute; right:0; top:7px; background-repeat: no-repeat; background-size:8px; background-image:url(\"../icons/chevron-down-dark.svg\"); width:8px; height: 13px; } border-bottom:1px solid $dark-forest-green; } .faq-item { margin-bottom:15px; } .faq-block { margin-bottom:45px; } @include bp(desktop){ .faq-block { margin-bottom:0; } .faq-item { margin-bottom:40px; } .toggle-button { font-size: 34px; line-height: 0.8; &:after { top:7px; } } } .block__content { margin-top:35px; @include p(); color:$light-green; } .faq-blocks { width: 100%; } .faq-block { display: flex; flex-flow: row wrap; } .category-block { margin-bottom:50px; } .category-block, .faq-items { width:100%; } @include bp(desktop){ .category-block { margin-bottom:0; width: $category-block-width; margin-right: 30px; text-transform:uppercase; } .faq-items { width:$faq-items-width; } }
Then the final touch is to add the javascript for the actual toggle action. Here is the javascript.
;( function( $, window, undefined ) { \'use strict\' const fnmap = { \'toggle\': \'toggle\', \'show\': \'add\', \'hide\': \'remove\' }; const collapse = (selector, cmd) => { const targets = Array.from(document.querySelectorAll(selector)); targets.forEach(target => { target.classList[fnmap[cmd]](\'show\'); }); }; // Grab all the trigger elements on the page const triggers = Array.from(document.querySelectorAll(\'[data-toggle=\"collapse\"]\')); // Listen for click events, but only on our triggers window.addEventListener(\'click\', (ev) => { const elm = ev.target; if (triggers.includes(elm)) { const selector = elm.getAttribute(\'data-target\'); if(elm.classList.contains(\'active\')){ elm.classList.remove(\'active\'); } else { [].forEach.call(triggers, function(el) { // check if its active and if we are clicking on another one if(el.classList.contains(\'active\') && selector !== el.getAttribute(\'data-target\')){ el.classList.remove(\"active\"); const Sselector = el.getAttribute(\'data-target\'); collapse(Sselector, \'toggle\'); } }); elm.classList.add(\'active\'); } collapse(selector, \'toggle\'); } }, false); } ( jQuery, window ) );
What it does is, it either removes or adds the class \’active\’ to the faq item, so that it opens or closes. Quite simple vanilla script.
Conclusion
So, with this simple approach we have a lot of control over the styling as well as the functionality. Currently, it \”groups\” the different FAQ items according to the taxonomies but this can easily be modified in the list.php file. Or if you don\’t like the styling this can be modified in the scss folder.
Hope this helps understanding how to build a plugin and let me know what you\’ve came up with!