Repeating WordPress Editor

In the event that you should need multiple, WordPress editor instances (perhaps multivariate testing or for various campaigns) it is my hope that this post will get you well on your way. All it requires is an understanding of WordPress meta boxes, passing arrays with form elements and a little jQuery.

Building a Meta Box

At it’s most basic level we’ll be creating a standard, WordPress meta box. However, we’ll be changing the location in which it is loaded and adding some scripts to make it repeatable. If you need a basic, WordPress meta box to begin with I suggest you use my WordPress meta box generator as a starting point. I wrote a short article explaining how it works as well, in case you need an assist. Below you’ll see a sample output with just one meta box.

/**
 * Generated by the WordPress Meta Box generator
 * at https://jeremyhixon.com/wp-tools/meta-box/
 */

function repeatable_editor_get_meta( $value ) {
	global $post;

	$field = get_post_meta( $post->ID, $value, true );
	if ( ! empty( $field ) ) {
		return is_array( $field ) ? stripslashes_deep( $field ) : stripslashes( wp_kses_decode_entities( $field ) );
	} else {
		return false;
	}
}

function repeatable_editor_add_meta_box() {
	add_meta_box(
		'repeatable_editor-repeatable-editor',
		__( 'Repeatable Editor', 'repeatable_editor' ),
		'repeatable_editor_repeatable_editor_html',
		'page',
		'normal',
		'high'
	);
}
add_action( 'add_meta_boxes', 'repeatable_editor_add_meta_box' );

function repeatable_editor_repeatable_editor_html( $post) {
	wp_nonce_field( '_repeatable_editor_repeatable_editor_nonce', 'repeatable_editor_repeatable_editor_nonce' ); ?>

	<p>
		<label for="repeatable_editor_repeatable_editor_content"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br>
		<textarea name="repeatable_editor_repeatable_editor_content" id="repeatable_editor_repeatable_editor_content" ><?php echo repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' ); ?></textarea>
	
	</p><?php
}

function repeatable_editor_repeatable_editor_save( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
	if ( ! isset( $_POST['repeatable_editor_repeatable_editor_nonce'] ) || ! wp_verify_nonce( $_POST['repeatable_editor_repeatable_editor_nonce'], '_repeatable_editor_repeatable_editor_nonce' ) ) return;
	if ( ! current_user_can( 'edit_post' ) ) return;

	if ( isset( $_POST['repeatable_editor_repeatable_editor_content'] ) )
		update_post_meta( $post_id, 'repeatable_editor_repeatable_editor_content', esc_attr( $_POST['repeatable_editor_repeatable_editor_content'] ) );
}
add_action( 'save_post', 'repeatable_editor_repeatable_editor_save' );

/*
	Usage: repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' )
*/

Here’s what you get as a result. Just a textarea, in it’s own meta box, at the bottom of the page editor window.

WordPress editor window with a custom meta box

In this case, I’m going to be replacing the standard editor with this, repeating one. So I’m going to need to hide the default one and move this one up into its place. If you’re content with it’s location at the bottom then you can skip the next section.

Replacing the Default Editor

Removing the default editor is pretty simple since we already have the code to generate the meta box. All we need to do is move it up in priority and then “unset” the editor.

Nudging our meta box up in priority is probably the easiest part of the whole thing. Simply passing “0” to the “priority” parameter of the add_action that instantiates our meta box will take take of that:

add_action( 'add_meta_boxes', 'repeatable_editor_add_meta_box', 0 );

As you can see below, this moves the meta box just under the default editor. All you can really see is that it’s now above my SEO meta box. However, that SEO meta box is instantiated much higher in my functions.php file so the “0” in the “priority” parameter of the new meta box is the only reason it now appears at the bottom.

WordPress editor window with a custom meta box directly underneath the default editor

Hiding the default editor is a little bit more complicated.

function repeatable_editor_add_meta_box() {
	global $_wp_post_type_features;
	if (isset($_wp_post_type_features['page']['editor']) && $_wp_post_type_features['page']['editor']) {
		unset($_wp_post_type_features['page']['editor']);
	}
	add_meta_box(
		'repeatable_editor-repeatable-editor',
		__( 'Repeatable Editor', 'repeatable_editor' ),
		'repeatable_editor_repeatable_editor_html',
		'page',
		'normal',
		'high'
	);
}
add_action( 'add_meta_boxes', 'repeatable_editor_add_meta_box', 0 );

