Ask your WordPress questions! Pay money and get answers fast! Comodo Trusted Site Seal
Official PayPal Seal

How to enqueue javascript in header if executed within shortcode? WordPress

  • SOLVED

I am the developer of the mapping plugin www.mapsmarker.com which allows user to use shortcodes like [mapsmarker marker="1"] within posts/pages/widgets or templates files to add maps to their sites.

Currently, jquery-javascript and my own file leaflet.js are added to the <head> of each page. Additionally, each map produced with a shortcodes generates an inline-javscript, so the page code looks like this (simplified):

<html>
<head>
<script type='text/javascript' src='jquery.js?ver=1.7.2'></script>
<script type='text/javascript' src='leaflet.js?ver=3.0'></script>
</head>
<body>
...
<div id="map">
<!--html-code for map-->
</div>
<script type="text/javascript">
<!--individual javscript for each map-->
</script>
...
</body>
</html>


For the next version I want to enqueue jquery.js and leaflet.js for performance issues only when a shortcode is executed/a map is displayed on the current page.

I already found out that I can do this by adding the enqueue-code to my function lmm_showmap() which gets executed by the action
add_shortcode($lmm_options['shortcode'], array(&$this, 'lmm_showmap'));
I thus added the following code to my function lmm_show() (simplified):

wp_enqueue_script( 'leafletmapsmarker', LEAFLET_PLUGIN_URL . 'leaflet-dist/leaflet.js', array('leafletmapsmarker-googlemaps-loader'), $plugin_version, false);


The problem is, that the scripts are added to the footer, although I set footer to false in wp_enqueue_script. Result is that the maps are not displayed, as the inline javascript needs jquery & leaflet.js to run before the inline javascript code (simplified):

<html>
<head>
</head>
<body>
...
<div id="map">
<!--html-code for map-->
</div>
<script type="text/javascript">
<!--individual javscript for each map-->
</script>
...
<script type='text/javascript' src='jquery.js?ver=1.7.2'></script>
<script type='text/javascript' src='leaflet.js?ver=3.0'></script>
</body>
</html>


I would need help with the following:
- can the enqueued javascripts jquery & leaflet.js be moved to the <head> at all if they are exectued within the action add_shortcode()? If yes, how to do that? <strong>This would be my prefered solution.</strong>

- as an alternative (if the first solution doesnt work), I tried to assign the inline javascript code to an extra variable like $javascript and also tried to enqueue this via wp_enqueue_scripts() (as I could set the order in which the javascripts are loaded).
Unfortunately I also failed here, as I dont know to enqueue dynamically generated javascript. Simplified how it should work:

wp_enqueue_script( 'leafletmapsmarker-mapjs', $javascript, array('jquery','leaflet-maps-marker'), false);

Anyone having a solution for this?

If you need more info - here the code on github:
code for main file: https://github.com/robertharm/Leaflet-Maps-Marker/blob/dev/leaflet-maps-marker.php
lmm_showmap(): https://github.com/robertharm/Leaflet-Maps-Marker/blob/dev/inc/showmap.php

Answers (5)

2012-12-02

John Cotton answers:

Robert

As a responsible plugin developer, you should be putting your scripts in the footer.

So register your scripts in the normal way, enqueing them when the shortcode is executed (ie in that function).

Then, to use wp_localize_script to add your paramaters.

So, assuming your script registration name is roberts-maps, then do this:


$map_data = array();
$map_data['lat'] = 1.23;
$map_data['lng'] = 41.23;
$map_data['zoom'] = 7;
wp_localize_script( ' roberts-maps', '_roberts_map_data', $map_data);


I use Google maps a lot on WP websites (sorry, not with your plugin!) and use this technique to pass all sorts of info.

You can have multiple variables if you like:


$map_data = array();
$map_data['lat'] = 1.23;
$map_data['lng'] = 41.23;
$map_data['zoom'] = 7;

$other_map_data = array();
$other_map_data['day'] = 'Tue';
$other_map_data['month'] = 'Dec';

wp_localize_script( ' roberts-maps', '_roberts_map_data', $map_data);
wp_localize_script( ' roberts-maps', '_roberts_other_map_data', $other_map_data);





John Cotton comments:

