Initial commit

This commit is contained in:
Cooper Dalrymple
2025-11-18 16:56:26 -06:00
parent 563fd88ec0
commit 4b2e3194be
15 changed files with 6683 additions and 3 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
vendor
lib
node_modules
*.zip

25
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Package",
"type": "shell",
"command": "gulp package",
"group": "build",
"presentation": {
"reveal": "silent",
"panel": "shared",
}
},
{
"label": "Install",
"type": "shell",
"command": "make install",
"group": "build",
"presentation": {
"reveal": "silent",
"panel": "shared",
}
}
]
}

22
Makefile Normal file
View File

@@ -0,0 +1,22 @@
all: install
reinstall: clean install
clean: clean-composer clean-npm
clean-composer:
rm -rf vendor/* || true
rm -rf lib/* || true
rm composer.lock || true
composer clearcache
clean-npm:
rm -rf node_modules/* || true
rm package-lock.json || true
install:
composer install
npm install
package: install
gulp package

View File

@@ -1,3 +0,0 @@
# ogre-sort
Plugin which enables sorting within the WordPress admin area for posts, terms, and posts within terms.

19
assets/sort.css Normal file
View File

@@ -0,0 +1,19 @@
/**
* @package ogre-sort
* @author cleverogre
* @version 1.0.0
* @since 1.0.0
*/
.ui-sortable tr:hover {
cursor: move;
}
.ui-sortable tr.alternate {
background-color: #f9f9f9;
}
.ui-sortable tr.ui-sortable-helper {
background-color: #f9f9f9;
border-top: 1px solid #dfdfdf;
}

70
assets/sort.js Normal file
View File

@@ -0,0 +1,70 @@
/**
* @package ogre-sort
* @author cleverogre
* @version 1.0.0
* @since 1.0.0
*/
jQuery(($) => {
if (typeof window['ogre_sort'] === 'undefined') return;
const update = (args) => {
$.ajax({
type: 'POST',
url: ogre_sort.ajaxurl,
data: args,
success: (response, textStatus, jqXHR) => {
if (response.success) {
console.log('Ogre Sort: Ajax sort request successful');
} else if (typeof response.data !== 'undefined' && response.data != '') {
console.log('Ogre Sort: Ajax sort error. ' + response.data);
} else {
console.log('Ogre Sort: Ajax sort error. Unknown sort error.');
}
},
error: (jqXHR, textStatus, errorThrown) => {
console.log('Ogre Sort: Ajax sort request failed');
},
complete: (jqXHR, textStatus) => {
console.log('Ogre Sort: Ajax request completed');
},
});
};
const fixHelper = (e, ui) => {
ui.children().children().each(function () {
$(this).width($(this).width());
});
return ui;
};
$('table.posts #the-list, table.pages #the-list').sortable({
'items': 'tr',
'axis': 'y',
'helper': fixHelper,
'update': (e, ui) => {
const args = {
action: 'ogre_sort',
relationship: ogre_sort.current_relationship,
order: $('#the-list').sortable('serialize'),
};
if (typeof ogre_sort.term != 'undefined') {
args.term = ogre_sort.term;
}
update(args);
},
});
$('table.tags #the-list').sortable({
'items': 'tr',
'axis': 'y',
'helper': fixHelper,
'update': (e, ui) => {
update({
action: 'ogre_sort',
relationship: ogre_sort.current_relationship,
order: $('#the-list').sortable('serialize'),
});
},
});
});

51
composer.json Normal file
View File

@@ -0,0 +1,51 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "cleverogre/ogre-sort",
"version": "1.0.0",
"title": "Ogre Sort",
"description": "WordPress plugin which enables drag-and-drop sorting within the admin area for posts, terms, and posts within terms.",
"author": "CleverOgre",
"license": "GPL-3.0+",
"keywords": [
"WordPress",
"Plugin",
"Sorting",
"menu_order",
"term_order",
"CPT"
],
"homepage": "https://cleverogre.com",
"repositories": {
"cleverogre/plugin-framework": {
"type": "vcs",
"url": "git@git.cleverogre.com:cleverogre/plugin-framework.git"
},
"wp-package-updater": {
"type": "package",
"package": {
"name": "froger-me/wp-package-updater",
"version": "1.4.0",
"source": {
"url": "https://github.com/froger-me/wp-package-updater.git",
"type": "git",
"reference": "master"
}
}
}
},
"require": {
"cleverogre/plugin-framework": "dev-main",
"simplehtmldom/simplehtmldom": "dev-master"
},
"replace": {
"wpengine/advanced-custom-fields-pro": "*"
},
"scripts": {
"post-update-cmd": [
"php vendor/magicoli/wp-package-updater-lib/install.php"
]
},
"config": {
"optimize-autoloader": true
}
}

243
composer.lock generated Normal file
View File