We’ll need to check and see if the editor is one of the features of the “page” post type. We do this by pulling in the $_wp_post_type_features global variable. This variable is an associative array of the objects needed to put together the edit pages for the various post types. Using if (isset($_wp_post_type_features['page']['editor']) && $_wp_post_type_features['page']['editor']) will tell us if the “editor” key or the “page” post type is set and verify that it isn’t “false”. As long as these criteria are met we can then remove the editor by using unset($_wp_post_type_features['page']['editor']);. This should completely remove the default editor:

WordPress editor window with a custom meta box and no default editor

Initializing a New Editor in Our Meta Box

Now that we’ve replaced the default editor with our custom meta box I’d like our textarea to load as a WordPress style editor. Luckily, WordPress supplies a function for this. All we need to do is replace the HTML in our meta box callback with the function that generates a WordPress style editor.

function repeatable_editor_repeatable_editor_html( $post) {
	wp_nonce_field( '_repeatable_editor_repeatable_editor_nonce', 'repeatable_editor_repeatable_editor_nonce' );
	 ?><div class="content-row">
		<label for="repeatable_editor_repeatable_editor_content"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br>
		<?php wp_editor( repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' ), 'repeatable_editor_repeatable_editor_content', array(
		    'wpautop'       => true,
		    'textarea_name' => 'repeatable_editor_repeatable_editor_content',
		    'textarea_rows' => 10,
		) ); ?>
	</div><?php
}

At it’s core the function takes the value of the editor (which we got from the script generated in the beginning) and “ID” for the editor itself and a series of arguments. I’m supplying it with “wpautop” to generate paragraphs automatically, “textarea_name” because we’ll need that later and “textarea_rows” because I wanted it to more closely resemble the default. There are lots of arguments to play with though. I also changed the wrapper for the whole thing from a paragraph to a div because wp_editor generates a lot of divs and wrapping paragraphs around divs never works. The div has a class of “content-row” that I can use as a selector later on in my jQuery statements.

WordPress editor window with a custom meta box styled like the default editor

That’s the basics of creating our own meta box, moving it to the top, removing the default editor and making our meta box look and behave like the default editor. Next comes the part where we make it repeatable.

Repeating the Editor

First off, a brief overview of passing arrays through form inputs. It’s very easy to do, you just append square brackets [] to the form element’s “name” attribute, but a little more complicated to understand in terms of storing those arrays in the database and retrieving them later.

Making the Editor Send as an Array

'textarea_name' => 'repeatable_editor_repeatable_editor_content[]',

In the wp_editor function arguments we add the square brackets to the end of the “textarea_name”. This will cause the textarea’s value to be sent as an array to the page’s “save” function. The fields will still be accessible through the normal $_POST['repeatable_editor_repeatable_editor_content'] method but this value will be an indexed array instead. So our first value will be stored in $_POST['repeatable_editor_repeatable_editor_content'][0].

Having done that, we’ll need to make some changes to our save dialog to make the value we’re passing sage to store in the database.

Storing the Array Data in the Database

function repeatable_editor_repeatable_editor_save( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
	if ( ! isset( $_POST['repeatable_editor_repeatable_editor_nonce'] ) || ! wp_verify_nonce( $_POST['repeatable_editor_repeatable_editor_nonce'], '_repeatable_editor_repeatable_editor_nonce' ) ) return;
	if ( ! current_user_can( 'edit_post' ) ) return;

	if ( isset( $_POST['repeatable_editor_repeatable_editor_content'] ) ) {
		$contents = base64_encode( serialize( $_POST['repeatable_editor_repeatable_editor_content'] ) );
		update_post_meta( $post_id, 'repeatable_editor_repeatable_editor_content', $contents );
	}
}

Since I added another line within the if ( isset( $_POST['repeatable_editor_repeatable_editor_content'] ) ) I wrapped it all in curly brackets. To make it easier to read I’m putting the value of the form elements into a variable after serializing and base64 encoding it. Serializing is a common method for preparing arrays for being stored in a database. It essentially converts the array to a string. Using base64_encode will insure that your HTML data, within the array, survives being stored in the database. Without it you’re almost sure to see errors when you attempt to unserialize the array on the other end.

Creating Multiple Editors Using Content from the Database

Let’s imagine that, in the near future, we’ll be displaying more than one editor in our meta box. That’s what we’re here for, right? We need to prepare our meta box to handle outputting multiple editors with different values. Since we’re storing these values in the database as an array we can use that array to generate all our editors.

