11

I am creating some WordPress short codes aimed at providing internal navigation on a page (one page with a lot of content sections and it own menu).

This is what I have:

//menu
function internal_menu($atts) {
  extract(shortcode_atts(array(
   'href1' => '#jl1',
   'href2' => '#jl2',
   'href3' => '#jl3',
   'href4' => '#jl4',
  ), $atts));
  return '<div id="internalPageMenu">
    <ul>
        <li><a href="' . $href1 . '"><i class="fa fa-bars"></i>link 1</a></li>
        <li><a href="' . $href2 . '">link 2</a></li>
        <li><a href="' . $href3 . '">link 3</a></li>
        <li><a href="' . $href4 . '">link 4</a></li>
    </ul>
    </div>';
}
add_shortcode('internal-menu', 'internal_menu');

//menu target
function internal_menu_target($atts) {
  extract(shortcode_atts(array(
   'id' => 'jl1',
   'text' => '',
   ), $atts));
   return '<h3 id="' . $id . '">' . $text . '</h3>';
}
add_shortcode('internal-menu-target', 'internal_menu_target');

And using this in my Wordpress admin panel:

[internal-menu]
[internal-menu-target id="jl1"]
Some content
[internal-menu-target id="jl2"]
...etc...

How do I make the menu dynamic (not restricted to the number of items it can have)? For example the short code would be:

[internal-menu targets="jl1, jl2, jl3, jl4, jl5, ...etc..."]

5 Answers 5

21

foreach would be your answer here. It will be the easiest and cleanest in my opinion. Before I give you a code example, lets analyze your code and look at all your flaws and how we will correct them

FLAWS

  • Never ever use extract(). exctract() creates variables on the fly which is problematic. You cannot properly debug extract() (if you even can), so when it fails you really have your work cut out for you, unnecessarily. For these reasons, it was completely removed from core and the codex. See trac ticket 22400. You should have an evil list with query_posts and extract() in the top two positions, that i how bad these two are.

  • You are not sanitizing and validating input data which can lead to hacker injecting jquery into your code in order to hack your site. Never trust any data that comes from user side and URL's, it might be infected.

  • As you already know, taken from your code, shortcodes cannot except array value, the values must be string. In your case, we need to create an array from string values. Again, because you can't trust a user to not use spaces before or after commas, it is wise and recommended to remove all white spaces, if any, in order for your explode function to correctly create your array

  • With this new approach, you need to make sure that your values in your strings are the in the correct order and that the strings is the correct lenght. If not, you will get unexpected output

Lets tackle the first shortcode: (PLEASE NOTE: All the code below is untested. It might be buggy or have syntax errors)

internal-menu

//menu
function internal_menu( $atts ) 
{
    $attributes = shortcode_atts(
        array(
           'href' => '',
         ), 
        $atts
    );

    $output = '',
    // Check if href has a value before we continue to eliminate bugs
    if ( !$attribute['href'] )
        return $output;
    // Create our array of values
    // First, sanitize the data and remove white spaces
    $no_whitespaces = preg_replace( '/\s*,\s*/', ',', filter_var( $attributes['href'], FILTER_SANITIZE_STRING ) ); 
    $href_array = explode( ',', $no_whitespaces );

    $output .= '<div id="internalPageMenu">';
        $output .= '<ul>';

            foreach ( $href_array as $k => $v ) { 
                // From your code, link 1 is different, so I kept it as is
                if ( $k == 0 ) {
                    $output .= '<li><a href="#' . $v . '"><i class="fa fa-bars"></i>link 1</a></li>';
                } else { 
                    $output .= '<li><a href="#' . $v . '">link ' . ($k + 1 ) . '</a></li>';
                }
            }

        $output .= '</ul>';
    $output .= '</div>';

    return $output;
}
add_shortcode( 'internal-menu', 'internal_menu' );

You can then use the shortcode as follow

[internal-menu href='jl1, jl2, jl3, jl4']

internal-menu-target

