labels->name ?? $tax_slug;
// Get associated post types to place it in the correct menu structure
$object_types = $tax_obj->object_type ?? array();
$parent_slug = 'edit.php'; // Fallback to posts
if ( ! empty( $object_types ) ) {
$first_pt = $object_types[0];
if ( $first_pt !== 'post' ) {
$parent_slug = 'edit.php?post_type=' . $first_pt;
}
}
add_submenu_page(
$parent_slug,
'Reorder ' . $label,
'Reorder ' . $label,
'manage_categories',
'metaforge_term_reorder_' . $tax_slug,
function () use ( $tax_slug, $tax_obj ) {
$this->reorder_page( $tax_slug, $tax_obj ); }
);
}
}
// Enqueue Scripts -------------------------------------------------------
public function enqueue_scripts( $hook ) {
if ( strpos( $hook, 'metaforge_term_reorder_' ) === false ) {
return;
}
wp_enqueue_script( 'metaforge-sortable-js' ); // registered by MetaForge_Loader
$js_ver = METAFORGE_VERSION;
wp_enqueue_script( 'metaforge-taxonomy-order-js', METAFORGE_URL . 'assets/js/taxonomy-order.js', array( 'jquery', 'metaforge-sortable-js' ), $js_ver, true );
wp_localize_script(
'metaforge-taxonomy-order-js',
'metaforgeTaxOrderConfig',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'metaforge_term_order_nonce' ),
)
);
// admin.css is already enqueued by MetaForge_Loader for $is_reorder pages
}
// Settings Page ---------------------------------------------------------
public function save_settings() {
if ( isset( $_POST['metaforge_save_term_order_settings'] ) ) {
check_admin_referer( 'metaforge_term_order_settings_action', 'metaforge_term_order_settings_nonce' );
if ( ! current_user_can( 'manage_metaforge' ) ) {
wp_die( 'Insufficient permissions.' );
}
$enabled = array();
if ( isset( $_POST['enabled_taxonomies'] ) && is_array( $_POST['enabled_taxonomies'] ) ) {
$enabled = array_map( 'sanitize_key', $_POST['enabled_taxonomies'] );
}
update_option( self::SETTINGS_KEY, array( 'enabled_taxonomies' => $enabled ) );
flush_rewrite_rules();
wp_safe_redirect( admin_url( 'admin.php?page=metaforge_term_order&settings-updated=true' ) );
exit;
}
}
public function settings_page() {
if ( ! current_user_can( 'manage_metaforge' ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice query flag.
$saved = isset( $_GET['settings-updated'] ) && sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) === 'true';
$settings = get_option( self::SETTINGS_KEY, array( 'enabled_taxonomies' => array() ) );
$enabled_taxs = $settings['enabled_taxonomies'] ?? array();
$builtin = get_taxonomies(
array(
'public' => true,
'_builtin' => true,
),
'objects'
);
$metaforge_taxs = get_option( 'metaforge_taxonomies', array() );
$all_taxs = $builtin;
foreach ( $metaforge_taxs as $tax ) {
$slug = $tax['slug'] ?? '';
if ( $slug && ( $obj = get_taxonomy( $slug ) ) ) {
$all_taxs[ $slug ] = $obj;
}
}
?>
labels->name ?? $tax_slug;
// Ensure all existing terms have an order meta value before displaying
$this->lazy_init_order( $tax_slug );
// Fetch terms sorted by saved order - bypass apply_term_order by using explicit args
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for taxonomy term reordering interface.
$terms = get_terms(
array(
'taxonomy' => $tax_slug,
'hide_empty' => false,
'meta_key' => self::META_KEY,
'orderby' => 'meta_value_num',
'order' => 'ASC',
)
);
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
if ( is_wp_error( $terms ) ) {
$terms = array();
}
?>
'Permission denied.' ) );
}
$term_ids = isset( $_POST['order'] ) ? array_map( 'intval', (array) $_POST['order'] ) : array();
$taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( $_POST['taxonomy'] ) : '';
if ( empty( $term_ids ) || empty( $taxonomy ) || ! taxonomy_exists( $taxonomy ) ) {
wp_send_json_error( array( 'message' => 'Invalid data.' ) );
}
// Multiply by 10 to leave gaps for future insertions and
// avoid collisions with lazy_init values (which start from max+10)
foreach ( $term_ids as $position => $term_id ) {
update_term_meta( $term_id, self::META_KEY, ( $position + 1 ) * 10 );
}
wp_send_json_success( array( 'message' => 'Taxonomy order saved.' ) );
}
// Hooks -----------------------------------------------------------------
public function filter_get_terms_args( $args, $taxonomies ) {
$settings = get_option( self::SETTINGS_KEY, array() );
$enabled_taxs = $settings['enabled_taxonomies'] ?? array();
if ( empty( $enabled_taxs ) ) {
return $args;
}
$has_enabled = false;
$current_tax = '';
foreach ( (array) $taxonomies as $tax ) {
if ( in_array( $tax, $enabled_taxs, true ) ) {
$has_enabled = true;
$current_tax = $tax;
break;
}
}
if ( ! $has_enabled ) {
return $args;
}
// Skip lightweight sub-queries — only sort full object result sets
$fields = $args['fields'] ?? '';
if ( in_array( $fields, array( 'count', 'ids', 'tt_ids', 'id=>name', 'id=>slug', 'id=>parent', 'names' ), true ) ) {
return $args;
}
// Only intercept default / name / count ordering
$current_orderby = $args['orderby'] ?? '';
if ( ! empty( $current_orderby ) && ! in_array( $current_orderby, array( 'name', 'count', '' ), true ) ) {
return $args;
}
// Guarantee every term in this taxonomy has a meta record so that the
// INNER JOIN WordPress adds for meta_key does not drop any rows.
// lazy_init_order() is a no-op (single fast SELECT) once all terms
// already have the key, so the overhead is negligible.
$this->lazy_init_order( $current_tax );
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for taxonomy term reordering filter.
$args['meta_key'] = self::META_KEY;
$args['orderby'] = 'meta_value_num';
$args['order'] = 'ASC';
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
return $args;
}
public function set_default_term_order( $term_id, $tt_id, $taxonomy ) {
$settings = get_option( self::SETTINGS_KEY, array() );
$enabled_taxs = $settings['enabled_taxonomies'] ?? array();
if ( ! in_array( $taxonomy, $enabled_taxs, true ) ) {
return;
}
$taxonomy_obj = get_taxonomy( $taxonomy );
if ( ! $taxonomy_obj || ! current_user_can( $taxonomy_obj->cap->edit_terms ) || ! current_user_can( 'edit_term', $term_id ) ) {
return;
}
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Querying max term order meta.
$terms = get_terms(
array(
'taxonomy' => $taxonomy,
'hide_empty' => false,
'fields' => 'ids',
'meta_key' => self::META_KEY,
'orderby' => 'meta_value_num',
'order' => 'DESC',
'number' => 1,
)
);
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
$max = ( ! is_wp_error( $terms ) && ! empty( $terms ) ) ? (int) get_term_meta( $terms[0], self::META_KEY, true ) : 0;
update_term_meta( $term_id, self::META_KEY, $max + 10 );
}
private function lazy_init_order( $taxonomy ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom query needed to lazy-init missing term order metadata.
$term_ids = $wpdb->get_col(
$wpdb->prepare(
"
SELECT tt.term_id
FROM {$wpdb->term_taxonomy} tt
LEFT JOIN {$wpdb->termmeta} tm ON (tt.term_id = tm.term_id AND tm.meta_key = %s)
WHERE tt.taxonomy = %s AND tm.meta_id IS NULL
",
self::META_KEY,
$taxonomy
)
);
if ( empty( $term_ids ) ) {
return;
}
// Find the current max order
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom query needed to get maximum term order.
$max = (int) $wpdb->get_var(
$wpdb->prepare(
"
SELECT MAX(CAST(meta_value AS SIGNED))
FROM {$wpdb->termmeta}
WHERE meta_key = %s
",
self::META_KEY
)
);
// Update the missing terms starting from max + 10
$position = $max + 10;
foreach ( $term_ids as $term_id ) {
update_term_meta( $term_id, self::META_KEY, $position );
$position += 10;
}
}
}
}