From 045dcbdd0ac95a17ad0195b8e93bb2347969f090 Mon Sep 17 00:00:00 2001 From: Hiroshi Urabe <urabe@ham.works> Date: Tue, 19 Apr 2022 06:41:36 +0900 Subject: [PATCH] term ui --- composer.json | 1 + includes/Assets.php | 7 +- includes/Plugin.php | 1 + includes/Term_UI.php | 113 ++++++ lib/JJJ/WP/Term/Meta/UI.php | 733 ++++++++++++++++++++++++++++++++++++ package.json | 4 +- src/admin/index.ts | 10 + src/{ => editor}/index.tsx | 0 8 files changed, 863 insertions(+), 6 deletions(-) create mode 100644 includes/Term_UI.php create mode 100644 lib/JJJ/WP/Term/Meta/UI.php create mode 100644 src/admin/index.ts rename src/{ => editor}/index.tsx (100%) diff --git a/composer.json b/composer.json index b59b897..5bcf29a 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "autoload": { "psr-4": { "HAMWORKS\\WP\\Schedule_Terms\\": "./includes", + "JJJ\\WP\\Term\\Meta\\": "./lib/JJJ/WP/Term/Meta", "HAMWORKS\\WP\\Schedule_Terms\\Tests\\": "./tests" } }, diff --git a/includes/Assets.php b/includes/Assets.php index 4da69cf..10302bf 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -23,20 +23,19 @@ public function __construct() { * Enqueue block editor assets. */ public function enqueue_block_editor_assets() { - $asset_file = include plugin_dir_path( __DIR__ ) . 'build/index.asset.php'; + $asset_file = include plugin_dir_path( __DIR__ ) . 'build/editor.asset.php'; foreach ( $asset_file['dependencies'] as $style ) { wp_enqueue_style( $style ); } - wp_register_script( + wp_enqueue_script( 'schedule-terms', - plugins_url( 'build/index.js', __DIR__ ), + plugins_url( 'build/editor.js', __DIR__ ), $asset_file['dependencies'], $asset_file['version'], true ); - wp_enqueue_script( 'schedule-terms' ); } } diff --git a/includes/Plugin.php b/includes/Plugin.php index f7b0933..619f9f2 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -17,6 +17,7 @@ class Plugin { */ public function __construct() { new Assets(); + new Term_UI( __DIR__ ); } } diff --git a/includes/Term_UI.php b/includes/Term_UI.php new file mode 100644 index 0000000..d5f5268 --- /dev/null +++ b/includes/Term_UI.php @@ -0,0 +1,113 @@ +<?php +/** + * Term_UI class. + * + * @package Schedule_Terms + */ + +namespace HAMWORKS\WP\Schedule_Terms; + +use JJJ\WP\Term\Meta\UI; + +/** + * Term meta UI controller. + */ +class Term_UI extends UI { + + /** + * Plugin version. + * + * @var string + */ + public $version = '2.0.0'; + + /** + * Database version. + * + * @var string + */ + public $db_version = 202204190000; + + /** + * Metadata key. + * + * @var string + */ + public $meta_key = 'use_schedule'; + + /** + * Constrouctor. + * + * @param string $file Plugin file. + */ + public function __construct( $file = '' ) { + $this->labels = array( + 'singular' => esc_html__( 'Scheduled', 'schedule-posts' ), + ); + + parent::__construct( $file ); + } + + /** + * Enqueue scripts. + */ + public function enqueue_scripts() { + $asset_file = include plugin_dir_path( __DIR__ ) . 'build/admin.asset.php'; + + foreach ( $asset_file['dependencies'] as $style ) { + wp_enqueue_style( $style ); + } + + wp_enqueue_script( + 'schedule-terms-admin', + plugins_url( 'build/admin.js', __DIR__ ), + $asset_file['dependencies'], + $asset_file['version'], + true + ); + } + + /** + * Output. + * + * @param mixed $meta term meta. + */ + protected function format_output( $meta = '' ) { + if ( $meta ) { + ?> + <span data-use-schedule><?php esc_html_e( 'Use Schedule', 'schedule-posts' ); ?></span> + <?php + } + } + + /** + * Output the form field + * + * @param \WP_Term|null $term term meta. + */ + protected function form_field( $term = null ) { + $value = isset( $term->term_id ) + ? $this->get_meta( $term->term_id ) + : ''; + + ?> + <input + type="checkbox" + name="term-<?php echo esc_attr( $this->meta_key ); ?>" + id="term-<?php echo esc_attr( $this->meta_key ); ?>" + <?php checked( ! ! $value, true, true ); ?> + /> + <?php + } + + /** + * Output the form field + */ + protected function quick_edit_form_field() { + ?> + <input type="checkbox" name="term-<?php echo esc_attr( $this->meta_key ); ?>"> + <?php + } + + +} diff --git a/lib/JJJ/WP/Term/Meta/UI.php b/lib/JJJ/WP/Term/Meta/UI.php new file mode 100644 index 0000000..105dae2 --- /dev/null +++ b/lib/JJJ/WP/Term/Meta/UI.php @@ -0,0 +1,733 @@ +<?php +namespace JJJ\WP\Term\Meta; + +/** + * Term Meta UI Class + * + * This class is base helper to be extended by add-ons that may want to + * provide a UI for term meta values. It hooks into several different WordPress + * core actions & filters to add columns to list tables, add fields to forms, + * and handle the sanitization & saving of values. + * + * @since 2.0.0 + * @version 4.0.0 + */ + +// Exit if accessed directly +defined( 'ABSPATH' ) || exit; + +if ( ! class_exists( __NAMESPACE__ . '\\UI' ) ) : + /** + * Main WP Term Meta UI class + * + * @since 2.0.0 + */ + class UI { + + /** + * @var string Plugin version + */ + protected $version = '0.0.0'; + + /** + * @var string Database version + */ + protected $db_version = 202004020001; + + /** + * @var string Database version + */ + protected $db_version_key = ''; + + /** + * @var string Metadata key + */ + protected $meta_key = ''; + + /** + * @var string No value + */ + protected $no_value = '—'; + + /** + * @var array Array of labels + */ + protected $labels = array( + 'singular' => '', + 'plural' => '', + 'descrption' => '' + ); + + /** + * @var string File for plugin + */ + public $file = ''; + + /** + * @var string URL to plugin + */ + public $url = ''; + + /** + * @var string Path to plugin + */ + public $path = ''; + + /** + * @var string Basename for plugin + */ + public $basename = ''; + + /** + * @var array Which taxonomies are being targeted? + */ + public $taxonomies = array(); + + /** + * @var bool Whether to use fancy UI + */ + public $fancy = false; + + /** + * @var bool Whether to show a column + */ + public $has_column = true; + + /** + * @var bool Whether to show fields + */ + public $has_fields = true; + + /** + * @var bool Whether to support quick edit + */ + public $has_quick = true; + + /** + * Hook into queries, admin screens, and more! + * + * @since 2.0.0 + */ + public function __construct( $file = '' ) { + + // Setup plugin + $this->file = $file; + $this->url = plugin_dir_url( $this->file ); + $this->path = plugin_dir_path( $this->file ); + $this->basename = plugin_basename( $this->file ); + + // Initialize late, after taxonomies are likely registered + add_action( 'init', array( $this, 'initialize' ), 999 ); + } + + /** + * Initialize on `init` action so taxonomies are registered + * + * @since 2.0.0 + */ + public function initialize() { + + // Get the targeted taxonomies + $this->taxonomies = $this->get_taxonomies(); + + // A simple filter to allow for UI variations + $this->fancy = apply_filters( "wp_fancy_term_{$this->meta_key}", true ); + + // Bail if no targeted taxonomies + if ( empty( $this->taxonomies ) ) { + return; + } + + // Setup Labels + $this->setup_labels(); + + // Register Meta + $this->register_meta(); + + // Add Hooks + $this->add_hooks(); + } + + /** + * Add the hooks, on the `init` action + * + * @since 2.0.0 + */ + public function add_hooks() { + + // Queries + add_action( 'create_term', array( $this, 'save_meta' ), 10, 3 ); + add_action( 'edit_term', array( $this, 'save_meta' ), 10, 3 ); + + // ajax actions + add_action( "wp_ajax_{$this->meta_key}_terms", array( $this, 'ajax_update' ) ); + + // Term meta orderby + add_filter( 'terms_clauses', array( $this, 'terms_clauses' ), 10, 3 ); + add_filter( 'get_terms_orderby', array( $this, 'get_terms_orderby' ), 10, 3 ); + + // Always hook these in, for ajax actions + foreach ( $this->taxonomies as $value ) { + + // Has column? + if ( true === $this->has_column ) { + add_filter( "manage_edit-{$value}_columns", array( $this, 'add_column_header' ) ); + add_filter( "manage_{$value}_custom_column", array( $this, 'add_column_value' ), 10, 3 ); + add_filter( "manage_edit-{$value}_sortable_columns", array( $this, 'sortable_columns' ) ); + } + + // Has fields? + if ( true === $this->has_fields ) { + add_action( "{$value}_add_form_fields", array( $this, 'add_form_field' ) ); + add_action( "{$value}_edit_form_fields", array( $this, 'edit_form_field' ) ); + } + } + + // Only blog admin screens + if ( is_blog_admin() || doing_action( 'wp_ajax_inline_save_tax' ) ) { + add_action( 'admin_init', array( $this, 'admin_init' ) ); + add_action( 'load-edit-tags.php', array( $this, 'edit_tags' ) ); + } + } + + /** + * Register term meta, key, and callbacks + * + * @since 2.0.0 + */ + public function register_meta() { + register_meta( 'term', $this->meta_key, array( + 'auth_callback' => array( $this, 'auth_callback' ), + 'sanitize_callback' => array( $this, 'sanitize_callback' ) + ) ); + } + + /** + * Stub method for sanitizing meta data + * + * @since 2.0.0 + * + * @param mixed $data + * @return mixed + */ + public function sanitize_callback( $data = '' ) { + return $data; + } + + /** + * Stub method for authorizing the saving of meta data + * + * @since 2.0.0 + * + * @param bool $allowed + * @param string $meta_key + * @param int $post_id + * @param int $user_id + * @param string $cap + * @param array $caps + * + * @return boolean + */ + public function auth_callback( $allowed = false, $meta_key = '', $post_id = 0, $user_id = 0, $cap = '', $caps = array() ) { + + // Bail if incorrect meta key + if ( $meta_key !== $this->meta_key ) { + return $allowed; + } + + return $allowed; + } + + /** + * Administration area hooks + * + * @since 2.0.0 + */ + public function admin_init() { + + // Check for DB update + $this->maybe_upgrade_database(); + } + + /** + * Administration area hooks + * + * @since 2.0.0 + */ + public function edit_tags() { + + // Enqueue javascript + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + add_action( 'admin_head', array( $this, 'help_tabs' ) ); + add_action( 'admin_head', array( $this, 'admin_head' ) ); + + // Quick edit + if ( true === $this->has_quick ) { + add_action( 'quick_edit_custom_box', array( $this, 'quick_edit_meta' ), 10, 3 ); + } + } + + /** Get Terms *************************************************************/ + + /** + * Filter `get_terms_orderby` and tweak for meta_query orderby's + * + * @since 2.0.0 + * + * @param string $orderby + * @param array $query_vars + * @param array $taxonomies + */ + public function get_terms_orderby( $orderby = '', $query_vars = array(), $taxonomies = array() ) { + + // Bail if not a target taxonomy + if ( ! $this->is_taxonomy( $taxonomies ) ) { + return $orderby; + } + + // Ordering by meta key + if ( ! empty( $_REQUEST['orderby'] ) && ( $this->meta_key === $_REQUEST['orderby'] ) ) { + $orderby = 'meta_value'; + } + + return $orderby; + } + + /** + * Filter get_terms() and maybe add `meta_query` + * + * @since 2.0.0 + * + * @param array $clauses + * @param array $taxonomies + * @param array $args + */ + public function terms_clauses( $clauses = array(), $taxonomies = array(), $args = array() ) { + global $wpdb; + + // Bail if not a target taxonomy + if ( ! $this->is_taxonomy( $taxonomies ) ) { + return $clauses; + } + + // Default allowed keys & primary key + $allowed_keys = array( $this->meta_key ); + + // Set allowed keys + $allowed_keys[] = 'meta_value'; + $allowed_keys[] = 'meta_value_num'; + + // Tweak orderby + $orderby = isset( $args[ 'orderby' ] ) + ? $args[ 'orderby' ] + : ''; + + // Bail if no orderby or allowed_keys + if ( ! in_array( $orderby, $allowed_keys, true ) ) { + return $clauses; + } + + // Join term meta data + $clauses['join'] .= " INNER JOIN {$wpdb->termmeta} AS tm ON t.term_id = tm.term_id"; + + // Maybe order by term meta + switch ( $args[ 'orderby' ] ) { + case $this->meta_key : + case 'meta_value' : + if ( ! empty( $this->key_type ) ) { + $clauses['orderby'] = "ORDER BY CAST(tm.meta_value AS tm)"; + } else { + $clauses['orderby'] = "ORDER BY tm.meta_value"; + } + $clauses['fields'] .= ', tm.*'; + $clauses['where'] .= " AND tm.meta_key = '{$this->meta_key}'"; + break; + case 'meta_value_num': + $clauses['orderby'] = "ORDER BY tm.meta_value+0"; + $clauses['fields'] .= ', tm.*'; + $clauses['where'] .= " AND tm.meta_key = '{$this->meta_key}'"; + break; + } + + // Return maybe modified clauses + return $clauses; + } + + /** Assets ****************************************************************/ + + /** + * Enqueue quick-edit JS + * + * @since 2.0.0 + */ + public function enqueue_scripts() { } + + /** + * Add help tabs for this metadata + * + * @since 2.0.0 + */ + public function help_tabs() { } + + /** + * Add help tabs for this metadata + * + * @since 2.0.0 + */ + public function admin_head() { } + + /** + * Quick edit ajax updating + * + * @since 2.0.0 + */ + public function ajax_update() {} + + /** + * Setup the singular, plural, and description labels. + * + * @since 4.0.0 + * + * @param array $args + * @return array + */ + public function setup_labels() {} + + /** + * Return the taxonomies used by this plugin + * + * @since 2.0.0 + * + * @param array $args + * @return array + */ + private function get_taxonomies( $args = array() ) { + + // The filter key/tag + $tag = "wp_term_{$this->meta_key}_get_taxonomies"; + + /** + * Allow filtering of affected taxonomies + * + * @since 2.0.0 + */ + $defaults = apply_filters( $tag, array( + 'show_ui' => true + ) ); + + // Parse arguments + $r = wp_parse_args( $args, $defaults ); + + // Get & return the taxonomies + return get_taxonomies( $r ); + } + + /** Columns ***************************************************************/ + + /** + * Add the "meta_key" column to taxonomy terms list-tables + * + * @since 2.0.0 + * + * @param array $columns + * + * @return array + */ + public function add_column_header( $columns = array() ) { + $columns[ $this->meta_key ] = $this->labels['singular']; + + return $columns; + } + + /** + * Output the value for the custom column + * + * @since 2.0.0 + * + * @param string $empty + * @param string $custom_column + * @param int $term_id + * + * @return mixed + */ + public function add_column_value( $empty = '', $custom_column = '', $term_id = 0 ) { + + // Bail if no taxonomy passed or not on the `meta_key` column + if ( empty( $_REQUEST['taxonomy'] ) || ( $this->meta_key !== $custom_column ) || ! empty( $empty ) ) { + return $empty; + } + + // Get the metadata + $meta = $this->get_meta( $term_id ); + $retval = $this->no_value; + + // Output HTML element if not empty + if ( ! empty( $meta ) ) { + $retval = $this->format_output( $meta ); + } + + echo $retval; + } + + /** + * Allow sorting by this `meta_key` + * + * @since 2.0.0 + * + * @param array $columns + * + * @return array + */ + public function sortable_columns( $columns = array() ) { + $columns[ $this->meta_key ] = $this->meta_key; + return $columns; + } + + /** + * Add `meta_key` to term when updating + * + * @since 2.0.0 + * + * @param int $term_id + * @param int $tt_id + * @param string $taxonomy + */ + public function save_meta( $term_id = 0, $tt_id = 0, $taxonomy = '' ) { + + // Bail if not a target taxonomy + if ( ! $this->is_taxonomy( $taxonomy ) ) { + return; + } + + // Get the term being posted + $term_key = 'term-' . $this->meta_key; + + // Bail if not updating meta_key + $meta = ! empty( $_POST[ $term_key ] ) + ? $_POST[ $term_key ] + : ''; + + $this->set_meta( $term_id, $taxonomy, $meta ); + } + + /** + * Set `meta_key` of a specific term + * + * @since 2.0.0 + * + * @param int $term_id + * @param string $taxonomy + * @param string $meta + * @param bool $clean_cache + */ + public function set_meta( $term_id = 0, $taxonomy = '', $meta = '', $clean_cache = false ) { + + // No meta_key, so delete + if ( empty( $meta ) ) { + delete_term_meta( $term_id, $this->meta_key ); + + // Update meta_key value + } else { + update_term_meta( $term_id, $this->meta_key, $meta ); + } + + // Maybe clean the term cache + if ( true === $clean_cache ) { + clean_term_cache( $term_id, $taxonomy ); + } + } + + /** + * Return the `meta_key` of a term + * + * @since 2.0.0 + * + * @param int $term_id + */ + public function get_meta( $term_id = 0 ) { + return get_term_meta( $term_id, $this->meta_key, true ); + } + + /** Markup ****************************************************************/ + + /** + * Output the form field for this metadata when adding a new term + * + * @since 2.0.0 + */ + public function add_form_field() { + ?> + + <div class="form-field term-<?php echo esc_attr( $this->meta_key ); ?>-wrap"> + <label for="term-<?php echo esc_attr( $this->meta_key ); ?>"> + <?php echo esc_html( $this->labels['singular'] ); ?> + </label> + + <?php $this->form_field(); ?> + + <?php if ( ! empty( $this->labels['description'] ) ) : ?> + + <p class="description"> + <?php echo esc_html( $this->labels['description'] ); ?> + </p> + + <?php endif; ?> + + </div> + + <?php + } + + /** + * Output the form field when editing an existing term + * + * @since 2.0.0 + * + * @param object $term + */ + public function edit_form_field( $term = false ) { + ?> + + <tr class="form-field term-<?php echo esc_attr( $this->meta_key ); ?>-wrap"> + <th scope="row" valign="top"> + <label for="term-<?php echo esc_attr( $this->meta_key ); ?>"> + <?php echo esc_html( $this->labels['singular'] ); ?> + </label> + </th> + <td> + <?php $this->form_field( $term ); ?> + + <?php if ( ! empty( $this->labels['description'] ) ) : ?> + + <p class="description"> + <?php echo esc_html( $this->labels['description'] ); ?> + </p> + + <?php endif; ?> + + </td> + </tr> + + <?php + } + + /** + * Output the quick-edit field + * + * @since 2.0.0 + * + * @param $term + */ + public function quick_edit_meta( $column_name = '', $screen = '', $name = '' ) { + + // Bail if not the meta_key column on the `edit-tags` screen for a visible taxonomy + if ( ( $this->meta_key !== $column_name ) || ( 'edit-tags' !== $screen ) || ! $this->is_taxonomy( $name ) ) { + return false; + } ?> + + <fieldset> + <div class="inline-edit-col"> + <label> + <span class="title"><?php echo esc_html( $this->labels['singular'] ); ?></span> + <span class="input-text-wrap"> + + <?php $this->quick_edit_form_field(); ?> + + </span> + </label> + </div> + </fieldset> + + <?php + } + + /** + * Output the form field + * + * @since 2.0.0 + * + * @param $term + */ + protected function form_field( $term = '' ) { + + // Get the meta value + $value = isset( $term->term_id ) + ? $this->get_meta( $term->term_id ) + : ''; ?> + + <input type="text" name="term-<?php echo esc_attr( $this->meta_key ); ?>" id="term-<?php echo esc_attr( $this->meta_key ); ?>" value="<?php echo esc_attr( $value ); ?>"> + + <?php + } + + /** + * Output the form field + * + * @since 2.0.0 + * + * @param $term + */ + protected function quick_edit_form_field() { + ?> + + <input type="text" class="ptitle" name="term-<?php echo esc_attr( $this->meta_key ); ?>" value=""> + + <?php + } + + /** Database Alters *******************************************************/ + + /** + * Should a database update occur + * + * Runs on `init` + * + * @since 2.0.0 + */ + protected function maybe_upgrade_database() { + + // Check DB for version + $db_version = get_option( $this->db_version_key ); + + // Needs + if ( $db_version < $this->db_version ) { + $this->upgrade_database( $db_version ); + } + } + + /** + * Upgrade the database as needed, based on version comparisons + * + * @since 2.0.0 + */ + private function upgrade_database() { + update_option( $this->db_version_key, $this->db_version ); + } + + /** Helpers ***************************************************************/ + + /** + * Compare some taxonomies against the ones for this term meta. + * + * @since 3.0.0 + * + * @param array $taxonomies + */ + private function is_taxonomy( $taxonomies = array() ) { + + // Bail early if empty + if ( empty( $taxonomies ) ) { + return false; + } + + // Always make sure this is an array + $taxonomies = (array) $taxonomies; + + // Check the intersect + $intersect = array_intersect( $taxonomies, $this->taxonomies ); + + // Return + return (bool) ! empty( $intersect ); + } + } +endif; diff --git a/package.json b/package.json index 9d38620..b065406 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "packages-update": "wp-scripts packages-update", "lint-php": "./vendor/bin/phpcs --standard=./.phpcs.xml.dist", "test": "wp-scripts test-unit-js", - "start": "wp-scripts start", - "build": "wp-scripts build", + "start": "wp-scripts start ./src/admin/ ./src/editor/", + "build": "wp-scripts build ./src/admin/ ./src/editor/", "format": "wp-scripts format", "lint-js": "wp-scripts lint-js", "lint-js:fix": "wp-scripts lint-js --fix", diff --git a/src/admin/index.ts b/src/admin/index.ts new file mode 100644 index 0000000..3b45882 --- /dev/null +++ b/src/admin/index.ts @@ -0,0 +1,10 @@ +document.addEventListener( "click", function ( e ) { + const target = e.target as HTMLElement; + if ( target.classList.contains( "editinline" ) ) { + const tr = target.closest( 'tr' ); + const id = tr.id; + const checked = !!document.getElementById( id )?.querySelector( "[data-use-schedule]" ); + const checkbox = document.querySelector( '.inline-edit-row input[name=term-use_schedule]' ) as HTMLInputElement; + checkbox.checked = checked; + } +} ); diff --git a/src/index.tsx b/src/editor/index.tsx similarity index 100% rename from src/index.tsx rename to src/editor/index.tsx