function repeatable_editor_repeatable_editor_html( $post) {
	wp_nonce_field( '_repeatable_editor_repeatable_editor_nonce', 'repeatable_editor_repeatable_editor_nonce' );

	$contents = repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' );
	if ($contents && !empty($contents)) {
		$contents = unserialize( base64_decode( $contents ) );
	} else {
		$contents = array('');
	}
	for ($i = 0; $i < count($contents); $i++) {
		?><p>
			<label for="repeatable_editor_repeatable_editor_content_<?php echo $i; ?>"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br>
			<?php wp_editor( $contents[$i], 'repeatable_editor_repeatable_editor_content_' . $i, array(
			    'wpautop'       => true,
			    'textarea_name' => 'repeatable_editor_repeatable_editor_content[]',
			    'textarea_rows' => 10,
			) ); ?>
		</p><?php
	}
}

Above I’m getting the values stored in the database and storing it in $contents. Then checking to see if there is actually anything store there or if it’s empty. If it’s empty or false I just replace it with an array with only one, empty value so that we’ll get a blank editor window. If there is a value stored there then I need to un-serialize and un-encode it. We then loop through that array to create our editors. One thing you have to remember to change is the first parameter of wp_editor, which is the value. Since we’re retrieving the values and looping through them you just want to replace repeatable_editor_get_meta( $contents[$i] ) with just $contents[$i]. I’m using a for loop because I need to use the index to give each editor a unique ID.

You won’t really see much difference if you save now and check your work as we’ve not duplicated any editors and stored the data. Let’s create that functionality now.

Creating New Editors with jQuery

What I’ll be doing is including a script that gets the number of editors currently in the page and creating a button that increases that count by 1 each time, outputs another textarea and initializes an instance of TinyMCE with it.

<p><a class="button" href="#" id="add_content">New Content</a></p>
<script>
	var startingContent = <?php echo count($contents) - 1; ?>;
	jQuery('#add_content').click(function(e) {
		e.preventDefault();
		startingContent++;
		var contentID = 'repeatable_editor_repeatable_editor_content_' + startingContent,
			contentRow = '<p class="content-row"><label for="' + contentID + '"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br><textarea name="repeatable_editor_repeatable_editor_content[]" id="' + contentID + '" rows="10"></textarea></p>';

		jQuery('.content-row').eq(jQuery('.content-row').length - 1).after(contentRow);
		tinymce.init({ selector: '#' + contentID });
	});
</script>

The code above goes right after the for loop in our meta box callback. So that it shows up underneath our generated content editors. The startingContent variable holds the index of the current, last content editor, initially 0. Each time the “New Content” button is clicked the startingContent variable is increased by 1 and used to generate the ID of the new editor. The code appended after the last editor is just a label and textarea to start out. After it’s added though we run tinymce.init on the new textarea to initialize a new editor.

I was unable to find a way to recreate the WordPress style editor programatically through JS so I settled for the basic, tinymce editor. If you save the page and allow it to reload though you will get the WordPress editor in it’s place as it’s being created from the database entries instead of dynamically through jQuery.

WordPress editor window with 2 content editors

Deleting Content Editors

You may need to get rid of your content editors dynamically as well. That’s relatively easy as well. We just need to add a delete button and some jQuery to manage the action.

<a class="content-delete" href="#" style="color:#a00;float:right;text-decoration:none">Delete the Above Content</a><br style="clear:both">

A simple link we can output with each editor that we can use to remove that editor if needs be

jQuery(document).on('click', '.content-delete', function() {
	if (
		jQuery('.content-row').length > 1 &&
		confirm('Are you sure you want to delete this content?')
	) {
		jQuery(this).parent().remove();
	}
});

jQuery’s .on function is used to monitor the DOM in realtime so that anything created dynamically can still fire the associated events without needing to be restated. So, on each “click” of elements with the “content-delete” class we’re checking to make sure we’re not deleting the last of the content editors, as we need at least one. If there’s more than one we’re targeting the parent of the delete link and removing it. You can then save your page and the content should be gone.

WordPress editor window with 2 content editors including links to delete

The Final, Complete Code

function repeatable_editor_get_meta( $value ) {
	global $post;

	$field = get_post_meta( $post->ID, $value, true );
	if ( ! empty( $field ) ) {
		return is_array( $field ) ? stripslashes_deep( $field ) : stripslashes( wp_kses_decode_entities( $field ) );
	} else {
		return false;
	}
}

function repeatable_editor_add_meta_box() {
	global $_wp_post_type_features;
	if (isset($_wp_post_type_features['page']['editor']) && $_wp_post_type_features['page']['editor']) {
		unset($_wp_post_type_features['page']['editor']);
	}
	add_meta_box(
		'repeatable_editor-repeatable-editor',
		__( 'Repeatable Editor', 'repeatable_editor' ),
		'repeatable_editor_repeatable_editor_html',
		'page',
		'normal',
		'high'
	);
}
add_action( 'add_meta_boxes', 'repeatable_editor_add_meta_box', 0 );

