'', 'title' => __( 'Additional Options', 'ski' ), 'desc' => __( '', 'ski' ), 'screens' => array( 'post' ), 'context' => 'advanced', 'priority' => 'default', 'controls' => array() ); $this->args = (object)wp_parse_args( $args, $defaults ); // Respect the WordPress timing for creation and saving of the metabox. add_action( 'add_meta_boxes', array( $this, 'add' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'save_post', array( $this, 'save' ) ); foreach ( $this->args->controls as $control ) { $control->hook(); } } /** * The main entry into creation of metaboxes - calling code defines the * boxes through a custom hook. */ function add() { // Make the call to WP for the metabox. foreach ( $this->args->screens as $screen ) { add_meta_box( $this->args->slug, $this->args->title, array( $this, 'render' ), $screen, $this->args->context, $this->args->priority ); } } /** * Queues up scripts and styles. */ function enqueue() { // Run through the controls. foreach ( $this->args->controls as $control ) { $control->enqueue(); } wp_enqueue_script( 'ski.wp.metabox.js', path::get_ski_directory_uri().'/controls/ski.wp.metabox.js', array(), SKI_VERSION, true ); wp_enqueue_style( 'ski.wp.metabox.css', path::get_ski_directory_uri().'/controls/ski.wp.metabox.css', array(), SKI_VERSION ); wp_enqueue_style( 'jquery-ui.css', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/smoothness/jquery-ui.css', array(), null ); } /** * Displays the contents of this metabox - * the controls should have been created in the constructor. * @param type $post An object representing the current post * * Note: A second parameter is optional that contains the ID, title, callback, and any args * passed from the add_meta_box call; however, this info is already stored on this object */ function render( $post ) { // Display the overall description. echo $this->args->desc; // Run through the controls. foreach ( $this->args->controls as $control ) { // Every control gets a nonce - just do it here to avoid duplicating everywhere else. wp_nonce_field( 'ski_metabox', 'ski_metabox_nonce' ); // Retrieve the value of the control - if not present and there is a starting value, use it. $value = get_post_meta( $post->ID, $control->get_slug(), true ); // Write the markup for the control - unless otherwise noted, all controls // have some text on the left and the actual control on the right. // // Note: When setting up the $class_type_name, do not use 'basename' without ensuring // there are forward slashes (e.g. no backslashes) - it appears that different // results happen based on the PHP version. For example, with these cases: // // Example: $test_1 = ski\mb_text_ctl // 5.4.12: basename( $test_1 ) -> mb_text_ctl // 5.4.19: basename( $test_1 ) -> ski\mb_text_ctl // // Example: $test_2 = ski/mb_text_ctl // 5.4.12: basename( $test_2 ) -> mb_text_ctl // 5.4.19: basename( $test_2 ) -> mb_text_ctl // // It looks like the official documentation states that Windows servers can handle // both kinds of slashes, but other environments can only handle the forward slash. // This must mean that 5.4.12 was broken in this regard: // // http://php.net/manual/en/function.basename.php // $class_type_name = basename( \ski\path::force_forward_slashes( get_class( $control ) ) ); $class_slug_name = 'container-'.$control->get_slug(); echo '
'. $control->get_label_markup(). '
'. $control->get_desc_markup(). '
'. '
'; $control->render( $value ); echo '
'. '
'; } } /** * The validation checking is pretty much verbatim from the WP codex. This function * does a bunch of security-checking, then runs through the controls to ask for the * POST value from each, then places that value in the database. * @param integer $post_id The ID of the current post. * @link http://codex.wordpress.org/Function_Reference/sanitize_text_field */ function save( $post_id ) { // Check if our nonce is set. if ( ! isset( $_POST['ski_metabox_nonce'] ) ) return; // Verify that the nonce is valid. if ( !wp_verify_nonce( $_POST['ski_metabox_nonce'], 'ski_metabox' ) ) return; // If this is an autosave, our form has not been submitted, so we don't want to do anything. if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; // Check the user's permissions. if ( isset( $_POST['post_type'] ) && 'page' == $_POST['post_type'] ) { if ( !current_user_can( 'edit_page', $post_id ) ) return; } else { if ( !current_user_can( 'edit_post', $post_id ) ) return; } // Making it here means it is cool to run through the controls - // do the following: // 1. Ask the control for the prepared POST value to save. // 2. Save that value to the database using the control's slug and metabox's post ID. // // If the control's value is not in the POST, then expect an exception to be thrown // from prepare_post_value() that indicates nothing should be updated in the database. foreach ( $this->args->controls as $control ) { try { $value = $control->prepare_post_value(); update_post_meta( $post_id, $control->get_slug(), $value ); } catch ( \Exception $e ) { // Nothing to save - something could be wrong with the POST data. continue; } } } } /** * Acts as an abstract class for displaying and saving controls in metaboxes. */ class mb_ctl { /** * @var array The arguments passed in when this control was initially created. */ protected $args = array(); /** * Constructor. * @param array $args Can be any of the following arguments: * string $slug The unique name to be used as both the control ID and the key in the database. * string $label Text that describes a potential group of desc/control combos; usually bolded and above. * string $desc Text that describes the purpose of the control; sits on the same line as the control. * string $starting_value Used in the case there is no database value. * * string $placeholder Displayed when textbox-like controls have no value. * string $text Main text that can be used next to check/radio controls, as the 'add' button in the media library, etc * array $choices Options for dropdown and radio controls * boolean $multiple Allow for multiple selections in dropdown (TBD) and media library controls * integer $range_min Minimum value allowed typically for constrained controls * integer $range_max Maximum value allowed typically for constrained controls * integer $range_step Amount to change the value typically for constrained controls * string $status Can be one of the following values: info, caution, fail * boolean $allow_html Setting to false not permit HTML to make it into the control value * * string $repeat_ctl The name of the mb_ctl class that will be repeated one under the other * integer $repeat_num The maximum number of times the $repeat_ctl can be repeated; use -1 for infinite */ public function __construct( $args = array() ) { // Fill in missing arguments and store for later use. $defaults = array( 'slug' => 'mb_ctl_dummy', 'label' => __( '', 'ski' ), 'desc' => __( '', 'ski' ), 'starting_value' => '', 'placeholder' => __( '', 'ski' ), 'text' => __( '', 'ski' ), 'choices' => array(), 'multiple' => false, 'range_min' => 0, 'range_max' => 100, 'range_step' => 1, 'status' => 'info', 'allow_html' => false, 'repeat_ctl' => '', 'repeat_num' => 2, ); $this->args = (object)wp_parse_args( $args, $defaults ); } /** * Offers an opportunity to hook into actions/filter - expecting child classes to implement. */ public function hook() { // Intentionally nothing here - expecting child classes to implement. } /** * Offers an opportunity to queue up CSS/JS - this is a stub for derived classes. */ public function enqueue() { // Intentionally nothing here - expecting child classes to implement. } /** * Displays this control - this is a stub for derived classes. * @param type $post_id The ID of the current post. */ public function render( $post_id ) { // Intentionally nothing here - expecting child classes to implement. } /** * Preps the corresponding POST value. * @return string The prepared value from the POST * @link http://codex.wordpress.org/Function_Reference/sanitize_text_field */ public function prepare_post_value() { // Default is to not save - an exception is the trigger for that. throw new \Exception(); } /** * Accessor for the unqiue slug. * @return string This object's slug value */ public function get_slug() { return $this->args->slug; } /** * The label should be uniform for every control - so just lay * it out once and reference it from the render callbacks everywhere. */ public function get_label_markup() { $label = isset( $this->args->label ) ? esc_html( $this->args->label ) : ''; if ( empty( $label ) ) return ''; return '
'.$this->args->label.'
'; } /** * The description should be uniform for every control - so just lay * it out once and reference it from the render callbacks everywhere. */ public function get_desc_markup() { $desc = isset( $this->args->desc ) ? esc_html( $this->args->desc ) : ''; if ( empty( $desc ) ) return ''; return ''.$desc.''; } } /** * Concrete class for a textbox that can be displayed inside of a metabox. */ class mb_checkbox_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); $text = esc_html( $this->args->text ); if ( empty( $value ) ) $value = $this->args->starting_value; echo ''; } /** * Saves the contents of this control. * @param type $post_id The ID of the current post. */ public function prepare_post_value() { // Convert to a meaningful value. $value = isset( $_POST[$this->args->slug] ) && $_POST[$this->args->slug] ? 'on' : 'off'; return $value; } } /** * Concrete class for a date picker that can be displayed inside of a metabox. */ class mb_date_picker_ctl extends mb_ctl { /** * Offers an opportunity to queue up CSS/JS. */ public function enqueue() { wp_enqueue_script( 'jquery-ui-datepicker' ); } /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); if ( empty( $value ) ) $value = $this->args->starting_value; echo ''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for a heading label and control description that can be displayed inside of a metabox. */ class mb_detail_ctl extends mb_ctl { // Intentionally nothing to do since the metabox class handles display of label/desc. } /** * Concrete class for a horizontal divider. */ class mb_divider_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { echo '
'; } } /** * Concrete class for a dropdown that can be displayed inside of a metabox. */ class mb_drop_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); if ( empty( $value ) ) $value = $this->args->starting_value; echo ''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for a media library selector that can be displayed inside of a metabox. */ class mb_media_library_ctl extends mb_ctl { /** * Offers an opportunity to queue up CSS/JS. */ public function enqueue() { wp_enqueue_media(); } /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); $multiple = empty( $this->args->multiple ) ? 'false' : 'true'; $text = esc_attr( $this->args->text ); if ( empty( $value ) ) $value = $this->args->starting_value; // The 'value' is a delimited list of attachment IDs - convert those // IDs to paths, which are both needed in the JS. $paths = array(); foreach ( explode( '|', $value ) as $attachment_id ) { $paths[] = wp_get_attachment_image_src( $attachment_id, 'full' )[0]; } $delimited_paths = implode( '|', $paths ); // Note: The preview images, select button, and remove buttons are all generated through JS. echo ''. ''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for a media library selector that can be displayed inside of a metabox. */ class mb_number_bar_ctl extends mb_ctl { /** * Offers an opportunity to queue up CSS/JS. */ public function enqueue() { wp_enqueue_script( 'jquery-ui-slider' ); } /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = intval( esc_attr( $value ) ); $min = intval( $this->args->range_min ); $max = intval( $this->args->range_max ); $step = $this->args->range_step; if ( $min > $max ) math::swap( $min, $max ); if ( empty( $value ) ) $value = $this->args->starting_value; $value = math::clamp( $value, $min, $max ); echo ''. ''.$min.''. '
'. '
'. '
'. ''.$max.''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Ensure it is a number. if ( !is_numeric( $_POST[$this->args->slug] ) ) return; // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for a set of radio image options that can be displayed inside of a metabox. */ class mb_radio_image_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { if ( empty( $this->args->choices ) ) return; $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); if ( !isset( $value ) ) $value = $this->args->starting_value; if ( empty( $value ) ) $value = array_keys( $this->args->choices )[0]; foreach ( $this->args->choices as $choice => $url ) { $checked = checked( $value, $choice, false ); echo ''; } } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Represents sidebar options: 0, l1, l2, l1r1, r2, r1 * @since 2.1 */ class mb_radio_image_sidebar_layout_ctl extends mb_radio_image_ctl { /** * Constructor. If no layouts are passed in, initialize with all site span layouts. * * @param array $args */ public function __construct( $args = array() ) { if ( !isset( $args['layouts'] ) ) $args['layouts'] = array( '0', 'l1', 'l2', 'l1r1', 'r2', 'r1' ); foreach ( $args['layouts'] as $layout ) { $args['choices'][$layout] = path::get_ski_directory_uri().'/assets/customizer/sidebar-'.$layout.'.png'; } parent::__construct( $args ); } } /** * Represents span options: bleed-wide, bleed-narrow, boxed-narrow * @since 2.1 */ class mb_radio_image_site_span_layout_ctl extends mb_radio_image_ctl { /** * Constructor. If no layouts are passed in, initialize with all site span layouts. * * @param array $args */ public function __construct( $args = array() ) { if ( !isset( $args['layouts'] ) ) $args['layouts'] = array( 'bleed-wide', 'bleed-narrow', 'boxed-narrow' ); foreach ( $args['layouts'] as $layout ) { $args['choices'][$layout] = path::get_ski_directory_uri().'/assets/customizer/span-'.$layout.'.png'; } parent::__construct( $args ); } } /** * Concrete class for a set of radio options that can be displayed inside of a metabox. */ class mb_radio_text_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { if ( empty( $this->args->choices ) ) return; $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); if ( empty( $value ) ) $value = $this->args->starting_value; if ( empty( $value ) ) $value = array_keys( $this->args->choices )[0]; foreach ( $this->args->choices as $choice => $label ) { echo ''; } } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for repeating any of the other mb_ctl * classes one under the other. */ class mb_repeater_ctl extends mb_ctl { /** * The maximum number of controls this repeater supports. */ const MAX_CTLS = 16; /** * @var array Representation of the controls currently in play. */ protected $ctls = array(); /** * Constructor. * @param array $args See the parent comments for a description of the arguments. */ public function __construct( $args ) { // Parent setup. parent::__construct( $args ); // If a bogus number was passed in as the max, then set to infinity. If a number // larger than the number supported was passed in, then clamp it to the max. if ( $this->args->repeat_num < 0 ) $this->args->repeat_num = self::MAX_CTLS; $this->args->repeat_num = min( $this->args->repeat_num, self::MAX_CTLS ); // Create all possible controls - the max number requested. for ( $i = 0; $i < $this->args->repeat_num; $i++ ) { $ctl_args = clone $this->args; $ctl_args->slug = $this->get_index_slug( $i ); $this->ctls[] = new $this->args->repeat_ctl( $ctl_args ); } } /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { // Run through all controls, assign values, and render. $ctl_count = count( $this->ctls ); $value_count = count( $value ); for( $i = 0; $i < $ctl_count; $i++ ) { $child_value = $this->args->starting_value; if ( is_array( $value ) && ( $value_count > $i ) ) { $child_value = $value[$i]; } echo '
'; $ctl = $this->ctls[$i]; $ctl->render( $child_value ); echo '
'; } } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Collect the values from each control and place into an array. $values = array(); foreach ( $this->ctls as $ctl ) { $values[] = $ctl->prepare_post_value(); } return $values; } /** * Generates a unique name for a child control, given it's index. * @param integer $index 0-based index * @return string The unqiue slug */ protected function get_index_slug( $index ) { return $this->args->slug.'__'.$index; } } /** * Concrete class for plain text. */ class mb_text_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { echo $this->args->text; } } /** * Concrete class for a textarea that can be displayed inside of a metabox. */ class mb_text_area_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_textarea( $value ); $placeholder = esc_attr( $this->args->placeholder ); if ( empty( $value ) ) $value = $this->args->starting_value; echo ''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * Concrete class for stylized text. */ class mb_text_notice_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { echo '
'. $this->args->text. '
'; } } /** * Concrete class for a textbox that can be displayed inside of a metabox. */ class mb_textbox_ctl extends mb_ctl { /** * Displays this control. * @param string $value Raw database value to assign */ public function render( $value ) { $id = esc_attr( $this->args->slug ); $value = esc_attr( $value ); $placeholder = esc_attr( $this->args->placeholder ); if ( empty( $value ) ) $value = $this->args->starting_value; echo ''; } /** * Preps the corresponding POST value. * @return string The prepared value from the POST */ public function prepare_post_value() { // Make sure that it is set. if ( !isset( $_POST[$this->args->slug] ) ) throw new \Exception(); // Sanitize user input. $value = $_POST[$this->args->slug]; if ( !$this->args->allow_html ) $value = sanitize_text_field( $value ); return $value; } } /** * From your theme, hook into this function like so: * if ( defined( 'WP_DEBUG' ) ) add_action( 'admin_init', 'ski\metabox_sample' ); * * This function will add a new section to the page/post edit screens with examplesof each control. * Note that doing this and clicking "Publish" will append these bogus settings to your database. */ function metabox_sample( $wp_customize ) { // Sample metabox. $args = array ( 'slug' => 'ski_metabox_sample', 'title' => __( 'Ski.Web Metabox Tests', 'ski' ), 'desc' => __( 'Demonstrates each Ski.Web control that can be used in a metabox.', 'ski' ), 'screens' => array( 'page', 'post' ), 'context' => 'advanced', 'priority' => 'low', 'controls' => array( // Detail new mb_detail_ctl( array( 'slug' => 'ski_meta_detail', 'label' => __( 'detail_ctl (args = label)', 'ski' ), 'desc' => __( 'detail_ctl (args = desc)', 'ski' ) )), // Divider new mb_divider_ctl( array( // None )), // Checkbox new mb_checkbox_ctl( array( 'slug' => 'ski_meta_checkbox', 'desc' => __( 'checkbox_ctl', 'ski' ), 'text' => __( 'additional text', 'ski' ) )), // Date Picker new mb_date_picker_ctl( array( 'slug' => 'ski_meta_date_picker', 'desc' => __( 'date_picker_ctl', 'ski' ) )), // Dropdown new mb_drop_ctl( array( 'slug' => 'ski_meta_dropdown', 'desc' => __( 'drop_ctl', 'ski' ), 'choices' => array( '' => '- Select -', '1' => 'first', '2' => 'second', '3' => 'third' ), )), // Media Library new mb_media_library_ctl( array( 'slug' => 'ski_meta_media_library', 'desc' => __( 'media_library_ctl', 'ski' ), 'multiple' => true, 'text' => __( 'Select an image', 'ski' ) )), // Number Bar new mb_number_bar_ctl( array( 'slug' => 'ski_meta_number_bar', 'desc' => __( 'number_bar_ctl', 'ski' ), 'range_min' => 50, 'range_max' => 40, 'range_step' => 5 )), // Radio Image new mb_radio_image_ctl( array( 'slug' => 'ski_meta_radio_image', 'desc' => __( 'radio_image_ctl', 'ski' ), 'choices' => array( '4' => path::get_ski_directory_uri().'/assets/customizer/logo-1.png', '5' => path::get_ski_directory_uri().'/assets/customizer/logo-2.png', '6' => path::get_ski_directory_uri().'/assets/customizer/logo-3.png' ), )), // Radio Text new mb_radio_text_ctl( array( 'slug' => 'ski_meta_radio_text', 'desc' => __( 'radio_text_ctl', 'ski' ), 'choices' => array( '1' => 'first', '2' => 'second', '3' => 'third' ), )), // Repeater new mb_repeater_ctl( array( 'slug' => 'ski_meta_repeater', 'desc' => __( 'repeater_ctl', 'ski' ), 'placeholder' => __( 'placeholder', 'ski' ), 'repeat_ctl' => 'ski\mb_textbox_ctl', 'repeat_num' => 3, )), // Text new mb_text_ctl( array( 'slug' => 'ski_meta_text', 'desc' => __( 'text_ctl', 'ski' ), 'text' => __( 'text only', 'ski' ) )), // Text area new mb_text_area_ctl( array( 'slug' => 'ski_meta_text_area', 'desc' => __( 'text_area_ctl', 'ski' ), 'placeholder' => __( 'placeholder', 'ski' ) )), // Text notice new mb_text_notice_ctl( array( 'slug' => 'ski_meta_text_notice', 'desc' => __( 'text_notice_ctl', 'ski' ), 'text' => __( 'text notice', 'ski' ), 'status' => 'fail' )), // Textbox new mb_textbox_ctl( array( 'slug' => 'ski_meta_textbox', 'desc' => __( 'textbox_ctl', 'ski' ), 'placeholder' => __( 'placeholder', 'ski' ) )), ) ); // Create the metabox with the plentiful arguments. new metabox( $args ); } } ?>