Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: javascript

  • Caching Dismissible Alerts With localStorage

    Caching Dismissible Alerts With localStorage

    There are a lot of reasons to want to have a temporary, but dismissible, alert on the front end of your website. And there are a million different ways to do that. The trick is … how do you do that and make your site remain cacheable?

    Web Storage (Maybe) Is the Answer

    I preface this with a warning. I’m going to be using Local Storage, and that is not something I generally advocate. Web storage (aka DOM storage) is the practice of storing data in the browser. You can do this in a few other ways, such as sessions or cookies. But both of those can be detrimental to caching. After all, PHP Sessions and cookies tell systems like Varnish and Nginx “Don’t cache this.” They’re also persistent, so if I get a cookie once, I keep it until it expires or I delete it.

    On the other hand, web storage is outside the website entirely, that is my server has no idea about them, and it doesn’t really impact caching at all. Varnish, Nginx, and all those sorts of server-based services don’t care about it, and if implemented properly, there’s no problem. You can store the data locally or per-session, which means ‘forever’ or ‘for as long as this browser window is open.’

    Don’t Use localStorage (Most of the Time)

    That all sounded awesome. So then why are there so many articles advocating you not use localStorage? Well there are some massive caveats:

    1. It’s pure javascript, so PHP doesn’t easily get it
    2. If you store sensitive data in it, you’re a numpty
    3. It can ‘only’ store 5 megs
    4. The content has no expiration
    5. You can only use string data
    6. All your localStorage is loaded on every single page load
    7. Any other javascript can see it and play with it

    That’s starting to sound squidgy, right? Randall Degges has a good writeup of the drawbacks.

    Well good news here. This use is possibly the only time I’ll ever advocate it’s use. I’m going to us it here because it works with caching, it works with most browsers (IE8+), and the worst case scenario here is that people will always see my alert.

    Pull Up With Bootstrap

    I’m using Bootstrap on this particular site, and it makes my life hella easy because they built in dismissible alerts. But the gotcha? Those alerts aren’t persistent. Which means if I want an alert to go away forever, then I’m SOL.

    Except I’m not. Bootstrap includes a way to run an ‘action’ on dismiss, which means I could do something like this:

    jQuery(document).ready(function($) {
        var gdpr = localStorage.getItem('gdpr-alerted') || '';
    
        if (gdpr = 'yes') {
            $('#myAlert').alert('close');
        }
    
        $('#myAlert').on('closed.bs.alert', function () {
           localStorage.setItem('gdpr-alerted','yes');
        })
    }
    

    What this does is it sets a variable (gdpr) based on the content of my item in localStorage (gdpr-alerted). If that value is yes, it closes the alert. Otherwise, it sets it to yes when someone closes the alert.

    That actually works just fine, but it has a weird blip if the javascript is loaded in the footer, where you would see my alert for a split second before the page finished loading. So I decided to go another way, and factor in some expirations.

    The (GDPR Related) Code

    First up the PHP:

    function my_gdpr_footer(){
    	if ( !is_user_logged_in() ) {
    		?>
    		<div id="GDPRAlert" class="alert alert-info alert-dismissible fade collapse alert-gdpr" role="alert">
    			This site uses cookies for stuff. Keep browsing and we keep tracking. Wanna know more? <a href="/terms-of-use">Read our Terms of Use</a>.
    			<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    		</div>
    		<?php
    	}
    }
    add_action( 'wp_footer', 'my_gdpr_footer', 5 );
    
    function my_scripts() {
    	wp_enqueue_script( 'bootstrap', get_template_directory_uri() . '/inc/bootstrap/js/bootstrap.min.js', array( 'jquery' ), '4.1.1', 'all', true );
    	wp_enqueue_script( 'lwtv-gdpr', get_template_directory_uri() . '/inc/js/gdpr.js', array( 'bootstrap' ), '1.0', 'all', true );
    }
    add_action( 'wp_enqueue_scripts', 'my_scripts' );
    

    Now my sneaky JS:

    // jQuery dismissable - sets localStorage a year
    
    jQuery(document).ready(function($) {
    	var gdpr = localStorage.getItem('gdpr-alerted') || '';
    	var year = new Date().setFullYear(new Date().getFullYear() + 1);
    
    	if (gdpr < new Date()) {
    		$('#GDPRAlert').addClass('show');
    		localStorage.removeItem('gdpr-alerted');
    	}
    
    	$('#GDPRAlert').on('closed.bs.alert', function () {
    		localStorage.setItem('gdpr-alerted',year);
    	})
    });
    

    The reason this works is that I remove the show class from the PHP and by default assume it shouldn’t show. Then in the javascript, I set up the value as ‘a year from now’ instead of ‘yes’, and check. If the localStorage value is less than ‘now’, then it’s expired and we should show the class (and delete the storage). Otherwise, it’s the same old same old.

    But Should We?

    That’s the big question though. Is this the best way to go about it? The next best choice would be IndexedDB, however that is not supported by ‘enough’ browsers yet, so yes. For now, this is the best choice.

  • jQuery Tablesorter

    jQuery Tablesorter

    So you want to sort tables on your WordPress site, without learning a whole mess of code.

    Good news. There’s a plugin called Table Sorter that can do this and in pretty much just works. Except… There are cases where you’re outputting data in a theme (or a plugin) and you can’t use that plugin to do it.

    Don’t panic, we’ve got you.

    Enqueue The Right Tablesorter

    I’m aware there’s a tablesorter.com – Don’t use it. It’s out of date at the site is possibly hacked. Instead, grab Tablesorter from RobG (aka mottie). Rob is still updating this plugin and debugging it, so it’s a better bet that the various other forks.

    You’ll enqueue this the ‘normal’ way:

    wp_enqueue_script( 'tablesorter', plugins_url( 'jquery.tablesorter.js', __FILE__ ), array( 'jquery' ), '2.30.5', false );
    

    There’s a second part to the enqueue though. You see you also need to tell it what to sort. That is, tell the javascript what to pay attention to.

    That’s done by using a class and an ID: <table id="myTable" class="tablesorter"></table>

    If you’re using the plugin I mentioned above, you only have to do the latter, because it accounts things differently but, properly, you should be using the ID. Then you have to insert this javascript:

    $(function() {
      $("#myTable").tablesorter();
    });
    

    Which is actually wrong. For WordPress. Again, no panicking!

    jQuery(document).ready(function($){
      $("#myTable").tablesorter();
    });
    </script>
    

    See? That was easy. If you wanted to be more WordPressy, you do this:

    wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter(); });' );
    

    You were expecting more?

    That’s really it. I do some extra weird stuff, since I call it on one page only (statistics) and that pages uses query variables so you can have /statistics/nations/ without me needing to make multiple sub pages, and it looks like this:

    	function enqueue_scripts() {
    
    		if ( is_page( array( 'statistics' ) ) ) {
    			$statistics = get_query_var( 'statistics', 'none' );
    			wp_enqueue_script( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/jquery.tablesorter.js' , array( 'jquery' ), '2.30.5', false );
    			wp_enqueue_style( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/theme.bootstrap_4.css' );
    
    			switch( $statistics ) {
    				case 'nations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    				case 'stations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#stationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    			}
    		}
    	}
    

    Oh right that also demonstrates a theme!

    Tablesorter lets you use themes like Bootstrap 4.x so your tables can be all matchy-matchy.

    But at this point, it should be enough to get your tables sorted.

  • CMB2: Conditional Meta Fields

    CMB2: Conditional Meta Fields

    Even though Gutenberg is on the rise, and every day gets us closer to using a whole new editor, we still use the ‘classic’ editor and we’re still beholden to it’s space limitations. I have strong feelings about how to properly utilize space when using CMB2, but not included in that specific post is this.

    Fields You Don’t (Always) Need

    There are three types of fields for CMB2.

    1. Fields you always need to use
    2. Fields that are optional
    3. Fields that are needed only in specific situations

    Most of the time we use option 2. Option 3 is the tricky one, though, since most of the time we end up having things show based on actions. That is, I save a post, and new options show up. When it comes to CMB2, we really don’t want to have to save and then edit and save and edit.

    Yuck!

    Thankfully this is all possible.

    Practical Example: Affiliates

    Today we’re making a box to handle affiliate links. There are three types of links: Amazon, Genric, and ‘Unique.’

    The Amazon one will link directly to a signup link and the generic one links to the same thing only on Click Junction. Both of those links will have affiliate details backed in so I don’t have to look them up later. This also means any partners I have on the site don’t need to know all the gory details. Bazinga.

    The last one, though ‘Unique’ is tricky. You see, when someone picks that, I want them to be able to put in a specific URL that may be affiliate linking somewhere else. But let’s start out with how it normally works.

    Make Your Fields

    function my_site_cmb2_metaboxes() {
    	// prefix for all custom fields
    	$prefix = 'my_sites_';
    
    	// Metabox Group: Must See
    	$cmb_affiliate = new_cmb2_box( array(
    		'id'           => 'affiliate_metabox',
    		'title'        => __( 'Affiliate Details', 'my-domain' ),
    		'object_types' => array( 'post_type_shows' ),
    		'show_in_rest' => true,
    	) );
    
    	// Field Box: Affiliate Type
    	$field_affiliatetype = $cmb_affiliate->add_field( array(
    		'name'             => __( 'Type', 'my-domain' ),
    		'id'               => $prefix . 'affiliate',
    		'type'             => 'select',
    		'options'          => array( 
    			'amazon'  => 'Amazon',
    			'generic' => 'Generic',
    			'url'     => 'Unique Link',
    		);
    		'show_option_none' => true,
    	) );
    	// Field Box: Affiliate Links
    	$field_affiliateurl = $cmb_affiliate->add_field( array(
    		'name'    => __( 'Link', 'my-domain' ),
    		'id'      => $prefix . 'affiliateurl',
    		'type'    => 'text_url',
    	) );
    );
    

    That’s pretty normal for CMB2 and looks like this:

    CMB2: Affiliate Details

    Normal, but … I want to hide that Link field unless a specific option is selected. Enter javascript.

    Hide That Field (Conditionally)

    Make a file for your javascript. I’ve called mine cmb2.js and put it in the same folder as my file that will enqueue the scripts.

    // Either create a new empty object, or work with the existing one.
    window.MySite_CMB2 = window.MySite_CMB2 || {};
    
    (function( window, document, $, app, undefined ) {
    	'use strict';
    
    	app.cache = function() {
    		app.$ = {};
    		app.$.select = $( document.getElementById( 'my_sites_affiliate' ) );
    		app.$.field = $( document.getElementById( 'my_sites_affiliateurl' ) );
    		app.$.field_container = app.$.field.closest( '.cmb-row');
    	};
    
    	app.init = function() {
    		app.cache();
    		app.$.select.on( 'change', function( event ) {
    			if ( 'url' === $(this).val() ) {
    				app.$.field_container.show();
    			} else {
    				app.$.field_container.hide();
    			}
    		} ).trigger( 'change' );
    	};
    
    	$( document ).ready( app.init );
    })( window, document, jQuery, MySite_CMB2 );
    

    And then call the enqueue, but only when appropriate (because we want to keep the load low):

    add_action( ‘admin_enqueue_scripts’, ‘my_site_admin_enqueue_scripts’ );

    function my_site_admin_enqueue_scripts( ) {
    $screen = get_current_screen();
    if ( ! isset( $screen->post_type ) || ‘post_type_shows’ !== $screen->post_type ) return;

    wp_enqueue_script( 'custom-js', plugins_url( '/cmb2.js' , __FILE__ ), array( 'jquery' ) );
    

    }

    And that works like this:

    GIF of how it shows and hides things

    A Word of Warning

    While all this is great for the sighted people, hiding things is not actually all that great for those who use screen-readers. For that you’d want to toggle the field to be disabled.

  • Stacked Charts Part 3: The Javascript

    Stacked Charts Part 3: The Javascript

    Finally!

    We have our data in a properly consumable array. It’s formatted the way we need. Now we just need to script the java.

    Take a deep breath.

    What We Want

    What we want is simple. A stacked bar chart that shows the values of all possible permutations. It looks like this:

    A stacked chart that shows how many characters per gender orientation there are per country
    A stacked chart

    That shows how many characters there are per gender orientation, and stacks it for a total count (which is why we needed that count you see).

    Send In The Clowns

    Since I’m already using Chart.js, I just need to have a function to output the javascript. But. Since I also have to loop through the arrays to get the collective data, I need a bit of PHP:

    /*
     * Statistics Display Barcharts
     *
     * Output the list of data usually from functions like self::meta_array
     * It loops through the arrays and outputs data as needed
     *
     * This relies on ChartJS existing
     *
     * @param string $subject The content subject (shows, characters)
     * @param string $data The data - used to generate the URLs
     * @param array $array The array of data
     *
     * @return Content
     */
    static function stacked_barcharts( $subject, $data, $array ) {
    
    	// Defaults
    	$data       = ( $data == 'nations' )? 'nations' : substr( $data, 8 );
    	$title      = ucfirst( substr($subject, 0, -1) ) . ' ' . ucfirst( $data );
    	$height     = '550';
    
    	// Define our settings
    	switch ( $data ) {
    		case 'gender':
    		case 'sexuality':
    		case 'romantic':
    			$title    = 'Character per Nation by ' . ucfirst( $data );
    			$datasets = array();
    			$terms    = get_terms( 'lez_' . $data, array( 'orderby' => 'count', 'order' => 'DESC', 'hide_empty' => 0 ) );
    			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    				foreach ( $terms as $term ) $datasets[] = $term->slug;
    			}
    			$counter  = 'characters';
    			$height   = '400';
    			break;
    	}
    	?>
    	<h3><?php echo $title; ?></h3>
    	<div id="container" style="width: 100%;">
    		<canvas id="barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>" width="700" height="<?php echo $height; ?>"></canvas>
    	</div>
    
    	<script>
    	// Defaults
    	Chart.defaults.global.responsive = true;
    	Chart.defaults.global.legend.display = false;
    
    	// Bar Chart
    	var barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>Data = {
    		labels : [
    		<?php
    			foreach ( $array as $item ) {
    				if ( $item[$counter] !== 0 ) {
    					$name = esc_html( $item['name'] );
    				}
    				echo '"'. $name .' ('.$item[$counter].')", ';
    			}
    		?>
    		],
    		datasets: [
    		<?php
    		foreach ( $datasets as $label ) {
    			$color = ( $label == 'undefined' )? 'nundefined' : str_replace( ["-", "–","-"], "", $label );
    			?>
    			{
    				borderWidth: 1,
    				backgroundColor: window.chartColors.<?php echo $color; ?>,
    				label: '<?php echo ucfirst( $label ); ?>',
    				stack: 'Stack',
    				data : [<?php
    					foreach ( $array as $item ) {
    						echo $item[ 'dataset' ][ $label ] . ',';
    					}
    				?>],
    			},
    			<?php
    		}
    		?>
    		]
    	};
    	var ctx = document.getElementById("barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>").getContext("2d");
    	var barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?> = new Chart(ctx, {
    		type: 'horizontalBar',
    		data: barStacked<?php echo ucfirst( $subject ) . ucfirst( $data ); ?>Data,
    		options: {
    			scales: {
    				xAxes: [{ stacked: true }],
    				yAxes: [{ stacked: true }]
    			},
    			tooltips: {
    				mode: 'index',
    				intersect: false
    			},
    		}
    	});
    
    	</script>
    	<?php
    }
    

    The Color

    You may have noticed a strange variable:

    $color = ( $label == 'undefined' )? 'nundefined' : str_replace( ["-", "–","-"], "", $label );
    

    Which was then called in the javascript here:

    backgroundColor: window.chartColors.<?php echo $color; ?>,
    

    I have this in a javascript file that is loaded on that page:

    // Color Defines
    window.chartColors = {
    	
    	// Gender
    	agender: 'rgba(255, 99, 132, 0.6)', // 'red'
    	cisgender: 'rgba(75, 192, 192, 0.6)', // 'aqua'
    	demigender: 'rgba(255, 205, 86, 0.6)', // 'goldenrod'
    	genderfluid: 'rgba(54, 162, 235, 0.6)', // 'light blue'
    	genderqueer: 'rgba(255, 159, 64, 0.6)', // 'orange'
    	nonbinary: 'rgba(201, 203, 207, 0.6)', // 'grey'
    	transman: 'rgba(0, 169, 80, 0.6)', // 'green'
    	transwoman: 'rgba(153, 102, 255, 0.6)', // 'purple'
    
    	// Sexuality
    	asexual: 'rgba(255, 99, 132, 0.6)', // 'red'
    	bisexual: 'rgba(75, 192, 192, 0.6)', // 'aqua'
    	heterosexual: 'rgba(255, 205, 86, 0.6)', // 'goldenrod'
    	homosexual: 'rgba(54, 162, 235, 0.6)', // 'light blue'
    	pansexual: 'rgba(255, 159, 64, 0.6)', // 'orange'
    	nundefined: 'rgba(201, 203, 207, 0.6)', // 'grey'
    	queer: 'rgba(0, 169, 80, 0.6)', // 'green'
    	demisexual: 'rgba(153, 102, 255, 0.6)', // 'purple'
    }
    

    The reason it’s ‘undefined’ is that things got weird when I had a variable with a name of undefined.

  • FacetWP: Spinning While Updating

    FacetWP: Spinning While Updating

    When you use FacetWP you can do some cool things like change the ‘count’ output of a page. Using the function facetwp_display() you can add facetwp_display( 'counts' ) to your page title, and then a boring old archive title goes from “List of Characters (2022)” to “List of Characters (1-24 of 2022)”

    But… What if you could do more?

    What Could Be More?

    If you have a lot of data, sometimes a page can load and FacetWP spins while it collects everything in it’s wee javascripty brain. When that happens, you have a cognitive moment of “What?” And in order not to lose a user, you want to indicate, somehow, that an action is happening. A spinning icon is, I think, a great way to do that.

    So with that in mind, I want to do this:

    List of Characters showing a spinning icon for the number

    And I did.

    The Code

    This needs javascript. I used some logic from the FacetWP documentation and some memories about how you can replace text with javascript and came up with this:

    (function($) {
    	$(document).on('facetwp-refresh', function() {
    		$('.facetwp-count').html('<i class="fa fa-spinner fa-pulse fa-fw"></i><span class="sr-only">Loading...</span>');
    	});
        
        $(document).on('facetwp-loaded', function() {
    	   $('.facetwp-count').html('');
    	});
        
    })(jQuery);
    

    Then I slap it into my PHP code like so:

    $count_posts = facetwp_display( 'counts' );
    
    the_archive_title( '<h1 class="facetwp-page-title page-title">' . $title, ' (' . $count_posts . '<span class="facetwp-count"></span>)</h1>' );
    

    The content for $count_posts shows nothing while it’s loading, so the check for facetwp-loaded will handle it perfectly.

  • Yes, You Can Use Enqueues

    Yes, You Can Use Enqueues

    One of the battles I face with plugins is explaining to people that they really do need to use wp_enqueue_scripts() for their code. And often I get an argument back that they can’t because they need to include parameters.

    Hold on to your hats. You totally can.

    The Misconception

    It’s easy to get confused with code. There are so many different ways to solve the same problem, we get twisted around. Let’s say that you wanted to include the following script in your website:

    <script src="https://mycoolsite.com/widget.js" async></script>
    

    That’s pretty straightforward in WordPress:

    wp_enqueue_script( 'my-widget', 'https://mycoolsite.com/widget.js', array( 'jquery' ), 1.0.0 );<
    

    But. What if you wanted to add this as well:

    <script type="text/javascript">
        var _widget_options = {
            theme: 'slow' // Choose from 'fast', 'stop', and 'slow'. Remove this property to get the default theme.
    };
    </script>
    

    Now you clearly have to hand-code this into WordPress. Right?

    Wrong!

    Use Inline Scripts!

    You may have heard about wp_add_inline_script before. If not, don’t feel bad. What this does is add extra, inline, code to an already enqueued script.

    Which means that to add the extra code, you do this:

    wp_add_inline_script( 'my-widget', 'var _widget_options = { theme: "slow" }', 'before' );
    

    Which will echo out exactly what you need.

    The cool thing about this, is what if you want your plugin to have a lot of options? Like you want to use the value for an option to determine what your script should do?

    $my-widget-inline = 'var _widget_options = { theme: "' . get_option( 'my-widget-theme' ) . '" }';
    wp_add_inline_script( 'my-widget', $my-widget-inline, 'before' );
    

    And now you’ve got flexibility.

    Keep In Mind…

    First, the name of the script matters. If you enqueue it as ‘my-widget’ then you call it as ‘my-widget’ as the first parameter in your inline script.

    Second, you can change ‘before’ to ‘after’ if you need it to be after the script.

    Third, as with all things, make sure you only load your javascript when you must. No one likes a slow site because you’re loading your javascript on every page when it only needs to be on a specific custom post type.