function repeatable_editor_repeatable_editor_html( $post) {
	wp_nonce_field( '_repeatable_editor_repeatable_editor_nonce', 'repeatable_editor_repeatable_editor_nonce' );

	$contents = repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' );
	if ($contents && !empty($contents)) {
		$contents = unserialize( base64_decode( $contents ) );
	} else {
		$contents = array('');
	}
	for ($i = 0; $i < count($contents); $i++) {
		?><div class="content-row">
			<label for="repeatable_editor_repeatable_editor_content_<?php echo $i; ?>"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br>
			<?php wp_editor( $contents[$i], 'repeatable_editor_repeatable_editor_content_' . $i, array(
			    'wpautop'       => true,
			    'textarea_name' => 'repeatable_editor_repeatable_editor_content[]',
			    'textarea_rows' => 10,
			) ); ?>
			<a class="content-delete" href="#" style="color:#a00;float:right;text-decoration:none">Delete the Above Content</a><br style="clear:both">
		</div><?php
	}
	?>
	<p><a class="button" href="#" id="add_content">New Content</a></p>
	<script>
		var startingContent = <?php echo count($contents) - 1; ?>;
		jQuery('#add_content').click(function(e) {
			e.preventDefault();
			startingContent++;
			var contentID = 'repeatable_editor_repeatable_editor_content_' + startingContent,
				contentRow = '<p class="content-row"><label for="' + contentID + '"><?php _e( 'Content', 'repeatable_editor' ); ?></label><br><textarea name="repeatable_editor_repeatable_editor_content[]" id="' + contentID + '" rows="10"></textarea></p>';

			jQuery('.content-row').eq(jQuery('.content-row').length - 1).after(contentRow);
			tinymce.init({ selector: '#' + contentID });
		});
		jQuery(document).on('click', '.content-delete', function() {
			if (
				jQuery('.content-row').length > 1 &&
				confirm('Are you sure you want to delete this content?')
			) {
				jQuery(this).parent().remove();
			}
		});
	</script>
	<?php
}

function repeatable_editor_repeatable_editor_save( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
	if ( ! isset( $_POST['repeatable_editor_repeatable_editor_nonce'] ) || ! wp_verify_nonce( $_POST['repeatable_editor_repeatable_editor_nonce'], '_repeatable_editor_repeatable_editor_nonce' ) ) return;
	if ( ! current_user_can( 'edit_post' ) ) return;

	if ( isset( $_POST['repeatable_editor_repeatable_editor_content'] ) ) {
		$contents = base64_encode( serialize( $_POST['repeatable_editor_repeatable_editor_content'] ) );
		update_post_meta( $post_id, 'repeatable_editor_repeatable_editor_content', $contents );
	}
}
add_action( 'save_post', 'repeatable_editor_repeatable_editor_save' );

That’s essentially all the parts needed to duplicate the editors in WordPress. There are different uses for multiple contents so I’ll cover using these in your templates

Using Multiple Content Areas

This is not unlike the way we get the values for the editors in the edit screen. There are, however, multiple ways that you can use this data.

The reason for my developing of this process was for showing different content based on a utm_source parameter in the URL. In this case there was an additional field being stored to provide a custom key for the content that matched the URL parameter passed. So my code looked like this:

$contents = repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' );
$utm_source = false;
if ( isset( $_GET['utm_source'] ) && !empty( $_GET['utm_source'] ) ) {
	$utm_source = $_GET['utm_source'];
}
if ( $contents && !empty( $contents ) && $utm_source ) {
	$contents = unserialize( base64_decode( $contents ) );
	echo apply_filters( 'the_content', $contents[$utm_source] );
}

In this snippet I’m checking to see if there’s content stored for this page and making sure that there’s a utm_source provided to serve as the key for our content array.

You might also look through the content to add a section for each.

$contents = repeatable_editor_get_meta( 'repeatable_editor_repeatable_editor_content' );
if ( $contents && !empty( $contents ) ) {
	$contents = unserialize( base64_decode( $contents ) );
	foreach ($contents as $content) {
?>		
		<section>
			<?php echo apply_filters( 'the_content', $content ); ?>
		</section>
<?php
	}
}

Basically the same process but looping through all the content areas and outputting them into sections instead of just outputting one content area based on a variable.

Share

Comments


Notice: ob_end_flush(): failed to send buffer of zlib output compression (0) in /home/jhixon/public_html/wp-includes/functions.php on line 3722