//menu target
function internal_menu_target($atts) 
{
    $attributes = shortcode_atts(
        array(
           'id' => '',
           'text' => '',
         ), 
        $atts
    );

    $output = '',
    // Check if href has a value before we continue to eliminate bugs
    if ( !$attribute['id'] || !$attribute['text'] )
        return $output;

    // Create our array of values
    // First, sanitize the data and remove white spaces
    $no_whitespaces_ids = preg_replace( '/\s*,\s*/', ',', filter_var( $attributes['id'], FILTER_SANITIZE_STRING ) ); 
    $ids_array = explode( ',', $no_whitespaces_ids );

    $no_whitespaces_text = preg_replace( '/\s*,\s*/', ',', filter_var( $attributes['text'], FILTER_SANITIZE_STRING ) ); 
    $text_array = explode( ',', $no_whitespaces_text );

    // We need to make sure that our two arrays are exactly the same lenght before we continue
    if ( count( $ids_array ) != count( $text_array ) )
        return $output;

    // We now need to combine the two arrays, ids will be keys and text will be value in our new arrays
    $combined_array = array_combine( $ids_array, $text_array );
    foreach ( $combined_array as $k => $v )
        $output .= '<h3 id="' . $k . '">' . $v . '</h3>';

    return $output;
}
add_shortcode('internal-menu-target', 'internal_menu_target');

You can use this shortcode as follow:

[internal-menu-target id='1,2,3,4' text='text 1, text 2, text 3, text 4']
Sign up to request clarification or add additional context in comments.

Comments

4

Simple way I use:

[my param="key1=value1&key2=value2"]

in shortcode callback, just do:

parse_str( str_replace("&amp;", "&", $attrs['param']), $array);
// var_dump( $array );

1 Comment

Clean solution!
3

Problems:

Wordpress shortcodes have some painful restrictions in the format of the data which can be passed...

space-delimited variables:

[shortcode a="1 2"]

result: $atts=['a'='"1', 0='2"']

']' closes the shortcode:

[shortcode b=[yay]]

result: $atts=['b'='[yay']

Solution:

You can get around this by using urlencode():

[shortcode atts=a=1+2&b=%5Byay%5D]

parse it like so:

parse_string($atts['atts'],$atts);

result: $atts=['a'=>'1 2', b=>'[yay]']

That will give you the array passing you want.

Now as for building your menu(s):

function internal_menu($atts) {
  // allow passing + and ] in the text of the links:
  parse_string($atts["links"],$links);

  // the defaults, verbatim from the question:
  if (!count($links)) $links=[
    'href1' => '#jl1',
    'href2' => '#jl2',
    'href3' => '#jl3',
    'href4' => '#jl4',
  ];

  foreach ($links as $text=>$href) $ul=."<li><a href=\"$href\">$text</a></li>";

  return '<div id="internalPageMenu"><ul>'.$ul.'</ul></div>';
}

add_shortcode('internal-menu', 'internal_menu');

//menu target

function internal_menu_target($atts) {
  // allow passing + and ] in the text:
  if (@$atts[text]) $atts['text']) = urldecode($atts['text']);

  // the defaults, verbatim from the question:
  $atts=array($atts)+['text'=>'','id'=>'jl1'];

  return '<h3 id="' . $link['id'] . '">' . $link['text'] . '</h3>';
}

add_shortcode('internal-menu-target', 'internal_menu_target');

and then feed it like so:

[internal-menu links=First+Link=#jl1&Second+Link=#jl2&Google=https://google.com]

[internal-menu-target text=Section+1 id=jl1]

etc.

BTW extract() is perfectly fine if you use it responsibly:

/* concatenates 3 words of wisdom into a polite policy direction
 *
 * @param $attr: hash of function args
 *          foo = the first word (Kindly)
 *          bar = the second word (ignore)
 *          baz = the third word (totalitarians)
 */

function excellent_policy($attr){
  $defaults=['foo'=>'Kindly', 'bar'=>'ignore', 'baz'=>'totalitarians'];
  extract((array)array_intersect_key($attr,$defaults)+$defaults);
  echo "$foo $bar $baz!";
}

This imports $foo, $bar, and $baz from $attr into local scope in a readable and predictable manner, provides defaults for those variables if they were not passed, and prevents the creation of any unexpected variables.

There's good ways and bad ways to use language features. Forbidding everyone from using a language feature because someone might use it poorly is like forbidding everyone from breathing because someone might try to inhale Jello.

4 Comments

Is this new? I have been working on a wordpress site that for years has been able to enter shortcodes that don't have space-delimited variables. We've recently done an upgrade, but I'm pretty sure other places still work with spaces as part of a single parameter.
@Millar248 see Shortcode API here: codex.wordpress.org/Shortcode_API The examples I gave still behave the way I describe them, and solve the OP's question, as-is.
Yes your code does work. I tried it, and it sorta fixed my problem (with additional work), but I found out the root of my problem was that I was passing html comments in the string of a parameter, which I guess is somehow not allowed. Removing the comments removed the space-delimited variables.
"forbidding everyone from breathing because someone might try to inhale Jello" The best I've heard today haha!
1

I have an alternative way to do this in $content of short code instead.

This solution increases the readability/relation of your data. But it might not be a good way if you want to use your $content to do other things too.

shortcode used is like...

// in wordpress editor or your *.php file

[testimonial_carousel_list]
[
  {
    "message": "Lorem ipsum dolor sit amet.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "John Doe"
  },
  {
    "message": "Vivamus vel ornare purus, in faucibus tellus.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "Jane Smith"
  },
  {
    "message": "Morbi tristique augue vel mi ornare, sit amet viverra lectus semper.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "David Lee"
  }
]
[/testimonial_carousel_list]

And here how to handle it intro your shortcode function

// it might be in functions.php of your theme

function testimonial_carousel_list_shortcode($_atts, $content = null) {
  $items = json_decode($content);

  // ...rest of your code whatever you what to do with array $items
}
add_shortcode('testimonial_carousel_list', 'testimonial_carousel_list_shortcode');

// this is required if you want to json_decode() on your $content.
// because wptexturize will change your `'` or `"` to `‘` or `“` it will made json_decode got error.
function shortcodes_to_exempt_from_wptexturize( $shortcodes ) {
    $shortcodes[] = 'testimonial_carousel_list';
    return $shortcodes;
}
add_filter('no_texturize_shortcodes', 'shortcodes_to_exempt_from_wptexturize');

4 Comments

Great approach, I'm just having trouble with json_decode, even though I tried it with the exact same code.
@ThomasD. Can you tell me what's wrong with json_decode? and logged the data of $content. That I can help you.
Thanks! This is the var_dump output of $content. json_decode returns NULL. string(282) " [ { „text“: „Hello world!“ }, { „text“: „<a href=\“google.com\“ target=\“_blank\“><strong>Bold text<strong> inside a link</a>“ } ] "
It seems it's caused by Gutenberg, which converts the double quotes " into . This happens in a parser inside Gutenberg before the shortcode itself is actually rendered. There does not seem to be a solution to this except using the classic editor, which isn't an option sadly.
0

I like your approach @Akegvd and was able to alter it so that it'll also work inside Gutenberg.

In Gutenberg plain text characters are replaced with formatted entities through wptexturize(). According to the documentation you can avoid this transformation easily with putting the relevant content inside special tags, e.g. <code>. With this you can avoid your json information, i.e. the double quotes being converted and furthermore use the closing tag </code> to separate the json from the rest of your shortcode's $content.

This provides a good solution to have a good readability and clean structure of the json data and also use the $content for other purposes.

Simply use the shortcode with <code> tag

// in Wordpress Gutenberg shortcode block write

[testimonial_carousel_list] 
<code>
[   
  {
    "message": "Lorem ipsum dolor sit amet.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "John Doe"   
  },   
  {
    "message": "Vivamus vel ornare purus, in faucibus tellus.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "Jane Smith"   
  },
  {
    "message": "Morbi tristique augue vel mi ornare, sit amet viverra lectus semper.",
    "avatar": "https://via.placeholder.com/150",
    "full_name": "David Lee"   
  }
] 
</code>
SOME OTHER CONTENT
[/testimonial_carousel_list]

and write the following in your functions.php

// it might be in functions.php of your theme

function testimonial_carousel_list_shortcode($atts, $content = null) {
  
    $atts = shortcode_atts( array(      
      'somedefaultkey' => 'val1',
      'otherdefaultkey' => 'val2'), 
       $atts ); 
            
    $temp=explode('</code>', $content);
    $json_str=$temp[0];
    $content=$temp[1];

    // strip automatically added html tags e.g. <br>    
    $json_str=strip_tags($json_str); 
    $items = json_decode($json_str, true); # set true to get accessible $items array

    // Loop through json_entries
    foreach($items as $value){
      // do something
    }
     // ...rest of code
}
add_shortcode('testimonial_carousel_list', 'testimonial_carousel_list_shortcode');

Hope this helps @Thomas D.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.