<blockquote>if I add my inline-js to wp_footer directly for example (not via wp_enqueue_scripts) - how can I assure that it is loaded after my scripts leaflet.js&jquery which are loaded via wp_enqueue_scripts?</blockquote>

You shouldn't looad via wp_footer directly, you should use wp_localize_script....that way, WordPress makes sure you page scripts come after the scripts they rely on.


John Cotton comments:

There is another, slightly cheaty way, and you can can see an example of it working here:

http://www.golfhotelwhiskey.com/airport-guides/

The map is included on the page with a shortcode and the data (and other script) us added via the WP global:



// In add_shortcode function..

// parts removed for brevity
wp_enqueue_script( 'google-maps-cluster' );
wp_enqueue_script( 'google-maps' );

$pins = json_encode($pins);

$js = <<< EOT
var dapgc = {};

dapgc.pins = $pins;

dapgc.bounds = undefined;
dapgc.markers = [];
<!-- Code removed for brevity -->
EOT;

global $wp_scripts;
$wp_scripts->add_data( 'google-maps', 'data', $js );



If you have a lot of script (rather than just data) to add, this is a convenient, though not necessarily future-proof, way of doing it.


John Cotton comments:

<blockquote>but it is my understanding that when the "add_shortcode" part is activated, the "wp_head" has already been processed, but "wp_footer" is still available.</blockquote>

That's correct - and logical. Remember that shortcodes are (generally) being trigger by the_content filter - so clearly come after all header action is complete.

But that doesn't matter, because we call know that script should be in the footer for best performance ;)


Robert Seyfriedsberger comments:

Hi John,
wp_localize_script() would be a good approach, if I only had to add variables to the script, but in my case, I have to add scripts like the following (simplified):

<script type="text/javascript">
var layers = {};
var markers = {};
var lmm_map_13562082 = {};
(function($) {
lmm_map_13562082 = new L.Map("lmm_map_13562082", { dragging: true, touchZoom: true, scrollWheelZoom: true, doubleClickZoom: true, boxzoom: true, trackResize: true, worldCopyJump: true, closePopupOnClick: true, keyboard: true, keyboardPanOffset: 80, keyboardZoomOffset: 1, inertia: true, inertiaDeceleration: 3000, inertiaMaxSpeed: 1500, zoomControl: true, crs: L.CRS.EPSG3857 });
var osm_mapnik = new L.TileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {maxZoom: 18, minZoom: 1, detectRetina: true});
var layersControl = new L.Control.Layers(
{'OSM Mapnik': osm_mapnik},
{},
{ collapsed: true } );
var marker = new L.Marker(new L.LatLng(55.689972, 160.927734),{ title: 'name', opacity: 1});
marker.bindPopup("Popuptext", {maxWidth: 300).openPopup();
})(jQuery);
</script>


as far as I understand wp_localize_script, this is not possible is it?
I created a test variable and then tried to add this by wp_localize_script:

$test = "<script type='text/javascript'>alert('test');</script>";
wp_localize_script('leafletmapsmarker', 'test', $test );


Output was

<script type='text/javascript'>
/* <![CDATA[ */
var test = "alert('test');";
/* ]]> */
</script>


and not

<script type='text/javascript'>
/* <![CDATA[ */
alert('test');
/* ]]> */
</script>


So I have to modify the code with $wp_scripts->add_data... right?


John Cotton comments:

<blockquote>So I have to modify the code with $wp_scripts->add_data... right?</blockquote>

Well that would certainly be the most straightforward route.

However, bear in mind that just because you output a fuller script now, that doesn't mean you have to continue to do so.

Looking at your existing script, it could easily be rewritten to pull from a variable written by wp_localize_script.

Instead of the following code being in the page, imagine it in one of your include js file....


var layers = {};
var markers = {};

var lmm_map_13562082 = {};