@@ -0,0 +1,243 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fe877c0f300b012a3667fb458d1768b3",
"packages": [
{
"name": "cleverogre/plugin-framework",
"version": "dev-main",
"source": {
"type": "git",
"url": "git@git.cleverogre.com:cleverogre/plugin-framework.git",
"reference": "5e2937c2bbb7bf2ea51e575e5655f5056956edfe"
},
"require": {
"froger-me/wp-package-updater": "^1.4.0",
"magicoli/wp-package-updater-lib": "^0.1.9",
"wpengine/advanced-custom-fields-pro": "^6.4.2",
"yahnis-elsts/plugin-update-checker": "^5.0"
},
"default-branch": true,
"type": "library",
"extra": {
"installer-paths": {
"acf/": [
"wpengine/advanced-custom-fields-pro"
]
}
},
"autoload": {
"files": [
"plugin-framework.php",
"inc/package-updater.php",
"inc/acf.php",
"inc/trait-singleton.php",
"inc/abstract-plugin.php"
]
},
"scripts": {
"post-update-cmd": [
"php vendor/magicoli/wp-package-updater-lib/install.php"
]
},
"license": [
"GPL-3.0+"
],
"description": "Framework for WordPress plugins created by CleverOgre",
"homepage": "https://cleverogre.com",
"keywords": [
"Framework",
"Plugin",
"WordPress"
],
"time": "2025-10-29T18:37:43+00:00"
},
{
"name": "froger-me/wp-package-updater",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/froger-me/wp-package-updater.git",
"reference": "master"
},
"type": "library"
},
{
"name": "magicoli/wp-package-updater-lib",
"version": "v0.1.10",
"source": {
"type": "git",
"url": "https://github.com/magicoli/wp-package-updater-lib.git",
"reference": "e822132741c08d054fb97f249b1143c220f30e6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/magicoli/wp-package-updater-lib/zipball/e822132741c08d054fb97f249b1143c220f30e6f",
"reference": "e822132741c08d054fb97f249b1143c220f30e6f",
"shasum": ""
},
"require-dev": {
"froger-me/wp-plugin-update-server": "*"
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"AGPL-3.0-or-later"
],
"authors": [
{
"name": "Magiiic"
}
],
"description": "Composer package for wp-package-updater library.",
"support": {
"issues": "https://github.com/magicoli/wp-package-updater-lib/issues",
"source": "https://github.com/magicoli/wp-package-updater-lib/tree/v0.1.10"
},
"time": "2023-06-09T12:42:28+00:00"
},
{
"name": "simplehtmldom/simplehtmldom",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/simplehtmldom/simplehtmldom.git",
"reference": "b8d048e46b7f1964c28ea041d39ccb1d05f9a0ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/simplehtmldom/simplehtmldom/zipball/b8d048e46b7f1964c28ea041d39ccb1d05f9a0ed",
"reference": "b8d048e46b7f1964c28ea041d39ccb1d05f9a0ed",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=5.6"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "*",
"phpcompatibility/php-compatibility": "^9",
"phpunit/phpunit": "^6 || ^7",
"squizlabs/php_codesniffer": "^2"
},
"suggest": {
"ext-curl": "Needed to support cURL downloads in class HtmlWeb",
"ext-mbstring": "Allows better decoding for multi-byte documents",
"ext-openssl": "Allows loading HTTPS pages when using cURL"
},
"default-branch": true,
"type": "library",
"autoload": {
"classmap": [
"./"
],
"exclude-from-classmap": [
"/example/",
"/docs/",
"/tests/",
"simple_html_dom.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "S.C. Chen",
"role": "Developer"
},
{
"name": "John Schlick",
"role": "Developer"
},
{
"name": "logmanoriginal",
"role": "Developer"
}
],
"description": "A fast, simple and reliable HTML document parser for PHP.",
"homepage": "https://simplehtmldom.sourceforge.io/",
"keywords": [
"Simple",
"dom",
"html",
"parser",
"php",
"simplehtmldom"
],
"support": {
"issues": "https://sourceforge.net/p/simplehtmldom/bugs/",
"rss": "https://sourceforge.net/p/simplehtmldom/news/feed.rss",
"source": "https://sourceforge.net/p/simplehtmldom/repository/",
"wiki": "https://simplehtmldom.sourceforge.io/docs/"
},
"time": "2022-04-25T19:21:05+00:00"
},
{
"name": "yahnis-elsts/plugin-update-checker",
"version": "v5.6",
"source": {
"type": "git",
"url": "https://github.com/YahnisElsts/plugin-update-checker.git",
"reference": "a2db6871deec989a74e1f90fafc6d58ae526a879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/YahnisElsts/plugin-update-checker/zipball/a2db6871deec989a74e1f90fafc6d58ae526a879",
"reference": "a2db6871deec989a74e1f90fafc6d58ae526a879",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.6.20"
},
"type": "library",
"autoload": {
"files": [
"load-v5p6.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Yahnis Elsts",
"email": "whiteshadow@w-shadow.com",
"homepage": "https://w-shadow.com/",
"role": "Developer"
}
],
"description": "A custom update checker for WordPress plugins and themes. Useful if you can't host your plugin in the official WP repository but still want it to support automatic updates.",
"homepage": "https://github.com/YahnisElsts/plugin-update-checker/",
"keywords": [
"automatic updates",
"plugin updates",
"theme updates",
"wordpress"
],
"support": {
"issues": "https://github.com/YahnisElsts/plugin-update-checker/issues",
"source": "https://github.com/YahnisElsts/plugin-update-checker/tree/v5.6"
},
"time": "2025-05-20T12:29:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"cleverogre/plugin-framework": 20,
"simplehtmldom/simplehtmldom": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

59
gulpfile.js Normal file
View File

@@ -0,0 +1,59 @@
const gulp = require('gulp'),
clean = require('gulp-clean'),
path = require('path'),
zip = require('gulp-zip').default;
const NAME = path.basename(__dirname);
// Clean Tasks
gulp.task('clean-package', () => {
return gulp.src(`${NAME}.zip`, {
read: false,
allowEmpty: true,
}).pipe(clean());
});
gulp.task(
'clean',
gulp.series(
'clean-package'
)
);
// Package Tasks
gulp.task('package-compress', () => {
return gulp.src([
'assets/**/*',
'inc/**/*',
'lib/**/*',
'vendor/**/*',
'LICENSE',
`${NAME}.php`,
'readme.txt'
], {
base: './',
encoding: false,
})
.pipe(zip(`${NAME}.zip`))
.pipe(gulp.dest('./'));
});
gulp.task(
'package',
gulp.series(
'clean',
'package-compress'
)
);
// Default Tasks
gulp.task(
'default',
gulp.series(
'clean',
'package'
)
);

160
inc/class-settings.php Normal file
View File

@@ -0,0 +1,160 @@
<?php
/**
* @package ogre-sort
* @author cleverogre
* @version 1.0.0
* @since 1.0.0
*/
namespace Ogre\Sort;
use Ogre\Singleton;
use Ogre\Sort as Plugin;
defined('ABSPATH') || exit;
final class Settings {
use Singleton;
const CAPABILITY = 'manage_options';
protected function __construct() {
add_filter('ogre_sort_relationships', [$this, 'register_relationships'], 10, 1);
add_action('admin_menu', [$this, 'menu']);
add_action('admin_init', [$this, 'init']);
}
public function register_relationships(array $relationships):array {
// Post Types
foreach ($this->get('post_types') as $post_type) {
$relationships[] = ['post_type' => $post_type];
}
// Taxonomies
foreach ($this->get('taxonomies') as $taxonomy) {
$relationships[] = ['taxonomy' => $taxonomy];
}
// Taxonomy Posts
foreach ($this->get('taxonomy_post_types') as $key) {
$parts = explode('~', $key);
if (count($parts) == 2) {
$relationships = [
'post_type' => $parts[0],
'taxonomy' => $parts[1],
];
}
}
return $relationships;
}
private function get(string $key = ''):array {
$data = (array) get_option(Plugin::get_id(), []);
if (!empty($key)) return array_key_exists($key, $data) ? (array) $data[$key] : [];
return $data;
}
private function is_checked(string $name, string $value):bool {
return is_array($option = $this->get($name)) && in_array($value, $option);
}
public function menu() {
if (!current_user_can(self::CAPABILITY)) return;
add_options_page(sprintf(Plugin::__('%s Settings'), Plugin::get_title()), Plugin::get_title(), self::CAPABILITY, Plugin::get_id(), [$this, 'page']);
}
public function init() {
register_setting('options', Plugin::get_id());
add_settings_section('post_types', Plugin::__('Post Types'), [$this, 'section'], Plugin::get_id());
foreach (get_post_types(output: 'objects') as $post_type) {
add_settings_field(
Plugin::get_id() . '_post_type_' . $post_type->name,
$post_type->label,
[$this, 'checkbox'],
Plugin::get_id(),
'post_types',
[
'name' => 'post_types',
'value' => $post_type->name,
]
);
}
add_settings_section('taxonomies', Plugin::__('Taxonomies'), [$this, 'section'], Plugin::get_id());
foreach (get_taxonomies(output: 'objects') as $taxonomy) {
add_settings_field(
Plugin::get_id() . '_taxonomy_' . $post_type->name,
$post_type->label,
[$this, 'checkbox'],
Plugin::get_id(),
'taxonomies',
[
'name' => 'taxonomies',
'value' => $taxonomy->name,
]
);
}
add_settings_section('taxonomy_post_types', Plugin::__('Taxonomy Posts'), [$this, 'section'], Plugin::get_id());
foreach (get_post_types(output: 'objects') as $post_type) {
foreach (get_object_taxonomies($post_type->name, 'objects') as $taxonomy) {
add_settings_field(
Plugin::get_id() . '_post_type_' . $post_type->name . '_taxonomy_' . $taxonomy->name,
sprintf('%s: %s', $post_type->label, $taxonomy->label),
[$this, 'checkbox'],
Plugin::get_id(),
'taxonomy_post_types',
[
'name' => 'taxonomy_post_types',
'value' => $post_type->name . '~' . $taxonomy->name,
]
);
}
}
}
public function page() {
if (!current_user_can(self::CAPABILITY)) return;
// Show update message
if (isset($_GET['settings-updated'])) add_settings_error(Plugin::get_id(), 'updated', Plugin::__('Settings Saved'), 'updated');
// Show error/update messages
settings_errors(Plugin::get_id());
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post"><?php
settings_fields(Plugin::get_id());
do_settings_sections(Plugin::get_id());
submit_button();
?></form>
</div>
<?php
}
public function section():void { }
public function field(array $args):void {
extract($args);
printf(
'<input type="checkbox" name="%s[]" value="%s" %s>',
esc_attr($name),
esc_attr($value),
checked($this->is_checked($name, $value), display: false)
);
}
public static function get_url():string {
return admin_url('options-general.php?page=' . Plugin::get_id());
}
}
Settings::instance();

873
inc/class-sort.php Normal file
View File

@@ -0,0 +1,873 @@
<?php
/**
* @package ogre-sort
* @author cleverogre
* @version 1.0.0
* @since 1.0.0
*/
namespace Ogre\Sort;
use Ogre\Singleton;
use Ogre\Sort as Plugin;
if (!defined('ABSPATH')) exit;
// TODO: Split this class up
final class Sort {
use Singleton;
const TYPE_ORDER = ['term', 'taxonomy', 'post_type'];
protected array $relationships = [];
protected array $current_relationship = [];
protected function __construct() {
$this->relationships = [];
$this->current_relationship = [];
add_action('init', [$this, 'init'], 100);
}
public function validate_relationship(array $relationship):bool {
if (!isset($relationship['post_type']) && !isset($relationship['taxonomy'])) return false;
if (isset($relationship['post_type']) && !post_type_exists($relationship['post_type'])) return false;
if (isset($relationship['taxonomy']) && !taxonomy_exists($relationship['taxonomy'])) return false;
if (isset($relationship['post_type']) && isset($relationship['taxonomy']) && !in_array($relationship['taxonomy'], get_object_taxonomies($relationship['post_type']))) return false;
return true;
}
public function get_relationship_type(array $relationship):bool {
if (isset($relationship['post_type']) && isset($relationship['taxonomy'])) {
return 'term';
} else if (isset($relationship['post_type']) && !isset($relationship['taxonomy'])) {
return 'post_type';
} else if (!isset($relationship['post_type']) && isset($relationship['taxonomy'])) {
return 'taxonomy';
} else {
return false;
}
}
private function get_relationships():array {
$relationships = apply_filters('ogre_sort_relationships', []);
if (!is_array($relationships) || empty($relationships)) return [];
// Filter out invalid post types
$relationships = array_filter($relationships, [$this, 'validate_relationship']);
if (empty($relationships)) return [];
// Assign relationship types
foreach ($relationships as &$relationship) {
$relationship['type'] = $this->get_relationship_type($relationship);
}
// Sort Taxonomy Types to prefer term over post_type
usort($relationships, fn (array $a, array $b):bool => array_search($a['type'], self::TYPE_ORDER) > array_search($b['type'], self::TYPE_ORDER));
return $relationships;
}
public function init() {
$this->relationships = $this->get_relationships();
if (empty($this->relationships)) return;
// Update all order meta
if (is_admin() && function_exists('is_plugin_active') && is_plugin_active('wp-rocket/wp-rocket.php')) {
add_action('admin_post_purge_cache', [$this, 'refresh_purge_cache'], 1);
} else if (empty($_GET) && is_admin()) {
add_action('current_screen', [$this, 'refresh_screen'], 10, 1);
}
// Check if has active relationship in admin, sets current_relationship
if (is_admin()) {
add_action('current_screen', [$this, 'current_screen']);
}
// Post Updating
add_action('save_post', [$this, 'save_post'], 10, 3);
// Post Types
add_action('pre_get_posts', [$this, 'pre_get_posts'], 10, 1);
add_filter('get_previous_post_where', [$this, 'previous_post_where']);
add_filter('get_previous_post_sort', [$this, 'previous_post_sort']);
add_filter('get_next_post_where', [$this, 'next_post_where']);
add_filter('get_next_post_sort', [$this, 'next_post_sort']);
// Taxonomy
add_filter('terms_clauses', [$this, 'terms_clauses'], 10, 3);
add_filter('get_terms_orderby', [$this, 'get_terms_orderby'], 10, 3);
add_filter('wp_get_object_terms', [$this, 'wp_get_object_terms'], 10, 4);
add_filter('get_terms', [$this, 'get_terms'], 10, 4);
add_action('create_term', [$this, 'add_term_relationship'], 10, 3);
// Ajax
add_action('wp_ajax_ogre_sort', [$this, 'sort']);
}
public function current_screen() {
$screen = get_current_screen();
foreach ($this->relationships as $relationship) {
switch ($relationship['type']) {
case 'term':
if ($screen->base == 'edit' && $screen->post_type == $relationship['post_type'] && (isset($_GET[$relationship['taxonomy']]) || (isset($_GET['taxonomy']) && $_GET['taxonomy'] == $relationship['taxonomy']))) {
$this->current_relationship = $relationship;
break;
}
break;
case 'post_type':
if ($screen->base == 'edit' && $screen->post_type == $relationship['post_type']) {
$taxes = get_object_taxonomies($screen->post_type, 'names');
$tax_active = false;
foreach ($taxes as $tax) {
if (isset($_GET[$tax])) {
$tax_active = true;
break;
}
}
if (!$tax_active) $this->current_relationship = $relationship;
break;
}
break;
case 'taxonomy':
if ($screen->base == 'edit-tags' && $screen->taxonomy == $relationship['taxonomy']) {
$this->current_relationship = $relationship;
break;
}
break;
}
if ($this->current_relationship != false) break;
}
if ($this->current_relationship == false) return;
// Ajax
add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
}
public function enqueue_scripts() {
$vars = [
'ajaxurl' => admin_url('admin-ajax.php'),
'current_relationship' => $this->current_relationship,
'relationships' => $this->relationships,
];
if (!empty($term = get_queried_object())) $vars['term'] = $term;
wp_enqueue_script('jquery');
wp_enqueue_script('jquery-ui-sortable');
wp_enqueue_script('ogre-sort', Plugin::get_url('assets/sort.js'), ['jquery'], Plugin::get_version(), true);
wp_localize_script('ogre-sort', 'ogre_sort', $vars);
wp_enqueue_style('ogre-sort', Plugin::get_url('assets/sort/css'), false, Plugin::get_version());
}
// Add Meta when Post is Updated
public function save_post($post_id, $post, $update) {
global $wpdb;
$_post = get_post($post_id);
if (wp_is_post_revision($post_id) || $_post->post_stauts == 'auto-draft') return;
if (empty($this->relationships)) return;
$relationships = [];
foreach ($this->relationships as $relationship) {
switch ($relationship['type']) {
case 'post_type':
if (isset($relationship['post_type']) && $relationship['post_type'] == $_post->post_type) {
$relationships[] = $relationship;
}
break;
case 'term':
$taxonomies = get_object_taxonomies($_post->post_type);
if (isset($relationship['taxonomy']) && is_array($taxonomies) && in_array($relationship['taxonomy'], $taxonomies)) {
$relationships[] = $relationship;
}
break;
}
}
if (empty($relationships)) return;
foreach ($relationships as $relationship) {
switch ($relationship['type']) {
case 'post_type':
if ($_post->post_status == 'draft' && !$update) {
$wpdb->update($wpdb->posts, [
'menu_order' => -1,
], [
'ID' => $post_id,
]);
}
break;
case 'term':
$terms = wp_get_object_terms($post_id, $relationship['taxonomy'], [
'fields' => 'ids',
]);
if (is_array($terms) && !empty($terms)) {
foreach ($terms as $term_id) {
if (metadata_exists('post', $post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}")) continue;
add_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}", -1, true);
}
}
break;
}
}
}
// Query Filters
public function pre_get_posts($wp_query) {
if (empty($this->relationships)) return false;
if (isset($wp_query->query['orderby']) && !empty($wp_query->query['orderby']) && ((is_admin() && isset($_GET['orderby'])) || $wp_query->query['orderby'] != 'date')) return false;
//if (!is_admin() && isset($wp_query->query['suppress_filters'])) return false; // NOTE: suppress_filters set to true by default in get_posts
//if (!isset($wp_query->query['post_type'])) $wp_query->set('post_type', 'post');
$relationships = [];
foreach ($this->relationships as $relationship) {
switch ($relationship['type']) {
case 'post_type':
if (isset($relationship['post_type']) && isset($wp_query->query['post_type']) && $relationship['post_type'] == $wp_query->query['post_type']) {
$relationships[] = $relationship;
}
break;
case 'term':
if (isset($relationship['taxonomy']) && isset($wp_query->query[$relationship['taxonomy']])) {
$relationships[] = $relationship;
} else if (isset($relationship['taxonomy']) && isset($wp_query->query['taxonomy']) && $wp_query->query['taxonomy'] == $relationship['taxonomy']) {
$relationships[] = $relationship;
} else if (isset($relationship['taxonomy']) && isset($wp_query->query['tax_query']) && is_array($wp_query->query['tax_query']) && !empty($wp_query->query['tax_query'])) {
foreach ($wp_query->query['tax_query'] as $tax) {
if (!isset($tax['taxonomy'])) continue;
if ($relationship['taxonomy'] != $tax['taxonomy']) continue;
$relationships[] = $relationship;
break;
}
}
break;
}
}
if (empty($relationships)) return false;
$current_relationship = $relationships[0];
foreach ($relationships as $relationship) {
// Term relationships have higher priority
if ($relationship['type'] == 'term') {
$current_relationship = $relationship;
break;
}
}
if (empty($current_relationship)) $current_relationship = $relationships[0];
if (empty($current_relationship)) return false;
switch ($current_relationship['type']) {
case 'term':
$term_by = 'slug';
$term_id = '';
if (isset($wp_query->query[$current_relationship['taxonomy']])) {
$term_id = $wp_query->query[$current_relationship['taxonomy']];
} else if (isset($wp_query->query['taxonomy']) && $wp_query->query['taxonomy'] == $current_relationship['taxonomy'] && isset($wp_query->query['term'])) {
$term_id = $wp_query->query['term'];
} else if (isset($wp_query->query['tax_query']) && is_array($wp_query->query['tax_query']) && !empty($wp_query->query['tax_query'])) {
foreach ($wp_query->query['tax_query'] as $tax) {
if (!isset($tax['taxonomy'])) continue;
if ($current_relationship['taxonomy'] != $tax['taxonomy']) continue;
$term_by = $tax['field'];
$term_id = $tax['terms'];
break;
}
}
if (empty($term_id)) break;
$term = get_term_by($term_by, $term_id, $current_relationship['taxonomy']);
if (!is_a($term, 'WP_Term')) break;
$wp_query->set('meta_key', "ogre-sort_{$current_relationship['taxonomy']}_{$term->term_id}");
$wp_query->set('orderby', 'meta_value_num');
break;
case 'post_type':
$wp_query->set('orderby', 'menu_order');
break;
}
$wp_query->set('order', 'ASC');
}
public function previous_post_where($where) {
if (empty($this->relationships)) return $where;
global $post;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'post_type' && $relationship['post_type'] == $post->post_type) {
$active = true;
break;
}
}
if ($active == false) return $where;
return str_replace("p.post_date < '{$post->post_date}'", "p.menu_order > '{$post->menu_order}'", $where);
}
public function previous_post_sort($orderby) {
if (empty($this->relationships)) return $orderby;
global $post;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'post_type' && $relationship['post_type'] == $post->post_type) {
$active = true;
break;
}
}
if ($active == false) return $orderby;
return 'ORDER BY p.menu_order ASC LIMIT 1';
}
public function next_post_where($where) {
if (empty($this->relationships)) return $where;
global $post;
if (empty($this->relationships)) return $where;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'post_type' && $relationship['post_type'] == $post->post_type) {
$active = true;
break;
}
}
if ($active == false) return $where;
return str_replace("p.post_date > '{$post->post_date}'", "p.menu_order < '{$post->menu_order}'", $where);
}
public function next_post_sort($orderby) {
if (empty($this->relationships)) return $orderby;
global $post;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'post_type' && $relationship['post_type'] == $post->post_type) {
$active = true;
break;
}
}
if ($active == false) return $orderby;
return 'ORDER BY p.menu_order DESC LIMIT 1';
}
public function terms_clauses($pieces, $taxonomies, $args) {
global $wpdb;
if ((is_admin() && isset($_GET['orderby'])) || empty($this->relationships) || !isset($pieces['fields']) || strpos($pieces['fields'], 'tr.') || !isset($pieces['join']) || strpos($pieces['join'], $wpdb->term_relationships) != false) return $pieces;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'taxonomy' && $args['taxonomy'] != NULL && ($relationship['taxonomy'] == $args['taxonomy'] || in_array($relationship['taxonomy'], $args['taxonomy']))) {
$active = true;
break;
}
}
if ($active == false) return $pieces;
$pieces['fields'] .= ", tr.term_order AS term_order";
$pieces['join'] .= " INNER JOIN {$wpdb->term_relationships} AS tr ON ( tr.term_taxonomy_id = t.term_id AND tr.term_taxonomy_id = tt.term_taxonomy_id AND tr.object_id = t.term_id )";
return $pieces;
}
public function get_terms_orderby($orderby, $args, $taxonomies) {
if ((is_admin() && isset($_GET['orderby'])) || empty($this->relationships)) return $orderby;
$active = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'taxonomy' && $args['taxonomy'] != NULL && ($relationship['taxonomy'] == $args['taxonomy'] || in_array($relationship['taxonomy'], $args['taxonomy']))) {
$active = true;
break;
}
}
if ($active == false) return $orderby;
return 'tr.term_order';
}
public function wp_get_object_terms($terms, $object_ids, $taxonomies, $args) {
return $this->get_object_terms($terms, $args);
}
public function get_terms($terms, $taxonomies, $args, $term_query) {
return $this->get_object_terms($terms, $args);
}
public function get_object_terms($terms, $args) {
global $wpdb;
if (empty($terms) || (is_admin() && isset($_GET['orderby'])) || empty($this->relationships) || !isset($args['taxonomy'])) return $terms;
// Check if valid query
$active_relationship = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'taxonomy' && ($relationship['taxonomy'] == $args['taxonomy'] || in_array($relationship['taxonomy'], $args['taxonomy']))) {
$active_relationship = $relationship;
break;
}
}
if ($active_relationship == false) return $terms;
// Add term_relationships.term_order to WP_Term object
foreach ($terms as $key => $term) {
if (is_numeric($term)) $term = get_term($term);
$results = $wpdb->get_results("
SELECT term_relationships.term_order AS term_order
FROM {$wpdb->term_relationships} AS term_relationships
WHERE term_relationships.term_taxonomy_id = {$term->term_taxonomy_id} AND term_relationships.object_id = {$term->term_id}
");
if (!empty($results)) {
$term->term_order = intval($results[0]->term_order);
}
}
usort($terms, function ($a, $b):int {
if (is_object($a)) {
if ($a->term_order == $b->term_order) return 0;
return ($a->term_order < $b->term_order) ? -1 : 1;
} else {
if ($a == $b) return 0;
return ($a < $b) ? -1 : 1;
}
});
return $terms;
}
public function add_term_relationship($term_id, $tt_id, $taxonomy) {
// Check if taxonomy is within relationships
$tax_relationship = false;
foreach ($this->relationships as $relationship) {
if ($relationship['type'] == 'taxonomy' && $relationship['taxonomy'] == $taxonomy) {
$tax_relationship = $relationship;
break;
}
}
if ($tax_relationship == false) return;
// Reset taxonomy terms to define term_relationship to added term
$this->refresh($tax_relationship);
}
// Ajax Functions
public function sort() {
global $wpdb;
$relationship = $_POST['relationship'];
if (!is_array($relationship)) {
wp_send_json_error(__('Invalid sort relationship.', 'ogrecore'));
exit;
}
$exists = false;
foreach ($this->relationships as $rel) {
if (($relationship['type'] == $rel['type']) && (!isset($relationship['post_type']) || $relationship['post_type'] == $rel['post_type']) && (!isset($relationship['taxonomy']) || $relationship['taxonomy'] == $rel['taxonomy'])) {
$exists = true;
break;
}
}
if ($exists == false) {
wp_send_json_error(__('Relationship does not exist.', 'ogrecore'));
exit;
}
parse_str($_POST['order'], $data);
if (!is_array($data)) {
wp_send_json_error(__('Order data invalid.', 'ogrecore'));
exit;
}
// Get objects per now page
$object_ids = [];
if (isset($data['post'])) {
$object_ids = array_filter(array_map('intval', $data['post']), function ($object_id) {
return is_int($object_id) && $object_id > 0;
});
} else if (isset($data['tag'])) {
$object_ids = array_filter(array_map('intval', $data['tag']), function ($object_id) {
return is_int($object_id) && $object_id > 0;
});
}
if (empty($object_ids)) {
wp_send_json_error(__('Unable to extract object ids from order data.', 'ogrecore'));
exit;
}
switch ($relationship['type']) {
case 'term':
$term = $_POST['term'];
if (!is_array($term)) {
wp_send_json_error(__('Invalid term provided.', 'ogrecore'));
exit;
}
foreach ($object_ids as $i => $post_id) {
update_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term['term_id']}", $i + 1);
}
break;
case 'post_type':
foreach ($object_ids as $i => $post_id) {
$wpdb->update($wpdb->posts, [
'menu_order' => $i + 1,
], [
'ID' => $post_id,
]);
}
break;
case 'taxonomy':
foreach ($object_ids as $i => $term_id) {
$result = $wpdb->get_row("
SELECT term_taxonomy.term_taxonomy_id AS term_taxonomy_id, term_relationships.term_order AS term_order
FROM {$wpdb->term_taxonomy} AS term_taxonomy
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON ( term_taxonomy.term_taxonomy_id = term_relationships.term_taxonomy_id AND term_taxonomy.term_id = term_relationships.object_id )
WHERE term_taxonomy.term_id = {$term_id}
");
$wpdb->update($wpdb->term_relationships, [
'term_order' => $i + 1,
], [
'object_id' => $term_id,
'term_taxonomy_id' => $result->term_taxonomy_id,
]);
}
break;
}
wp_send_json_success();
exit;
}
// Core Functions
public function valid($relationship, $post_id = false, $term_id = false) {
switch ($relationship['type']) {
case 'term':
if ($post_id == false || !get_post_status($post_id)) { return false; }
if ($term_id == false || !term_exists($term_id, $relationship['taxonomy'])) { return false; }
break;
case 'post_type':
if ($post_id == false || !get_post_status($post_id)) { return false; }
break;
case 'taxonomy':
if ($term_id == false || !term_exists($term_id, $relationship['taxonomy'])) { return false; }
break;
default:
return false;
}
return true;
}
public function set($relationship, $order, $post_id = false, $term_id = false, $term_taxonomy_id = false) {
global $wpdb;
if (!$this->valid($relationship, $post_id, $term_id) || !is_int($order) || $order < 0) {
return false;
}
switch ($relationship['type']) {
case 'term':
update_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}", $order);
break;
case 'post_type':
$wpdb->update($wpdb->posts, [
'menu_order' => $order,
], [
'ID' => $post_id,
]);
break;
case 'taxonomy':
$wpdb->update($wpdb->term_relationships, [
'term_order' => $order,
], [
'object_id' => $term_id,
'term_taxonomy_id' => $term_taxonomy_id,
]);
break;
}
return true;
}
public function get($relationship, $post_id = false, $term_id = false, $term_taxonomy_id = false) {
global $wpdb;
if (!$this->valid($relationship, $post_id, $term_id)) {
return false;
}
switch ($relationship['type']) {
case 'term':
return intval(get_post_meta($post_id, 'ogre-sort_' . $relationship['taxonomy'] . '_' . $term_id, true));
case 'post_type':
$_post = get_post($post_id);
return intval($_post->menu_order);
case 'taxonomy':
$results = $wpdb->get_results("SELECT term_order FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = {$term_taxonomy_id} AND object_id = {$term_id}");
if (!empty($results)) {
return intval($results[0]->term_order);
} else {
$orders = get_option('term_order_' . $relationship['taxonomy'], []);
if (!empty($orders)) {
foreach ($orders as $position => $value) {
if (intval($value) === intval($term_id)) return intval($position);
}
}
}
break;
}
return false;
}
public function reset($relationship) {
global $wpdb;
switch ($relationship['type']) {
case 'term':
$terms = get_terms([
'taxonomy' => $relationship['taxonomy'],
'hide_empty' => false,
'fields' => 'ids',
]);
if (!empty($terms)) {
foreach ($terms as $term_id) {
$posts = get_posts([
'post_type' => $relationship['post_type'],
'posts_per_page' => -1,
'tax_query' => [[
'taxonomy' => $relationship['taxonomy'],
'field' => 'id',
'terms' => $term_id,
]],
'fields' => 'ids',
]);
if (!empty($posts)) {
foreach ($posts as $post_id) {
delete_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}");
}
}
}
}
break;
case 'post_type':
$results = $wpdb->get_results("SELECT ID FROM {$wpdb->posts} WHERE post_type = {$relationship['taxonomy']} AND post_status IN ('publish', 'pending', 'draft', 'private', 'future') ORDER BY post_date DESC");
if (!empty($results)) {
$i = 0;
foreach ($results as $row) {
$wpdb->update($wpdb->posts, [
'menu_order' => $i,
], [
'ID' => $row->ID,
]);
$i++;
}
}
break;
case 'taxonomy':
$terms = get_terms([
'taxonomy' => $relationship['taxonomy'],
'hide_empty' => false,
'fields' => 'all',
'orderby' => 'name',
'order' => 'ASC',
]);
if (!empty($terms)) {
foreach ($terms as $term) {
$wpdb->delete($wpdb->term_relationships, [
'object_id' => $term->term_id,
'term_taxonomy_id' => $term->term_taxonomy_id,
]);
}
}
break;
}
return true;
}
public function reset_all() {
foreach ($this->relationships as $relationship) {
$this->reset($relationship);
}
}
public function refresh($relationship) {
global $wpdb;
switch ($relationship['type']) {
case 'term':
$terms = get_terms([
'taxonomy' => $relationship['taxonomy'],
'hide_empty' => true,
'fields' => 'ids',
]);
if (empty($terms)) return false;
$terms = array_unique($terms);
foreach ($terms as $term_id) {
// Set all undefined meta data
$posts = get_posts([
'post_type' => $relationship['post_type'],
'posts_per_page' => -1,
'offset' => 0,
'post_status' => 'any',
'fields' => 'ids',
'tax_query' => [[
'taxonomy' => $relationship['taxonomy'],
'field' => 'term_id',
'terms' => $term_id,
'include_children' => true,
'operator' => 'IN',
]],
'meta_query' => [
'relation' => 'OR',
[
'key' => "ogre-sort_{$relationship['taxonomy']}_{$term_id}",
'compare' => 'NOT EXISTS',
'value' => '',
],
[
'key' => "ogre-sort_{$relationship['taxonomy']}_{$term_id}",
'value' => '',
],
],
]);
$posts = array_unique($posts);
foreach ($posts as $post_id) {
update_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}", -1);
}
// Reorder
$posts = get_posts([
'post_type' => $relationship['post_type'],
'posts_per_page' => -1,
'offset' => 0,
'post_status' => 'any',
'fields' => 'ids',
'tax_query' => [[
'taxonomy' => $relationship['taxonomy'],
'field' => 'term_id',
'terms' => $term_id,
'include_children' => true,
'operator' => 'IN',
]],
'meta_key' => "ogre-sort_{$relationship['taxonomy']}_{$term_id}",
'orderby' => 'meta_value_num',
]);
$posts = array_values(array_unique($posts));
foreach ($posts as $i => $post_id) {
update_post_meta($post_id, "ogre-sort_{$relationship['taxonomy']}_{$term_id}", $i + 1);
}
}
break;
case 'post_type':
$result = $wpdb->get_row("
SELECT count(*) as cnt, max(menu_order) as max, min(menu_order) as min
FROM {$wpdb->posts}
WHERE post_type = '{$relationship['post_type']}' AND post_status IN ('publish', 'pending', 'draft', 'private', 'future')
");
if ($result->cnt == 0 || $result->cnt == $result->max) return false;
$results = $wpdb->get_results("
SELECT ID
FROM {$wpdb->posts}
WHERE post_type = '{$relationship['post_type']}' AND post_status IN ('publish', 'pending', 'draft', 'private', 'future')
ORDER BY menu_order ASC
");
foreach ($results as $key => $result) {
$wpdb->update($wpdb->posts, [
'menu_order' => $key + 1,
], [
'ID' => $result->ID,
]);
}
break;
case 'taxonomy':
// Set all undefined term_orders
$results = $wpdb->get_results("
SELECT terms.term_id AS term_id, term_taxonomy.term_taxonomy_id AS term_taxonomy_id
FROM {$wpdb->terms} AS terms
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy ON ( terms.term_id = term_taxonomy.term_id )
WHERE term_taxonomy.taxonomy = '{$relationship['taxonomy']}'
");
if (empty($results)) return false;
foreach ($results as $key => $result) {
$term_order = $wpdb->get_results("
SELECT term_relationships.term_order, term_relationships.object_id
FROM {$wpdb->term_relationships} AS term_relationships
WHERE term_relationships.term_taxonomy_id = {$result->term_taxonomy_id} AND term_relationships.object_id = {$result->term_id}
");
if (empty($term_order)) {
$wpdb->insert($wpdb->term_relationships, [
'object_id' => $result->term_id,
'term_taxonomy_id' => $result->term_taxonomy_id,
'term_order' => -1,
]);
}
}
// Reorder term_orders
$results = $wpdb->get_results("
SELECT terms.term_id AS term_id, term_taxonomy.term_taxonomy_id AS term_taxonomy_id, term_relationships.term_order
FROM {$wpdb->terms} AS terms
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy ON ( terms.term_id = term_taxonomy.term_id )
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON ( term_taxonomy.term_taxonomy_id = term_relationships.term_taxonomy_id AND term_taxonomy.term_id = term_relationships.object_id )
WHERE term_taxonomy.taxonomy = '{$relationship['taxonomy']}'
ORDER BY term_order ASC
");
foreach ($results as $key => $result) {
$wpdb->update($wpdb->term_relationships, [
'term_order' => $key + 1,
], [
'object_id' => $result->term_id,
'term_taxonomy_id' => $result->term_taxonomy_id,
]);
}
break;
}
return true;
}
public function refresh_purge_cache() {
if (!isset($_GET['type'], $_GET['_wpnonce'])) return;
$_type = explode('-', $_GET['type']);
$_type = reset($_type);
if ($_type == 'all') {
$this->refresh_all();
}
}
public function refresh_screen($current_screen) {
// TODO: Move this to button on settings with post action
if ($current_screen->id == 'dashboard') {
$this->refresh_all();
}
}
public function refresh_all() {
foreach ($this->relationships as $relationship) {
$this->refresh($relationship);
}
}
}
Sort::instance();

57
ogre-sort.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
/*
Plugin Name: Ogre Sort
Plugin URI: https://plugins.cleverogre.com/plugin/ogre-sort/
Description: WordPress plugin which enables drag-and-drop sorting within the admin area for posts, terms, and posts within terms.
Version: 1.0.0
Author: CleverOgre
Author URI: http://cleverogre.com/
Text Domain: ogre-sort
License: GPLv3 or later
License URI: http://www.gnu.org/licenses/gpl-3.0.html
Copyright: CleverOgre, Inc.
*/
namespace Ogre;
use Ogre\Sort\Settings;
defined('ABSPATH') || exit;
require_once 'vendor/autoload.php';
final class Sort extends Plugin {
protected function __construct() {
parent::__construct();
$this->add_files([
'inc/class-settings.php',
'inc/class-sort.php',
]);
}
protected function enable():void {
parent::enable();
add_filter('plugin_action_links_' . self::get_basename(), [$this, 'action_links']);
}
protected function disable():void {
parent::disable();
remove_filter('plugin_action_links_' . self::get_basename(), [$this, 'action_links']);
}
public function action_links($links) {
if (current_user_can('manage_options')) {
array_unshift($links, sprintf(
'<a href="%s">%s</a>',
esc_url(Settings::get_url()),
esc_html(self::__('Settings'))
));
}
return $links;
}
}
Sort::instance();

5040
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"$schema": "https://www.schemastore.org/package.json",
"name": "cleverogre/ogre-sort",
"version": "1.0.0",
"title": "Ogre Sort",
"description": "WordPress plugin which enables drag-and-drop sorting within the admin area for posts, terms, and posts within terms.",
"author": "CleverOgre",
"license": "GPL-3.0+",
"keywords": [
"WordPress",
"Plugin",
"Sorting",
"menu_order",
"term_order",
"CPT"
],
"homepage": "https://cleverogre.com",
"engines": {
"node": ">=21.1.0",
"npm": ">=10.2.3"
},
"devDependencies": {
"gulp": "^5.0.0",
"gulp-cli": "^2.3.0",
"gulp-clean": "^0.4.0",
"gulp-zip": "^6.1.0"
}
}

32
readme.txt Normal file
View File

@@ -0,0 +1,32 @@
=== Ogre Sort ===
Contributors: ogrecooper, cleverogre
Tested up to: 6.8
Requires at least: 5.0
Requires PHP: 8.0
Version: 1.0.0
License: GPLv3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Copyright: CleverOgre
Donate link: https://cleverogre.com/
Tags: wordpress, plugin, sorting, menu_order, term_order, cpt
WordPress plugin which enables drag-and-drop sorting within the admin area for posts, terms, and posts within terms.
== Installation ==
The required libraries can be installed using the following command:
composer install
You can install the optional development tools using the following command:
npm install
== FAQ ==
= What is this plugin? =
If you don't know what plugin you have downloaded, please contact [CleverOgre](team@cleverogre.com) for more information. This plugin is only developed for a small, private audience.
== Changelog ==
= 1.0.0 - 2025-11-18 =
* Initial release