(function($) {

lmm_map_13562082 = new L.Map("lmm_map_13562082", { dragging: lmm_global.dragging, touchZoom: lmm_global.touchZoom, scrollWheelZoom: inertiaDeceleration: lmm_global.inertiaDeceleration, inertiaMaxSpeed: lmm_global.inertiaMaxSpeed, zoomControl: true, crs: L.CRS.EPSG3857 });

var osm_mapnik = new L.TileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {maxZoom: lmm_global.maxZoom, minZoom: lmm_global.minZoom, detectRetina: true});

var layersControl = new L.Control.Layers(

{'OSM Mapnik': osm_mapnik},

{},

{ collapsed: true } );

var marker = new L.Marker(new L.LatLng(lmm_global.markerLat, lmm_global.markerLng),{ title: 'name', opacity: 1});

})(jQuery);


I've not changed everything above, but hopefully you get the idea. With that in your external js file, you could then localise as follows:


$lmm_global = array();
$lmm_global['dragging'] = true;
$lmm_global['touchZoom'] = true;
$lmm_global['inertiaDeceleration'] = 3000;
$lmm_global['inertiaMaxSpeed'] = 1500;
$lmm_global['markerLat'] = 55.689972;
$lmm_global['markerLng'] = 160.927734;
wp_localize_script( ' roberts-maps', 'lmm_global', $lmm_global);


By separating the code from the data values, you can easily achieve what you want - AND stick to the API structure so future proofing your code.


Robert Seyfriedsberger comments:

unfortunately this is not really an option - but thanks to your input, I nearly got it - just one step left - perhaps you could help with this too:

I assigned all javascript to the variable $lmmjs_out - then I added those variable with priority 25 to wp_footer (wp_enqueue_scripts have 20) by

function map_javascript() {
global $lmmjs_out;
echo $lmmjs_out;
}
add_action('wp_footer', 'map_javascript', 25);


For maps on single page this already works - problem is with e.g. archive pages or on posts where multiple maps are shown. Here I get the error <strong>Cannot redeclare your_function()</strong>

I tried to create a dynamic function name based on the map ID but failed - hereĀ“s what I tried:


$func_name = 'map_javascript_' . $id;
function $func_name {
global $lmmjs_out;
echo $lmmjs_out;
}
add_action('wp_footer', $func_name, 25);


Do you know how to solve this? I already searched Google for "dynamic function names" but didnt get along with the answers. It would be really great if you could help me overcoming this last hurdle...


John Cotton comments:

<blockquote>function map_javascript() {

global $lmmjs_out;

echo $lmmjs_out;

}

add_action('wp_footer', 'map_javascript', 25);
</blockquote>

No, no, no!!

Don't try to beat the system - work with it!!

If you really must output functioning code rather than just data - and trust me Robert, whatever your code is, you can output just data - done hundreds of complex js websites and never failed to be able to do it - but if you do, then use the wp_script global as described above.

DON'T JUST STICK IT IN THE FOOTER - IT CAN'T BE RELIED UPON!!!!



John Cotton comments:

In your shortcode function do


global $lmmjs_out, $wp_scripts;

$wp_scripts->add_data( 'your-script-name-goes-here', 'lmmjs_out', $lmmjs_out );


Robert Seyfriedsberger comments:

:-( I was so happy I just found a nearly working solution...
Unfortunately I dont understand your code - could I ask you once more for help? I copied my code here: https://gist.github.com/4190766

On line 385 I added

global $lmmjs_out, $wp_scripts;

on line 756 I added

$wp_scripts->add_data( 'leafletmapsmarker', 'lmmjs_out', $lmmjs_out );

As I havent worked with add_data() before, I am not sure if thats the right usage...
thx


John Cotton comments:

OK - for starters, you don't need to open/close the script/CDATA elements on lines 386/387 and 713/714.

I see you're trying to use both wp_localize_script and add_data on lines 747/756. I suspect (but am not sure without trying it) that that won't work. Go with one or the other for now and see what happens.

Other than that, the code on that page doesn't give any context as to where it is called so I can't be certain why it might not work. If you can link to the output put of it, I might be able to comment further.

2012-12-02

Kyle answers:

Did you try hooking the function to enqueue the script into the wp_head action? Something like..


function load_js_file()
{
wp_enqueue_script( 'leafletmapsmarker-mapjs', $javascript, array('jquery','leaflet-maps-marker'), false);
}

add_action('wp_head', 'load_js_file', 0);


Note: I made a correction to action to make sure it loads early enough


Robert Seyfriedsberger comments:

problem is, that wp_enqueue_scripts doesnt allow to use content from dynamically generated $javascript variable - it only can enqueue .js-files which can be called directly to (like leaflet.js)


Robert Seyfriedsberger comments:

another question: if I add my inline-js to wp_footer directly for example (not via wp_enqueue_scripts) - how can I assure that it is loaded after my scripts leaflet.js&jquery which are loaded via wp_enqueue_scripts?


Kyle comments:

Okay, something I've used with my payment gateway implementation for my site is seen [[LINK href="http://scribu.net/wordpress/optimal-script-loading.html"]]here[[/LINK]]

Essentially, declare the enqueue scipt, tie it to a flag, then insert the flag in your shortcode, then run a function on page load that checks content early enough for the flag to add the script

class My_Shortcode {
static $add_script;

static function init() {
add_shortcode('myshortcode', array(__CLASS__, 'handle_shortcode'));

add_action('init', array(__CLASS__, 'register_script'));
add_action('wp_footer', array(__CLASS__, 'print_script'));
}

static function handle_shortcode($atts) {
self::$add_script = true;

// actual shortcode handling here
}

static function register_script() {
wp_register_script('my-script', plugins_url('my-script.js', __FILE__), array('jquery'), '1.0', true);
}

static function print_script() {
if ( ! self::$add_script )
return;

wp_print_scripts('my-script');
}
}

My_Shortcode::init();

2012-12-02

Dbranes answers:

Hi Robert,

I was looking for the same thing few weeks ago, but it is my understanding that when the "add_shortcode" part is activated, the "wp_head" has already been processed, but "wp_footer" is still available.

So I concluded that wp_enqueue_script was only going to display the javascript in the "wp_footer" and not in the "wp_head" when used in the shortcode.


Robert Seyfriedsberger comments:

I thought that something like that is happening. If there is no other workaround for this, I have to move my dynamic inline javascript also to the footer after jquery & leaflet.js. Do you know how to achieve this? as wroten, wp_enqueue only supports adding scripts which can be called directly via URL (leaflet.js for example). How did you solve your problem with this?


Dbranes comments:

When I tested a code like this:


// [test_enqueue_script]
add_shortcode('test_enqueue_script', 'test_enqueue_script_handler');
function test_enqueue_script_handler($atts) {
wp_enqueue_script('test-script', plugins_url('test-script.js', __FILE__), array('jquery'), '1.1', false); // in header
//wp_enqueue_script('test-script', plugins_url('test-script.js', __FILE__), array('jquery'), '1.1', true); // in footer
}


it always showed up in the footer.

After some hairpulling, I had to change my setup, and load the javascipt files earlier in my plugin class to be able to put it inside the header tags (wp_head) ;-)



Dbranes comments:

I used the 'wp_enqueue_scripts' hook for my script.

There is a helpful tutorial on this on wp.tutsplus.com :
[[LINK href="http://wp.tutsplus.com/articles/how-to-include-javascript-and-css-in-your-wordpress-themes-and-plugins/"]]http://wp.tutsplus.com/articles/how-to-include-javascript-and-css-in-your-wordpress-themes-and-plugins/[[/LINK]]

2012-12-02

Austin Passy answers:

I do this in my Hide Content and Catch Email shortcode plugins on [[LINK href="http://extendd.com"]]extendd.com[[/LINK]].

Try


class mapsmarker {

function __construct() {


/* Register the scripts */
add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );

/* Shortcode */
add_shortcode( 'mapsmarker', array( $this, 'shortcode' ), 10, 2 );


/**
* Register all javascript files needed
*
*/
function register_scripts() {

/* Scripts */
wp_register_script( 'mapsmarker', plugins_url( 'library/js/functions.js', __FILE__ ), array( 'jquery' ), '1.0', true );
wp_localize_script( 'mapsmarker', 'mapsmarker',
array(
'ajaxurl' => esc_url( admin_url( 'admin-ajax.php' ) ),
// your localization here
)
);

}

function shortcode( $attr = array(), $content = null ) {
global $post;

extract( shortcode_atts( array(
'marker' => '',
), $attr ) );

/* Enqueue the scripts */
wp_enqueue_script( 'mapsmarker' );

$content = '';

return $content;
}

}

2012-12-02

Martin Pham answers:

Hello Robert,
I changed your code, to be able to lazy call javascript
---------------
leaflet-maps-marker.php:(Lines 493+494) [[LINK href="https://gist.github.com/4189940"]]https://gist.github.com/4189940[[/LINK]]
showmap.php: (lines: 398 to 436 ; 746 to 752) [[LINK href="https://gist.github.com/4189923"]]https://gist.github.com/4189923[[/LINK]]

inline-script:

var MapsMarker = {
gJ: function (u,c) {
var s = document.createElement("script"),h = document.getElementsByTagName("head")[0],d = !1;
s.src = u;
s.onload=s.onreadystatechange = function() {
if (!d && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete")) {
d = !0;
if(c) c();
s.onload = s.onreadystatechange = null;
h.removeChild(s);
}
};
h.appendChild(s);
},
ini_lib: function() {
var s = this;
if (typeof jQuery == "undefined") {
s.gJ("http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js", null);
}
return s.load_leaflet();
},
load_leaflet: function() {
var s = this;
if (typeof L == "undefined") {
s.gJ("'.LEAFLET_PLUGIN_URL .'leaflet-dist/leaflet.js",function() {
return s.map_loader();
});
} else {
return s.map_loader();
}
},
map_loader: function () {
// map data
}
}

// Run
var RSCI = setInterval(function() {
if (document.readyState === "complete") {
MapsMarker.ini_lib();
clearInterval(RSCI);
}
}, 100);


Robert Seyfriedsberger comments:

Hi Martin,
thanks, this would be a great approach, unfortunately it is not quite working, as the following code is not considered and needed for my maps to be displayed (loading google maps api & localize leaflet.js via wp_localize_script):

$lmm_options = get_option( 'leafletmapsmarker_options' );
$plugin_version = get_option('leafletmapsmarker_version');
if ( is_admin() ) { $gmaps_libraries = '&libraries=places'; } else { $gmaps_libraries = ''; }
//info: Google language localization (JSON API)
if ($lmm_options['google_maps_language_localization'] == 'browser_setting') {
$google_language = '';
} else if ($lmm_options['google_maps_language_localization'] == 'wordpress_setting') {
if ( defined('WPLANG') ) { $google_language = "&language=" . substr(WPLANG, 0, 2); } else { $google_language = '&language=en'; }
} else {
$google_language = "&language=" . $lmm_options['google_maps_language_localization'];
}
if ($lmm_options['google_maps_base_domain_custom'] == '') {
$gmaps_base_domain = "&base_domain=" . $lmm_options['google_maps_base_domain'];
} else {
$gmaps_base_domain = "&base_domain=" . $lmm_options['google_maps_base_domain_custom'];
}
wp_enqueue_script( array ( 'jquery' ) );
//info: Google API key
if ( isset($lmm_options['google_maps_api_key']) && ($lmm_options['google_maps_api_key'] != NULL) ) { $google_maps_api_key = $lmm_options['google_maps_api_key']; } else { $google_maps_api_key = ''; }
wp_enqueue_script( 'leafletmapsmarker-googlemaps-loader', 'https://www.google.com/jsapi?key='.$google_maps_api_key, array(), NULL);
//info: Bing culture code
if ($lmm_options['bingmaps_culture'] == 'automatic') {
if ( defined('WPLANG') ) { $bing_culture = WPLANG; } else { $bing_culture = 'en_us'; }
} else {
$bing_culture = $lmm_options['bingmaps_culture'];
}
//info: load leaflet.js + plugins
wp_enqueue_script( 'leafletmapsmarker', LEAFLET_PLUGIN_URL . 'leaflet-dist/leaflet.js', array('leafletmapsmarker-googlemaps-loader'), $plugin_version);
wp_localize_script('leafletmapsmarker', 'leafletmapsmarker_L10n', array(
'lmm_zoom_in' => __( 'Zoom in', 'lmm' ),
'lmm_zoom_out' => __( 'Zoom out', 'lmm' ),
'lmm_googlemaps_language' => $google_language,
'lmm_googlemaps_libraries' => $gmaps_libraries,
'lmm_googlemaps_base_domain' => $gmaps_base_domain,
'lmm_bing_culture' => $bing_culture
) );


Would that also be possible by "lazy javascript"?