diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 8305a33..2c5c0a0 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -21,8 +21,8 @@ /coverage/* - - *.min.js + + *.js diff --git a/README.md b/README.md index 4d6c6be..95c3b89 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Fewer Tags helps WordPress sites avoid thin, low-value tag archives. Instead of letting every tag create another archive URL, the plugin lets you set a minimum number of posts a tag needs before it is considered live on your site. Tags below that threshold are hidden from visitors and search engines, redirected to your homepage, and excluded from supported XML sitemaps. +As of version 2.0, Fewer Tags also lets you merge terms across taxonomies and create redirects when you merge or delete terms — all the functionality previously in Fewer Tags Pro is now included for free. + That means fewer useless tag pages, cleaner taxonomy archives, and less crawl waste. ## What it does @@ -25,6 +27,12 @@ Fewer Tags lets you define the minimum number of posts a tag needs before it bec The default threshold is 10 posts, and you can change it under **Settings → Reading**. +## Merging and redirects + +- Merge any tag, category, or custom taxonomy term into another — even across taxonomies. All posts from the source term are moved to the target term, and a 301 redirect is created from the old term archive to the new one. +- When you delete a term, Fewer Tags prompts you to create a redirect to the homepage or any other URL. +- Redirects are created via the [Redirection plugin](https://wordpress.org/plugins/redirection/) or Yoast SEO Premium. + ## Why use it? Many WordPress sites accumulate lots of tags that only contain one or two posts. Those tag archives rarely help users, and they create extra URLs for search engines to crawl and index. @@ -44,6 +52,7 @@ Fewer Tags gives you a simple way to keep useful tag archives while suppressing 2. Activate the plugin. 3. Go to **Settings → Reading**. 4. Choose how many posts a tag needs before it becomes live on your site. +5. For the merge and redirect-on-delete features, install and activate the [Redirection plugin](https://wordpress.org/plugins/redirection/) or Yoast SEO Premium. ## FAQ @@ -61,10 +70,9 @@ Please use the Patchstack Vulnerability Disclosure Program to report security is ## Learn more +- Plugin home: https://progressplanner.com/plugins/fewer-tags/ - Research: https://fewertags.com/research/ - Free plugin walkthrough: https://www.youtube.com/watch?v=KItn1X1qMas -- Fewer Tags Pro: https://fewertags.com/ -- Fewer Tags Pro video: https://www.youtube.com/watch?v=NkF3Y6iIoDk ## Development diff --git a/composer.json b/composer.json index a9c02bf..785b762 100644 --- a/composer.json +++ b/composer.json @@ -15,10 +15,10 @@ "phpcompatibility/phpcompatibility-wp": "*", "php-parallel-lint/php-parallel-lint": "^1.3", "yoast/wp-test-utils": "^1.2", - "phpstan/phpstan": "^1.10", - "szepeviktor/phpstan-wordpress": "^1.3", - "phpstan/extension-installer": "^1.3", - "phpcompatibility/php-compatibility": "dev-develop as 9.99.99" + "phpstan/phpstan": "^2.1", + "szepeviktor/phpstan-wordpress": "^2.0", + "phpstan/extension-installer": "^1.4", + "phpcompatibility/php-compatibility": "^9.3" }, "scripts": { "check-cs": [ @@ -44,6 +44,9 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, "phpstan/extension-installer": true + }, + "platform": { + "php": "7.4.33" } } } diff --git a/composer.lock b/composer.lock index 42a2790..6af7183 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "005df9e8bb8ad210ae6ac2a8a7304864", + "content-hash": "c81256e86b3ae98b873a86cbd305b634", "packages": [], "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.2.1", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245" + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/1bf183a3e1bd094f231a2128b9ecc5363c269245", - "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce", "shasum": "" }, "require": { @@ -51,9 +51,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.2.1" + "source": "https://github.com/antecedent/patchwork/tree/2.2.3" }, - "time": "2024-12-11T10:19:54+00:00" + "time": "2025-09-17T09:00:56+00:00" }, { "name": "brain/monkey", @@ -61,23 +61,22 @@ "source": { "type": "git", "url": "https://github.com/Brain-WP/BrainMonkey.git", - "reference": "a8502a89e818f843a674163dfe0dc688f0e41554" + "reference": "789a8b06f0ba18767109767a058792065c3237b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/a8502a89e818f843a674163dfe0dc688f0e41554", - "reference": "a8502a89e818f843a674163dfe0dc688f0e41554", + "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/789a8b06f0ba18767109767a058792065c3237b3", + "reference": "789a8b06f0ba18767109767a058792065c3237b3", "shasum": "" }, "require": { - "antecedent/patchwork": "^2.1.17", - "mockery/mockery": "^1.3.5 || ^1.4.4", - "php": ">=5.6.0" + "antecedent/patchwork": "^2.2.3", + "mockery/mockery": "~1.3.6 || ~1.4.4 || ~1.5.1 || ^1.6.10", + "php": ">=7.2.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", - "phpcompatibility/php-compatibility": "^9.3.0", - "phpunit/phpunit": "^5.7.26 || ^6.0 || ^7.0 || >=8.0 <8.5.12 || ^8.5.14 || ^9.0" + "phpcompatibility/php-compatibility": "^10.0.0@alpha", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "default-branch": true, "type": "library", @@ -124,33 +123,33 @@ "issues": "https://github.com/Brain-WP/BrainMonkey/issues", "source": "https://github.com/Brain-WP/BrainMonkey" }, - "time": "2025-01-15T09:49:20+00:00" + "time": "2026-02-09T19:48:37+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", @@ -169,9 +168,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -179,7 +178,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -200,9 +198,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" }, { "name": "doctrine/instantiator", @@ -276,20 +293,20 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "dev-master", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "ea3c21ca78e9696dbd7d8f16e1141ca7be241e6a" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/ea3c21ca78e9696dbd7d8f16e1141ca7be241e6a", - "reference": "ea3c21ca78e9696dbd7d8f16e1141ca7be241e6a", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -300,7 +317,6 @@ "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -322,9 +338,9 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/master" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2025-01-01T06:33:44+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { "name": "mockery/mockery", @@ -414,12 +430,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "5ee4e978b7fec6dbd844282126a5a32daa2044c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5ee4e978b7fec6dbd844282126a5a32daa2044c6", + "reference": "5ee4e978b7fec6dbd844282126a5a32daa2044c6", "shasum": "" }, "require": { @@ -459,7 +475,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.x" }, "funding": [ { @@ -467,20 +483,20 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-12-30T16:10:21+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "50f0d9c9d0e3cff1163c959c50aaaaa4a7115f08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50f0d9c9d0e3cff1163c959c50aaaaa4a7115f08", + "reference": "50f0d9c9d0e3cff1163c959c50aaaaa4a7115f08", "shasum": "" }, "require": { @@ -493,13 +509,14 @@ "ircmaxell/php-yacc": "^0.0.7", "phpunit/phpunit": "^9.0" }, + "default-branch": true, "bin": [ "bin/php-parse" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -523,9 +540,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/master" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2026-02-26T13:20:22+00:00" }, { "name": "phar-io/manifest", @@ -533,12 +550,12 @@ "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "reference": "c581d4941e196459bf76c945a8ca922963a66708" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/c581d4941e196459bf76c945a8ca922963a66708", + "reference": "c581d4941e196459bf76c945a8ca922963a66708", "shasum": "" }, "require": { @@ -585,7 +602,7 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" + "source": "https://github.com/phar-io/manifest/tree/master" }, "funding": [ { @@ -593,7 +610,7 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2025-11-27T15:23:09+00:00" }, { "name": "phar-io/version", @@ -709,26 +726,30 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.7.2", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "c04f96cb232fab12a3cbcccf5a47767f0665c3f4" + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/c04f96cb232fab12a3cbcccf5a47767f0665c3f4", - "reference": "c04f96cb232fab12a3cbcccf5a47767f0665c3f4", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", "shasum": "" }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^1.11", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, @@ -751,51 +772,39 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" }, - "time": "2025-02-12T04:51:58+00:00" + "time": "2026-02-03T19:29:21+00:00" }, { "name": "phpcompatibility/php-compatibility", - "version": "dev-develop", + "version": "9.3.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9013cd039fe5740953f9fdeebd19d901b80e26f2" + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9013cd039fe5740953f9fdeebd19d901b80e26f2", - "reference": "9013cd039fe5740953f9fdeebd19d901b80e26f2", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.12", - "squizlabs/php_codesniffer": "^3.10.0" + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" }, - "replace": { - "wimg/php-compatibility": "*" + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.1.3", - "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.1.0", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, - "default-branch": true, "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "9.x-dev", - "dev-develop": "10.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0-or-later" @@ -821,46 +830,26 @@ "keywords": [ "compatibility", "phpcs", - "standards", - "static analysis" + "standards" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibility" }, - "funding": [ - { - "url": "https://github.com/PHPCompatibility", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcompatibility", - "type": "thanks_dev" - } - ], - "time": "2025-01-20T20:06:48+00:00" + "time": "2019-12-27T09:44:58+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", "shasum": "" }, "require": { @@ -917,27 +906,32 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:30:46+00:00" + "time": "2025-09-19T17:43:28+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "dev-master", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "bcefd1ebb188c4099116743f67abf7c36abc1015" + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/bcefd1ebb188c4099116743f67abf7c36abc1015", - "reference": "bcefd1ebb188c4099116743f67abf7c36abc1015", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0" @@ -946,7 +940,6 @@ "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, - "default-branch": true, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ @@ -994,7 +987,7 @@ "type": "thanks_dev" } ], - "time": "2025-03-03T03:03:54+00:00" + "time": "2025-10-18T00:05:59+00:00" }, { "name": "phpcsstandards/phpcsextra", @@ -1002,25 +995,25 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "cf28c840b04985ef6a9aabb15ef14dd7b9d3b550" + "reference": "85b0130b8cc8c570c2ae1fb5eb0eb22f8b55f7af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/cf28c840b04985ef6a9aabb15ef14dd7b9d3b550", - "reference": "cf28c840b04985ef6a9aabb15ef14dd7b9d3b550", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/85b0130b8cc8c570c2ae1fb5eb0eb22f8b55f7af", + "reference": "85b0130b8cc8c570c2ae1fb5eb0eb22f8b55f7af", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.8.0" + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "default-branch": true, "type": "phpcodesniffer-standard", @@ -1077,7 +1070,7 @@ "type": "thanks_dev" } ], - "time": "2025-03-03T01:39:12+00:00" + "time": "2026-03-06T01:09:19+00:00" }, { "name": "phpcsstandards/phpcsutils", @@ -1085,24 +1078,24 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "060222e14c567c00fc4fd056ec7af809810c1fbc" + "reference": "8082141d724d3d72a2d4cc677268a1321abdff00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/060222e14c567c00fc4fd056ec7af809810c1fbc", - "reference": "060222e14c567c00fc4fd056ec7af809810c1fbc", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/8082141d724d3d72a2d4cc677268a1321abdff00", + "reference": "8082141d724d3d72a2d4cc677268a1321abdff00", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.1 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "default-branch": true, @@ -1141,6 +1134,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -1170,7 +1164,7 @@ "type": "thanks_dev" } ], - "time": "2025-02-15T23:56:59+00:00" + "time": "2026-03-06T01:47:45+00:00" }, { "name": "phpstan/extension-installer", @@ -1178,23 +1172,23 @@ "source": { "type": "git", "url": "https://github.com/phpstan/extension-installer.git", - "reference": "c243505d48dcce6fcf4118cb5b65ad17a3b06391" + "reference": "1754f69aa40d7e42ae0fa94c11036a80dcd0c331" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/c243505d48dcce6fcf4118cb5b65ad17a3b06391", - "reference": "c243505d48dcce6fcf4118cb5b65ad17a3b06391", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/1754f69aa40d7e42ae0fa94c11036a80dcd0c331", + "reference": "1754f69aa40d7e42ae0fa94c11036a80dcd0c331", "shasum": "" }, "require": { "composer-plugin-api": "^2.0", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.0 || ^2.0" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "require-dev": { "composer/composer": "^2.0", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + "phpstan/phpstan-strict-rules": "^2.0" }, "default-branch": true, "type": "composer-plugin", @@ -1219,28 +1213,24 @@ "issues": "https://github.com/phpstan/extension-installer/issues", "source": "https://github.com/phpstan/extension-installer/tree/1.4.x" }, - "time": "2025-01-28T09:27:10+00:00" + "time": "2026-02-26T12:27:12+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.x-dev", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "9cd58b5d4b4c82a18ae289937dd85ce9d5478641" - }, + "version": "2.2.x-dev", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9cd58b5d4b4c82a18ae289937dd85ce9d5478641", - "reference": "9cd58b5d4b4c82a18ae289937dd85ce9d5478641", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/988d56d9457e91e9be6cef3f4974654d20b3603c", + "reference": "988d56d9457e91e9be6cef3f4974654d20b3603c", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" }, + "default-branch": true, "bin": [ "phpstan", "phpstan.phar" @@ -1277,7 +1267,7 @@ "type": "github" } ], - "time": "2025-03-09T12:17:34+00:00" + "time": "2026-03-19T12:50:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1285,12 +1275,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "0448d60087a382392a1b2a1abe434466e03dcc87" + "reference": "653bca7ea05439961818f429a2a49ec8a8c7d2fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0448d60087a382392a1b2a1abe434466e03dcc87", - "reference": "0448d60087a382392a1b2a1abe434466e03dcc87", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/653bca7ea05439961818f429a2a49ec8a8c7d2fb", + "reference": "653bca7ea05439961818f429a2a49ec8a8c7d2fb", "shasum": "" }, "require": { @@ -1353,9 +1343,21 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-10-31T05:58:25+00:00" + "time": "2025-11-26T14:28:02+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1604,12 +1606,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "7fcb3793ca4cf63ad51605747e52b32ad788f61c" + "reference": "a71778ade547d17881d1039314375ec273a0b32f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7fcb3793ca4cf63ad51605747e52b32ad788f61c", - "reference": "7fcb3793ca4cf63ad51605747e52b32ad788f61c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a71778ade547d17881d1039314375ec273a0b32f", + "reference": "a71778ade547d17881d1039314375ec273a0b32f", "shasum": "" }, "require": { @@ -1620,7 +1622,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -1631,11 +1633,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -1687,19 +1689,11 @@ }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2025-03-04T12:16:09+00:00" + "time": "2026-03-03T16:43:55+00:00" }, { "name": "sebastian/cli-parser", @@ -1874,12 +1868,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b247957a1c8dc81a671770f74b479c0a78a818f1", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -1932,15 +1926,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:46:14+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -2134,12 +2140,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -2195,15 +2201,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -2211,12 +2229,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -2259,15 +2277,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -2444,12 +2474,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -2491,15 +2521,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2667,16 +2709,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "dev-master", + "version": "3.x-dev", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "82a9ec27b3eaf2e41fe658d44b7dcc4a8e4bd3a1" + "reference": "101786698b7686dc5c26aafe941e4791dbe0efee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/82a9ec27b3eaf2e41fe658d44b7dcc4a8e4bd3a1", - "reference": "82a9ec27b3eaf2e41fe658d44b7dcc4a8e4bd3a1", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/101786698b7686dc5c26aafe941e4791dbe0efee", + "reference": "101786698b7686dc5c26aafe941e4791dbe0efee", "shasum": "" }, "require": { @@ -2688,17 +2730,11 @@ "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, - "default-branch": true, "bin": [ "bin/phpcbf", "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -2748,117 +2784,41 @@ "type": "thanks_dev" } ], - "time": "2025-03-09T23:43:06+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "1.x-dev", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "default-branch": true, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-03-07T04:19:43+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.5", + "version": "2.x-dev", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", - "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.31", - "symfony/polyfill-php73": "^1.12.0" + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" }, "require-dev": { "composer/composer": "^2.1.14", + "composer/semver": "^3.4", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.1", - "phpstan/phpstan-strict-rules": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" }, + "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -2886,22 +2846,22 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/2.x" }, - "time": "2024-06-28T22:27:19+00:00" + "time": "2025-09-14T02:58:22+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -2930,7 +2890,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -2938,20 +2898,20 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "wp-coding-standards/wpcs", - "version": "3.1.0", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", "shasum": "" }, "require": { @@ -2959,17 +2919,17 @@ "ext-libxml": "*", "ext-tokenizer": "*", "ext-xmlreader": "*", - "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.10", - "squizlabs/php_codesniffer": "^3.9.0" + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^8.0 || ^9.0" }, "suggest": { "ext-iconv": "For improved results", @@ -3004,7 +2964,7 @@ "type": "custom" } ], - "time": "2024-03-25T16:39:00+00:00" + "time": "2025-11-25T12:08:04+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -3012,12 +2972,12 @@ "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "e6faedf5e34cea4438e341f660e2f719760c531d" + "reference": "80d5533d724635ecc611d9c208b061d7c7a1c3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e6faedf5e34cea4438e341f660e2f719760c531d", - "reference": "e6faedf5e34cea4438e341f660e2f719760c531d", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/80d5533d724635ecc611d9c208b061d7c7a1c3bd", + "reference": "80d5533d724635ecc611d9c208b061d7c7a1c3bd", "shasum": "" }, "require": { @@ -3027,7 +2987,7 @@ "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "yoast/yoastcs": "^3.1.0" + "yoast/yoastcs": "^3.4.0" }, "type": "library", "extra": { @@ -3067,7 +3027,7 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2025-02-09T18:13:44+00:00" + "time": "2026-03-06T02:44:20+00:00" }, { "name": "yoast/wp-test-utils", @@ -3075,23 +3035,23 @@ "source": { "type": "git", "url": "https://github.com/Yoast/wp-test-utils.git", - "reference": "430933b3b34783fe212ea8e883f5dca461c1fdbf" + "reference": "2090cefcd2376ce8c64a118795377335d530ccba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/wp-test-utils/zipball/430933b3b34783fe212ea8e883f5dca461c1fdbf", - "reference": "430933b3b34783fe212ea8e883f5dca461c1fdbf", + "url": "https://api.github.com/repos/Yoast/wp-test-utils/zipball/2090cefcd2376ce8c64a118795377335d530ccba", + "reference": "2090cefcd2376ce8c64a118795377335d530ccba", "shasum": "" }, "require": { - "brain/monkey": "^2.6.2", + "brain/monkey": "^2.7.0", "php": ">=5.6", - "yoast/phpunit-polyfills": "^1.1.1" + "yoast/phpunit-polyfills": "^1.1.5" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "yoast/yoastcs": "^3.1.0" + "yoast/yoastcs": "^3.4.0" }, "default-branch": true, "type": "library", @@ -3140,24 +3100,18 @@ "security": "https://github.com/Yoast/wp-test-utils/security/policy", "source": "https://github.com/Yoast/wp-test-utils" }, - "time": "2025-01-05T21:43:33+00:00" - } - ], - "aliases": [ - { - "package": "phpcompatibility/php-compatibility", - "version": "dev-develop", - "alias": "9.99.99", - "alias_normalized": "9.99.99.0" + "time": "2026-02-21T16:57:55+00:00" } ], + "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "phpcompatibility/php-compatibility": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "7.4.33" + }, "plugin-api-version": "2.6.0" } diff --git a/fewer-tags.php b/fewer-tags.php index 32424cf..5c4159b 100644 --- a/fewer-tags.php +++ b/fewer-tags.php @@ -3,14 +3,14 @@ * Plugin that redirects tag pages to the home page if they contain fewer than a specified number of posts. * * @package FewerTags - * @version 1.5.1 + * @version 2.0 * * Plugin Name: Fewer Tags - * Plugin URI: https://fewertags.com/ - * Description: Redirects tag pages to the home page if they contain fewer than a specified number of posts, defaults to 10. Change under Settings → Reading. Learn more about this plugin at fewertags.com. + * Plugin URI: https://progressplanner.com/plugins/fewer-tags/ + * Description: Manage your site's tags: set minimum post counts, merge terms, and create redirects. Change settings under Settings → Reading. Learn more at progressplanner.com/plugins/fewer-tags/. * Requires at least: 6.2 * Requires PHP: 7.4 - * Version: 1.5.1 + * Version: 2.0 * Author: Joost de Valk * Author URI: https://joost.blog/ * License: GPL-3.0+ @@ -23,6 +23,7 @@ } define( 'FEWER_TAGS_DIR', __DIR__ ); +define( 'FEWER_TAGS_FILE', __FILE__ ); require_once __DIR__ . '/src/autoload.php'; // Instantiate the plugin class. diff --git a/js/fewer-tags.js b/js/fewer-tags.js new file mode 100644 index 0000000..07d1058 --- /dev/null +++ b/js/fewer-tags.js @@ -0,0 +1,470 @@ +/** + * Loaded on edit-tags admin pages, this file contains the JavaScript for the FewerTags plugin. + * + * @file This files contains the functionality for the FewerTags plugin. + * @author Joost de Valk + */ + +/* global fewerTags, tb_remove, Choices */ + +/** + * A helper to make AJAX requests. + * + * @param {Object} params The callback parameters. + * @param {string} params.url The URL to send the request to. + * @param {Object} params.data The data to send with the request. + * @param {Function} params.successAction The callback to run on success. + * @param {Function} params.failAction The callback to run on failure. + */ +const fewerTagsAjaxRequest = ( { url, data, successAction, failAction } ) => { + const http = new XMLHttpRequest(); + http.open( 'POST', url, true ); + http.onreadystatechange = () => { + let response; + try { + response = JSON.parse( http.response ); + } catch ( e ) { + if ( http.readyState === 4 && http.status !== 200 ) { + // eslint-disable-next-line no-console + console.warn( http, e ); + return http.response; + } + } + if ( http.readyState === 4 && http.status === 200 ) { + return successAction ? successAction( response ) : response; + } + return failAction ? failAction( response ) : response; + }; + + const dataForm = new FormData(); + + // eslint-disable-next-line prefer-const + for ( let [ key, value ] of Object.entries( data ) ) { + dataForm.append( key, value ); + } + + http.send( dataForm ); +}; + +/** + * Similar to jQuery's $( document ).ready(). + * Runs a callback when the DOM is ready. + * + * @param {Function} callback The callback to run when the DOM is ready. + */ +function fewerTagsDomReady( callback ) { + if ( document.readyState !== 'loading' ) { + callback(); + return; + } + document.addEventListener( 'DOMContentLoaded', callback ); +} + +/** + * A helper to allow listening for element removals, + * and run a callback when that happens. + * + * @param {Element[]} elements The elements to observe. + * @param {Function} callback The callback to run when the element is removed. + */ +const fewerTagsObserveElementRemoval = ( elements, callback ) => { + const theList = document.getElementById( 'the-list' ); + if ( ! theList ) { + return; + } + elements.forEach( ( element ) => { + let inDom = theList.contains( element ); + const observer = new MutationObserver( () => { + if ( ! theList.contains( element ) && inDom ) { + inDom = false; + callback(); + } + } ); + observer.observe( theList, { childList: true, subtree: true } ); + } ); +}; + +/** + * Create a redirect for a term, and show a notice. + * This function creates an AJAX request to the server. + * + * @param {string} slug The slug of the term to redirect. + * @param {string} taxonomy The taxonomy of the term to redirect. + * @param {string} target The target URL to redirect to. + * @param {string} nonce The nonce to use for the request. + */ +function fewerTagsRedirectToUrl( slug, taxonomy, target, nonce ) { // eslint-disable-line no-unused-vars + fewerTagsAjaxRequest( { + url: fewerTags.ajaxUrl, + data: { + action: 'fewer_tags_redirect_url', + slug, + taxonomy, + target, + _ajax_nonce: nonce, + }, + successAction: ( response ) => { + document.getElementById( `fewer-tags-redirect-${ response.data.slug }` ) + .outerHTML = ` +
+

${ response.data.msg }

+ +
`; + }, + } ); +} + +async function fewerTagsGetTaxonomies( currentTaxonomy ) { + const apiUrl = '/wp-json/wp/v2/taxonomies?context=edit'; + + try { + const response = await fetch( apiUrl, { + credentials: 'include', // Include cookies with the request + headers: { + 'X-WP-Nonce': fewerTags.restAPInonce, + }, + } ); + + if ( ! response.ok ) { + throw new Error( `Network response was not ok (Status: ${ response.status })` ); + } + + const taxonomies = await response.json(); + const publicTaxonomies = Object.keys( taxonomies ) + .filter( ( key ) => taxonomies[ key ].visibility && taxonomies[ key ].visibility.public ) // Ensure visibility information is present and public + .map( ( key ) => ( { + value: taxonomies[ key ].slug, + label: taxonomies[ key ].labels.singular_name, + selected: ( taxonomies[ key ].slug === currentTaxonomy ), + customProperties: { + post_type: taxonomies[ key ].types, + rest_base: taxonomies[ key ].rest_base, + }, + } ) ); + + return publicTaxonomies; // This array contains objects with the slug and plural name of each public taxonomy + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error fetching public taxonomies with edit context and cookies:', error ); + } +} + +let FTterms = []; + +/** + * Get all terms from the WordPress REST API. + * + * @param {string} taxonomy The taxonomy to get the terms for. + * @param {string} restBase Optional REST base for the taxonomy endpoint. + * + * @return {Promise} The promise of the terms. + */ +async function fewerTagsGetAllTerms( taxonomy, restBase ) { + const retrievedTerms = []; + const perPage = 100; // Max allowed by WP REST API + let page = 1; + let hasMore = true; + const termsOutput = []; + + let endpoint = ''; + if ( restBase ) { + endpoint = `/wp-json/wp/v2/${ restBase }`; + } else if ( taxonomy === 'post_tag' ) { + endpoint = `/wp-json/wp/v2/tags`; + } else if ( taxonomy === 'category' ) { + endpoint = `/wp-json/wp/v2/categories`; + } else { + endpoint = `/wp-json/wp/v2/${ taxonomy }`; + } + + while ( hasMore ) { + const apiUrl = `${ endpoint }?per_page=${ perPage }&page=${ page }`; + + try { + const response = await fetch( apiUrl ); + if ( ! response.ok ) { + throw new Error( `Network response was not ok (Status: ${ response.status })` ); + } + const data = await response.json(); + retrievedTerms.push( ...data ); + + if ( data.length < perPage ) { + hasMore = false; // Break the loop if we got less than perPage items + } else { + page++; // Prepare to fetch the next page + } + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error fetching post tags:', error ); + break; // Exit the loop in case of an error + } + } + + // Process the tags + retrievedTerms.forEach( ( tag ) => { + termsOutput.push( + { + value: tag.id, + label: tag.name, + customProperties: { + count: tag.count, + }, + }, + ); + } ); + return termsOutput; +} + +/** + * Function to remove an object from the array by its (integer) value. + * + * @param {Array} arr The array of terms. + * @param {number} valueToRemove The ID of the term to remove. + * + * @return {Array} Returns the array without the item we removed. + */ +function fewerTagsRemoveObjectByValue( arr, valueToRemove ) { + arr = arr.filter( ( item ) => parseInt( item.value ) !== parseInt( valueToRemove ) ); + return arr; +} + +/** + * Function to get an object from the array by its (integer) value. + * + * @param {Array} arr The array of terms. + * @param {number} valueToFind The term-ID. + * + * @return {Object} The term details. + */ +function fewerTagsGetObjectByValue( arr, valueToFind ) { + return arr.find( ( item ) => parseInt( item.value ) === parseInt( valueToFind ) ); +} + +let FTchoices = {}; +let FTbackup = {}; + +const FTchoicesElement = document.getElementById( 'fewer-tags-target-term-id' ); +fewerTagsGetAllTerms( FTchoicesElement.dataset.taxonomy ).then( ( terms ) => { + FTterms = terms; + FTchoices = new Choices( FTchoicesElement, { + choices: FTterms, + allowHTML: false, + position: 'bottom', + itemSelectText: '', + renderChoiceLimit: -1, + removeItemButton: true, + placeholder: false, + labelId: 'fewer-tags-target-term-id', + classNames: { + containerOuter: 'choices terms', + }, + callbackOnCreateTemplates( template ) { + return { + choice: ( { classNames }, data ) => { + return template( ` +
0 ? 'role="treeitem"' : 'role="option"' } + > + ${ data.label } ${ data.customProperties.count } +
+ ` ); + }, + }; + }, + } ); +} ); + +fewerTagsDomReady( () => { + let FTtaxonomies = []; + + const FTtaxonomyElement = document.getElementById( 'fewer-tags-target-taxonomy-slug' ); + fewerTagsGetTaxonomies( FTtaxonomyElement.dataset.currentTaxonomy ).then( ( taxonomies ) => { + FTtaxonomies = taxonomies; + new Choices( FTtaxonomyElement, { + choices: FTtaxonomies, + allowHTML: false, + position: 'bottom', + itemSelectText: '', + renderChoiceLimit: -1, + removeItemButton: true, + placeholder: false, + callbackOnCreateTemplates( template ) { + return { + choice: ( { classNames }, data ) => { + return template( ` +
0 ? 'role="treeitem"' : 'role="option"' } + > + ${ data.label } ${ data.customProperties.post_type } +
+ ` ); + }, + }; + }, + } ); + } ); + + FTtaxonomyElement.addEventListener( 'change', ( event ) => { + const targetTaxonomy = event.target.value; + const selectedTaxonomy = FTtaxonomies.find( ( t ) => t.value === targetTaxonomy ); + const restBase = selectedTaxonomy && selectedTaxonomy.customProperties ? selectedTaxonomy.customProperties.rest_base : ''; + fewerTagsGetAllTerms( targetTaxonomy, restBase ).then( ( terms ) => { + FTterms = terms; + FTchoices.setChoices( FTterms, 'value', 'label', true ); + document.getElementById( 'fewer-tags-target-term-label' ).textContent = 'Target ' + targetTaxonomy; + document.getElementById( 'fewer-tags-target-taxonomy-slug' ).value = targetTaxonomy; + } ); + } ); + + fewerTagsObserveElementRemoval( document.querySelectorAll( '#the-list tr' ), () => { + fewerTagsAjaxRequest( { + url: fewerTags.ajaxUrl, + data: { + action: 'fewer_tags_get_just_deleted_term', + _ajax_nonce: fewerTags.deleteTermNonce, + }, + successAction: ( response ) => { + const ajaxResponseEl = document.getElementById( 'ajax-response' ); + if ( ajaxResponseEl ) { + ajaxResponseEl.replaceChildren(); + ajaxResponseEl.innerHTML = response.data; + } + window.scrollTo( 0, 0 ); + }, + } ); + } ); + + document.querySelectorAll( '.fewer-tags-redirect-notice .notice-dismiss' ).forEach( ( element ) => { + element.addEventListener( 'click', () => { + fewerTagsAjaxRequest( { + url: fewerTags.ajaxUrl, + data: { + action: 'fewer_tags_dismiss_notice', + id: element.getAttribute( 'data-id' ), + taxonomy: element.getAttribute( 'data-taxonomy' ), + _ajax_nonce: element.getAttribute( 'data-nonce' ), + }, + successAction: ( response ) => { + // Remove the notice from the DOM. + document.getElementById( `fewer-tags-redirect-${ response.data }` ).remove(); + }, + } ); + } ); + } ); + + // Handler when 'Merge' link is clicked. Thickbox opens automatically, this sets the values for the form. + document.querySelectorAll( '.fewer-tags-merge-action' ).forEach( ( element ) => { + element.addEventListener( 'click', () => { + const termID = element.getAttribute( 'data-term-id' ); + const termName = element.getAttribute( 'data-term-name' ); + + document.getElementById( 'fewer-tags-source-term-id' ).value = termID; + document.getElementById( 'fewer-tags-source-term-name' ).value = termName; + // If we want to merge a term with another, + // then we need to remove the option in the dropdown + // because we obviously can't merge it with itself. + // The option is backed up, so we can add it again + // when the modal closes (see separate MutationObserver). + FTbackup = fewerTagsGetObjectByValue( FTterms, termID ); + FTterms = fewerTagsRemoveObjectByValue( FTterms, termID ); + FTchoices.setChoices( FTterms, 'value', 'label', true ); + + // Otherwise the old value will show up in the dropdown placeholder, super annoying. + document.querySelector( '.choices.terms .choices__item' ).innerText = ''; + + if ( fewerTags.defaultTermId && parseInt( termID ) === fewerTags.defaultTermId ) { + document.getElementById( 'fewer-tags-note' ).style.display = ''; + document.querySelector( '#fewer-tags-merge-form h3' ).style.display = 'none'; + } else { + document.getElementById( 'fewer-tags-note' ).style.display = 'none'; + document.querySelector( '#fewer-tags-merge-form h3' ).style.display = ''; + } + } ); + } ); + + document.getElementById( 'fewer-tags-merge-form' ).addEventListener( 'submit', ( e ) => { + e.preventDefault(); + + fewerTagsAjaxRequest( { + url: fewerTags.ajaxUrl, + data: { + action: 'fewer_tags_merge_terms', + source_taxonomy: document.getElementById( 'fewer-tags-taxonomy' ).value, + source_id: document.getElementById( 'fewer-tags-source-term-id' ).value, + target_taxonomy: document.getElementById( 'fewer-tags-target-taxonomy-slug' ).value, + target_id: document.getElementById( 'fewer-tags-target-term-id' ).value, + _ajax_nonce: document.getElementById( 'fewer-tags-merge-terms-nonce' ).value, + }, + successAction: ( response ) => { + // Remove the option from the list of terms and from the backup, as it must not exist there in case we want to merge more terms and should not be added back to the list. + FTbackup = {}; + FTterms = fewerTagsRemoveObjectByValue( FTterms, response.data.source_id ); + + // Hide the row from the table. + document.querySelector( `tr#tag-${ response.data.source_id }` ).style.display = 'none'; + + document.getElementById( 'ajax-response' ) + .innerHTML = ` +
+

${ response.data.msg }

+ +
`; + + const elTr = document.querySelector( `tr#tag-${ response.data.target_id } td.column-posts a` ); + if ( elTr ) { + elTr.innerHTML = response.data.target_count; + } + + // Add event listener to the dismiss button. + document.querySelector( '#ajax-response .notice-dismiss' ).addEventListener( 'click', ( event ) => { + event.target.closest( '.notice' ).remove(); + } ); + + tb_remove(); + + window.scrollTo( 0, 0 ); + }, + } ); + } ); +} ); + +// When the modal closes, we need to show all options again, +// So they are available for the next merge. +// We can do that by using a MutationObserver, and checking +// if the modal-open class gets removed from the . +let fewerTagsModalWasOpen = document.body.classList.contains( 'modal-open' ); +const fewerTagsBodyObserver = new MutationObserver( ( mutations ) => { + mutations.forEach( ( mutation ) => { + if ( mutation.attributeName === 'class' ) { + const currentState = mutation.target.classList.contains( 'modal-open' ); + if ( fewerTagsModalWasOpen !== currentState ) { + fewerTagsModalWasOpen = currentState; + if ( ! currentState ) { + // The modal was closed, so we can restore the removed option from the backup. + FTterms.push( FTbackup ); + } + } + } + } ); +} ); +fewerTagsBodyObserver.observe( document.body, { + attributes: true, + attributeOldValue: true, + attributeFilter: [ 'class' ], +} ); diff --git a/js/vendor/choices.min.css b/js/vendor/choices.min.css new file mode 100644 index 0000000..9260536 --- /dev/null +++ b/js/vendor/choices.min.css @@ -0,0 +1 @@ +.choices{position:relative;overflow:hidden;margin-bottom:24px;font-size:16px}.choices:focus{outline:0}.choices:last-child{margin-bottom:0}.choices.is-open{overflow:visible}.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices [hidden]{display:none!important}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:100%;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.25}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #00bcd4}.choices[data-type*=select-one] .choices__item[data-value=""] .choices__button{display:none}.choices[data-type*=select-one]::after{content:"";height:0;width:0;border-style:solid;border-color:#333 transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open::after{border-color:transparent transparent #333;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]::after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0-4px 0 8px;padding-left:16px;border-left:1px solid #008fa1;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);background-size:8px;width:8px;line-height:1;opacity:.75;border-radius:0}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:100%;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:44px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#00bcd4;border:1px solid #00a5bb;color:#fff;word-break:break-all;box-sizing:border-box}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#00a5bb;border:1px solid #008fa1}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown,.choices__list[aria-expanded]{visibility:hidden;z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all;will-change:visibility}.is-active.choices__list--dropdown,.is-active.choices__list[aria-expanded]{visibility:visible}.is-open .choices__list--dropdown,.is-open .choices__list[aria-expanded]{border-color:#b7b7b7}.is-flipped .choices__list--dropdown,.is-flipped .choices__list[aria-expanded]{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list,.choices__list[aria-expanded] .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item,.choices__list[aria-expanded] .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item,[dir=rtl] .choices__list[aria-expanded] .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable,.choices__list[aria-expanded] .choices__item--selectable{padding-right:100px}.choices__list--dropdown .choices__item--selectable::after,.choices__list[aria-expanded] .choices__item--selectable::after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable,[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable::after,[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable::after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted,.choices__list[aria-expanded] .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted::after,.choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;-webkit-appearance:none;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus,.choices__input:focus{outline:0}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}.choices__input::-webkit-search-cancel-button,.choices__input::-webkit-search-decoration,.choices__input::-webkit-search-results-button,.choices__input::-webkit-search-results-decoration{display:none}.choices__input::-ms-clear,.choices__input::-ms-reveal{display:none;width:0;height:0}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5} \ No newline at end of file diff --git a/js/vendor/choices.min.js b/js/vendor/choices.min.js new file mode 100644 index 0000000..af28094 --- /dev/null +++ b/js/vendor/choices.min.js @@ -0,0 +1,2 @@ +/*! For license information please see choices.min.js.LICENSE.txt */ +!function(){"use strict";var e={282:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.clearChoices=t.activateChoices=t.filterChoices=t.addChoice=void 0;var n=i(883);t.addChoice=function(e){var t=e.value,i=e.label,r=e.id,s=e.groupId,o=e.disabled,a=e.elementId,c=e.customProperties,l=e.placeholder,h=e.keyCode;return{type:n.ACTION_TYPES.ADD_CHOICE,value:t,label:i,id:r,groupId:s,disabled:o,elementId:a,customProperties:c,placeholder:l,keyCode:h}},t.filterChoices=function(e){return{type:n.ACTION_TYPES.FILTER_CHOICES,results:e}},t.activateChoices=function(e){return void 0===e&&(e=!0),{type:n.ACTION_TYPES.ACTIVATE_CHOICES,active:e}},t.clearChoices=function(){return{type:n.ACTION_TYPES.CLEAR_CHOICES}}},783:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.addGroup=void 0;var n=i(883);t.addGroup=function(e){var t=e.value,i=e.id,r=e.active,s=e.disabled;return{type:n.ACTION_TYPES.ADD_GROUP,value:t,id:i,active:r,disabled:s}}},464:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.highlightItem=t.removeItem=t.addItem=void 0;var n=i(883);t.addItem=function(e){var t=e.value,i=e.label,r=e.id,s=e.choiceId,o=e.groupId,a=e.customProperties,c=e.placeholder,l=e.keyCode;return{type:n.ACTION_TYPES.ADD_ITEM,value:t,label:i,id:r,choiceId:s,groupId:o,customProperties:a,placeholder:c,keyCode:l}},t.removeItem=function(e,t){return{type:n.ACTION_TYPES.REMOVE_ITEM,id:e,choiceId:t}},t.highlightItem=function(e,t){return{type:n.ACTION_TYPES.HIGHLIGHT_ITEM,id:e,highlighted:t}}},137:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.setIsLoading=t.resetTo=t.clearAll=void 0;var n=i(883);t.clearAll=function(){return{type:n.ACTION_TYPES.CLEAR_ALL}},t.resetTo=function(e){return{type:n.ACTION_TYPES.RESET_TO,state:e}},t.setIsLoading=function(e){return{type:n.ACTION_TYPES.SET_IS_LOADING,isLoading:e}}},373:function(e,t,i){var n=this&&this.__spreadArray||function(e,t,i){if(i||2===arguments.length)for(var n,r=0,s=t.length;r=0?this._store.getGroupById(r):null;return this._store.dispatch((0,l.highlightItem)(i,!0)),t&&this.passedElement.triggerEvent(d.EVENTS.highlightItem,{id:i,value:o,label:c,groupValue:h&&h.value?h.value:null}),this},e.prototype.unhighlightItem=function(e){if(!e||!e.id)return this;var t=e.id,i=e.groupId,n=void 0===i?-1:i,r=e.value,s=void 0===r?"":r,o=e.label,a=void 0===o?"":o,c=n>=0?this._store.getGroupById(n):null;return this._store.dispatch((0,l.highlightItem)(t,!1)),this.passedElement.triggerEvent(d.EVENTS.highlightItem,{id:t,value:s,label:a,groupValue:c&&c.value?c.value:null}),this},e.prototype.highlightAll=function(){var e=this;return this._store.items.forEach((function(t){return e.highlightItem(t)})),this},e.prototype.unhighlightAll=function(){var e=this;return this._store.items.forEach((function(t){return e.unhighlightItem(t)})),this},e.prototype.removeActiveItemsByValue=function(e){var t=this;return this._store.activeItems.filter((function(t){return t.value===e})).forEach((function(e){return t._removeItem(e)})),this},e.prototype.removeActiveItems=function(e){var t=this;return this._store.activeItems.filter((function(t){return t.id!==e})).forEach((function(e){return t._removeItem(e)})),this},e.prototype.removeHighlightedItems=function(e){var t=this;return void 0===e&&(e=!1),this._store.highlightedActiveItems.forEach((function(i){t._removeItem(i),e&&t._triggerChange(i.value)})),this},e.prototype.showDropdown=function(e){var t=this;return this.dropdown.isActive||requestAnimationFrame((function(){t.dropdown.show(),t.containerOuter.open(t.dropdown.distanceFromTopWindow),!e&&t._canSearch&&t.input.focus(),t.passedElement.triggerEvent(d.EVENTS.showDropdown,{})})),this},e.prototype.hideDropdown=function(e){var t=this;return this.dropdown.isActive?(requestAnimationFrame((function(){t.dropdown.hide(),t.containerOuter.close(),!e&&t._canSearch&&(t.input.removeActiveDescendant(),t.input.blur()),t.passedElement.triggerEvent(d.EVENTS.hideDropdown,{})})),this):this},e.prototype.getValue=function(e){void 0===e&&(e=!1);var t=this._store.activeItems.reduce((function(t,i){var n=e?i.value:i;return t.push(n),t}),[]);return this._isSelectOneElement?t[0]:t},e.prototype.setValue=function(e){var t=this;return this.initialised?(e.forEach((function(e){return t._setChoiceOrItem(e)})),this):this},e.prototype.setChoiceByValue=function(e){var t=this;return!this.initialised||this._isTextElement||(Array.isArray(e)?e:[e]).forEach((function(e){return t._findAndSelectChoiceByValue(e)})),this},e.prototype.setChoices=function(e,t,i,n){var r=this;if(void 0===e&&(e=[]),void 0===t&&(t="value"),void 0===i&&(i="label"),void 0===n&&(n=!1),!this.initialised)throw new ReferenceError("setChoices was called on a non-initialized instance of Choices");if(!this._isSelectElement)throw new TypeError("setChoices can't be used with INPUT based Choices");if("string"!=typeof t||!t)throw new TypeError("value parameter must be a name of 'value' field in passed objects");if(n&&this.clearChoices(),"function"==typeof e){var s=e(this);if("function"==typeof Promise&&s instanceof Promise)return new Promise((function(e){return requestAnimationFrame(e)})).then((function(){return r._handleLoadingState(!0)})).then((function(){return s})).then((function(e){return r.setChoices(e,t,i,n)})).catch((function(e){r.config.silent||console.error(e)})).then((function(){return r._handleLoadingState(!1)})).then((function(){return r}));if(!Array.isArray(s))throw new TypeError(".setChoices first argument function must return either array of choices or Promise, got: ".concat(typeof s));return this.setChoices(s,t,i,!1)}if(!Array.isArray(e))throw new TypeError(".setChoices must be called either with array of choices with a function resulting into Promise of array of choices");return this.containerOuter.removeLoadingState(),this._startLoading(),e.forEach((function(e){if(e.choices)r._addGroup({id:e.id?parseInt("".concat(e.id),10):null,group:e,valueKey:t,labelKey:i});else{var n=e;r._addChoice({value:n[t],label:n[i],isSelected:!!n.selected,isDisabled:!!n.disabled,placeholder:!!n.placeholder,customProperties:n.customProperties})}})),this._stopLoading(),this},e.prototype.clearChoices=function(){return this._store.dispatch((0,a.clearChoices)()),this},e.prototype.clearStore=function(){return this._store.dispatch((0,h.clearAll)()),this},e.prototype.clearInput=function(){var e=!this._isSelectOneElement;return this.input.clear(e),!this._isTextElement&&this._canSearch&&(this._isSearching=!1,this._store.dispatch((0,a.activateChoices)(!0))),this},e.prototype._render=function(){if(!this._store.isLoading()){this._currentState=this._store.state;var e=this._currentState.choices!==this._prevState.choices||this._currentState.groups!==this._prevState.groups||this._currentState.items!==this._prevState.items,t=this._isSelectElement,i=this._currentState.items!==this._prevState.items;e&&(t&&this._renderChoices(),i&&this._renderItems(),this._prevState=this._currentState)}},e.prototype._renderChoices=function(){var e=this,t=this._store,i=t.activeGroups,n=t.activeChoices,r=document.createDocumentFragment();if(this.choiceList.clear(),this.config.resetScrollPosition&&requestAnimationFrame((function(){return e.choiceList.scrollToTop()})),i.length>=1&&!this._isSearching){var s=n.filter((function(e){return!0===e.placeholder&&-1===e.groupId}));s.length>=1&&(r=this._createChoicesFragment(s,r)),r=this._createGroupsFragment(i,n,r)}else n.length>=1&&(r=this._createChoicesFragment(n,r));if(r.childNodes&&r.childNodes.length>0){var o=this._store.activeItems,a=this._canAddItem(o,this.input.value);if(a.response)this.choiceList.append(r),this._highlightChoice();else{var c=this._getTemplate("notice",a.notice);this.choiceList.append(c)}}else{var l=void 0;c=void 0,this._isSearching?(c="function"==typeof this.config.noResultsText?this.config.noResultsText():this.config.noResultsText,l=this._getTemplate("notice",c,"no-results")):(c="function"==typeof this.config.noChoicesText?this.config.noChoicesText():this.config.noChoicesText,l=this._getTemplate("notice",c,"no-choices")),this.choiceList.append(l)}},e.prototype._renderItems=function(){var e=this._store.activeItems||[];this.itemList.clear();var t=this._createItemsFragment(e);t.childNodes&&this.itemList.append(t)},e.prototype._createGroupsFragment=function(e,t,i){var n=this;return void 0===i&&(i=document.createDocumentFragment()),this.config.shouldSort&&e.sort(this.config.sorter),e.forEach((function(e){var r=function(e){return t.filter((function(t){return n._isSelectOneElement?t.groupId===e.id:t.groupId===e.id&&("always"===n.config.renderSelectedChoices||!t.selected)}))}(e);if(r.length>=1){var s=n._getTemplate("choiceGroup",e);i.appendChild(s),n._createChoicesFragment(r,i,!0)}})),i},e.prototype._createChoicesFragment=function(e,t,i){var r=this;void 0===t&&(t=document.createDocumentFragment()),void 0===i&&(i=!1);var s=this.config,o=s.renderSelectedChoices,a=s.searchResultLimit,c=s.renderChoiceLimit,l=this._isSearching?f.sortByScore:this.config.sorter,h=function(e){if("auto"!==o||r._isSelectOneElement||!e.selected){var i=r._getTemplate("choice",e,r.config.itemSelectText);t.appendChild(i)}},u=e;"auto"!==o||this._isSelectOneElement||(u=e.filter((function(e){return!e.selected})));var d=u.reduce((function(e,t){return t.placeholder?e.placeholderChoices.push(t):e.normalChoices.push(t),e}),{placeholderChoices:[],normalChoices:[]}),p=d.placeholderChoices,m=d.normalChoices;(this.config.shouldSort||this._isSearching)&&m.sort(l);var v=u.length,g=this._isSelectOneElement?n(n([],p,!0),m,!0):m;this._isSearching?v=a:c&&c>0&&!i&&(v=c);for(var _=0;_=n){var o=r?this._searchChoices(e):0;this.passedElement.triggerEvent(d.EVENTS.search,{value:e,resultCount:o})}else s&&(this._isSearching=!1,this._store.dispatch((0,a.activateChoices)(!0)))}},e.prototype._canAddItem=function(e,t){var i=!0,n="function"==typeof this.config.addItemText?this.config.addItemText(t):this.config.addItemText;if(!this._isSelectOneElement){var r=(0,f.existsInArray)(e,t);this.config.maxItemCount>0&&this.config.maxItemCount<=e.length&&(i=!1,n="function"==typeof this.config.maxItemText?this.config.maxItemText(this.config.maxItemCount):this.config.maxItemText),!this.config.duplicateItemsAllowed&&r&&i&&(i=!1,n="function"==typeof this.config.uniqueItemText?this.config.uniqueItemText(t):this.config.uniqueItemText),this._isTextElement&&this.config.addItems&&i&&"function"==typeof this.config.addItemFilter&&!this.config.addItemFilter(t)&&(i=!1,n="function"==typeof this.config.customAddItemText?this.config.customAddItemText(t):this.config.customAddItemText)}return{response:i,notice:n}},e.prototype._searchChoices=function(e){var t="string"==typeof e?e.trim():e,i="string"==typeof this._currentValue?this._currentValue.trim():this._currentValue;if(t.length<1&&t==="".concat(i," "))return 0;var r=this._store.searchableChoices,s=t,c=Object.assign(this.config.fuseOptions,{keys:n([],this.config.searchFields,!0),includeMatches:!0}),l=new o.default(r,c).search(s);return this._currentValue=t,this._highlightPosition=0,this._isSearching=!0,this._store.dispatch((0,a.filterChoices)(l)),l.length},e.prototype._addEventListeners=function(){var e=document.documentElement;e.addEventListener("touchend",this._onTouchEnd,!0),this.containerOuter.element.addEventListener("keydown",this._onKeyDown,!0),this.containerOuter.element.addEventListener("mousedown",this._onMouseDown,!0),e.addEventListener("click",this._onClick,{passive:!0}),e.addEventListener("touchmove",this._onTouchMove,{passive:!0}),this.dropdown.element.addEventListener("mouseover",this._onMouseOver,{passive:!0}),this._isSelectOneElement&&(this.containerOuter.element.addEventListener("focus",this._onFocus,{passive:!0}),this.containerOuter.element.addEventListener("blur",this._onBlur,{passive:!0})),this.input.element.addEventListener("keyup",this._onKeyUp,{passive:!0}),this.input.element.addEventListener("focus",this._onFocus,{passive:!0}),this.input.element.addEventListener("blur",this._onBlur,{passive:!0}),this.input.element.form&&this.input.element.form.addEventListener("reset",this._onFormReset,{passive:!0}),this.input.addEventListeners()},e.prototype._removeEventListeners=function(){var e=document.documentElement;e.removeEventListener("touchend",this._onTouchEnd,!0),this.containerOuter.element.removeEventListener("keydown",this._onKeyDown,!0),this.containerOuter.element.removeEventListener("mousedown",this._onMouseDown,!0),e.removeEventListener("click",this._onClick),e.removeEventListener("touchmove",this._onTouchMove),this.dropdown.element.removeEventListener("mouseover",this._onMouseOver),this._isSelectOneElement&&(this.containerOuter.element.removeEventListener("focus",this._onFocus),this.containerOuter.element.removeEventListener("blur",this._onBlur)),this.input.element.removeEventListener("keyup",this._onKeyUp),this.input.element.removeEventListener("focus",this._onFocus),this.input.element.removeEventListener("blur",this._onBlur),this.input.element.form&&this.input.element.form.removeEventListener("reset",this._onFormReset),this.input.removeEventListeners()},e.prototype._onKeyDown=function(e){var t=e.keyCode,i=this._store.activeItems,n=this.input.isFocussed,r=this.dropdown.isActive,s=this.itemList.hasChildren(),o=String.fromCharCode(t),a=/[^\x00-\x1F]/.test(o),c=d.KEY_CODES.BACK_KEY,l=d.KEY_CODES.DELETE_KEY,h=d.KEY_CODES.ENTER_KEY,u=d.KEY_CODES.A_KEY,p=d.KEY_CODES.ESC_KEY,f=d.KEY_CODES.UP_KEY,m=d.KEY_CODES.DOWN_KEY,v=d.KEY_CODES.PAGE_UP_KEY,g=d.KEY_CODES.PAGE_DOWN_KEY;switch(this._isTextElement||r||!a||(this.showDropdown(),this.input.isFocussed||(this.input.value+=e.key.toLowerCase())),t){case u:return this._onSelectKey(e,s);case h:return this._onEnterKey(e,i,r);case p:return this._onEscapeKey(r);case f:case v:case m:case g:return this._onDirectionKey(e,r);case l:case c:return this._onDeleteKey(e,i,n)}},e.prototype._onKeyUp=function(e){var t=e.target,i=e.keyCode,n=this.input.value,r=this._store.activeItems,s=this._canAddItem(r,n),o=d.KEY_CODES.BACK_KEY,c=d.KEY_CODES.DELETE_KEY;if(this._isTextElement)if(s.notice&&n){var l=this._getTemplate("notice",s.notice);this.dropdown.element.innerHTML=l.outerHTML,this.showDropdown(!0)}else this.hideDropdown(!0);else{var h=(i===o||i===c)&&t&&!t.value,u=!this._isTextElement&&this._isSearching,p=this._canSearch&&s.response;h&&u?(this._isSearching=!1,this._store.dispatch((0,a.activateChoices)(!0))):p&&this._handleSearch(this.input.rawValue)}this._canSearch=this.config.searchEnabled},e.prototype._onSelectKey=function(e,t){var i=e.ctrlKey,n=e.metaKey;(i||n)&&t&&(this._canSearch=!1,this.config.removeItems&&!this.input.value&&this.input.element===document.activeElement&&this.highlightAll())},e.prototype._onEnterKey=function(e,t,i){var n=e.target,r=d.KEY_CODES.ENTER_KEY,s=n&&n.hasAttribute("data-button");if(this._isTextElement&&n&&n.value){var o=this.input.value;this._canAddItem(t,o).response&&(this.hideDropdown(!0),this._addItem({value:o}),this._triggerChange(o),this.clearInput())}if(s&&(this._handleButtonAction(t,n),e.preventDefault()),i){var a=this.dropdown.getChild(".".concat(this.config.classNames.highlightedState));a&&(t[0]&&(t[0].keyCode=r),this._handleChoiceAction(t,a)),e.preventDefault()}else this._isSelectOneElement&&(this.showDropdown(),e.preventDefault())},e.prototype._onEscapeKey=function(e){e&&(this.hideDropdown(!0),this.containerOuter.focus())},e.prototype._onDirectionKey=function(e,t){var i=e.keyCode,n=e.metaKey,r=d.KEY_CODES.DOWN_KEY,s=d.KEY_CODES.PAGE_UP_KEY,o=d.KEY_CODES.PAGE_DOWN_KEY;if(t||this._isSelectOneElement){this.showDropdown(),this._canSearch=!1;var a=i===r||i===o?1:-1,c="[data-choice-selectable]",l=void 0;if(n||i===o||i===s)l=a>0?this.dropdown.element.querySelector("".concat(c,":last-of-type")):this.dropdown.element.querySelector(c);else{var h=this.dropdown.element.querySelector(".".concat(this.config.classNames.highlightedState));l=h?(0,f.getAdjacentEl)(h,c,a):this.dropdown.element.querySelector(c)}l&&((0,f.isScrolledIntoView)(l,this.choiceList.element,a)||this.choiceList.scrollToChildElement(l,a),this._highlightChoice(l)),e.preventDefault()}},e.prototype._onDeleteKey=function(e,t,i){var n=e.target;this._isSelectOneElement||n.value||!i||(this._handleBackspace(t),e.preventDefault())},e.prototype._onTouchMove=function(){this._wasTap&&(this._wasTap=!1)},e.prototype._onTouchEnd=function(e){var t=(e||e.touches[0]).target;this._wasTap&&this.containerOuter.element.contains(t)&&((t===this.containerOuter.element||t===this.containerInner.element)&&(this._isTextElement?this.input.focus():this._isSelectMultipleElement&&this.showDropdown()),e.stopPropagation()),this._wasTap=!0},e.prototype._onMouseDown=function(e){var t=e.target;if(t instanceof HTMLElement){if(_&&this.choiceList.element.contains(t)){var i=this.choiceList.element.firstElementChild,n="ltr"===this._direction?e.offsetX>=i.offsetWidth:e.offsetX0&&this.unhighlightAll(),this.containerOuter.removeFocusState(),this.hideDropdown(!0))},e.prototype._onFocus=function(e){var t,i=this,n=e.target;n&&this.containerOuter.element.contains(n)&&((t={})[d.TEXT_TYPE]=function(){n===i.input.element&&i.containerOuter.addFocusState()},t[d.SELECT_ONE_TYPE]=function(){i.containerOuter.addFocusState(),n===i.input.element&&i.showDropdown(!0)},t[d.SELECT_MULTIPLE_TYPE]=function(){n===i.input.element&&(i.showDropdown(!0),i.containerOuter.addFocusState())},t)[this.passedElement.element.type]()},e.prototype._onBlur=function(e){var t,i=this,n=e.target;if(n&&this.containerOuter.element.contains(n)&&!this._isScrollingOnIe){var r=this._store.activeItems.some((function(e){return e.highlighted}));((t={})[d.TEXT_TYPE]=function(){n===i.input.element&&(i.containerOuter.removeFocusState(),r&&i.unhighlightAll(),i.hideDropdown(!0))},t[d.SELECT_ONE_TYPE]=function(){i.containerOuter.removeFocusState(),(n===i.input.element||n===i.containerOuter.element&&!i._canSearch)&&i.hideDropdown(!0)},t[d.SELECT_MULTIPLE_TYPE]=function(){n===i.input.element&&(i.containerOuter.removeFocusState(),i.hideDropdown(!0),r&&i.unhighlightAll())},t)[this.passedElement.element.type]()}else this._isScrollingOnIe=!1,this.input.element.focus()},e.prototype._onFormReset=function(){this._store.dispatch((0,h.resetTo)(this._initialState))},e.prototype._highlightChoice=function(e){var t=this;void 0===e&&(e=null);var i=Array.from(this.dropdown.element.querySelectorAll("[data-choice-selectable]"));if(i.length){var n=e;Array.from(this.dropdown.element.querySelectorAll(".".concat(this.config.classNames.highlightedState))).forEach((function(e){e.classList.remove(t.config.classNames.highlightedState),e.setAttribute("aria-selected","false")})),n?this._highlightPosition=i.indexOf(n):(n=i.length>this._highlightPosition?i[this._highlightPosition]:i[i.length-1])||(n=i[0]),n.classList.add(this.config.classNames.highlightedState),n.setAttribute("aria-selected","true"),this.passedElement.triggerEvent(d.EVENTS.highlightChoice,{el:n}),this.dropdown.isActive&&(this.input.setActiveDescendant(n.id),this.containerOuter.setActiveDescendant(n.id))}},e.prototype._addItem=function(e){var t=e.value,i=e.label,n=void 0===i?null:i,r=e.choiceId,s=void 0===r?-1:r,o=e.groupId,a=void 0===o?-1:o,c=e.customProperties,h=void 0===c?{}:c,u=e.placeholder,p=void 0!==u&&u,f=e.keyCode,m=void 0===f?-1:f,v="string"==typeof t?t.trim():t,g=this._store.items,_=n||v,y=s||-1,E=a>=0?this._store.getGroupById(a):null,b=g?g.length+1:1;this.config.prependValue&&(v=this.config.prependValue+v.toString()),this.config.appendValue&&(v+=this.config.appendValue.toString()),this._store.dispatch((0,l.addItem)({value:v,label:_,id:b,choiceId:y,groupId:a,customProperties:h,placeholder:p,keyCode:m})),this._isSelectOneElement&&this.removeActiveItems(b),this.passedElement.triggerEvent(d.EVENTS.addItem,{id:b,value:v,label:_,customProperties:h,groupValue:E&&E.value?E.value:null,keyCode:m})},e.prototype._removeItem=function(e){var t=e.id,i=e.value,n=e.label,r=e.customProperties,s=e.choiceId,o=e.groupId,a=o&&o>=0?this._store.getGroupById(o):null;t&&s&&(this._store.dispatch((0,l.removeItem)(t,s)),this.passedElement.triggerEvent(d.EVENTS.removeItem,{id:t,value:i,label:n,customProperties:r,groupValue:a&&a.value?a.value:null}))},e.prototype._addChoice=function(e){var t=e.value,i=e.label,n=void 0===i?null:i,r=e.isSelected,s=void 0!==r&&r,o=e.isDisabled,c=void 0!==o&&o,l=e.groupId,h=void 0===l?-1:l,u=e.customProperties,d=void 0===u?{}:u,p=e.placeholder,f=void 0!==p&&p,m=e.keyCode,v=void 0===m?-1:m;if(null!=t){var g=this._store.choices,_=n||t,y=g?g.length+1:1,E="".concat(this._baseId,"-").concat(this._idNames.itemChoice,"-").concat(y);this._store.dispatch((0,a.addChoice)({id:y,groupId:h,elementId:E,value:t,label:_,disabled:c,customProperties:d,placeholder:f,keyCode:v})),s&&this._addItem({value:t,label:_,choiceId:y,customProperties:d,placeholder:f,keyCode:v})}},e.prototype._addGroup=function(e){var t=this,i=e.group,n=e.id,r=e.valueKey,s=void 0===r?"value":r,o=e.labelKey,a=void 0===o?"label":o,l=(0,f.isType)("Object",i)?i.choices:Array.from(i.getElementsByTagName("OPTION")),h=n||Math.floor((new Date).valueOf()*Math.random()),u=!!i.disabled&&i.disabled;l?(this._store.dispatch((0,c.addGroup)({value:i.label,id:h,active:!0,disabled:u})),l.forEach((function(e){var i=e.disabled||e.parentNode&&e.parentNode.disabled;t._addChoice({value:e[s],label:(0,f.isType)("Object",e)?e[a]:e.innerHTML,isSelected:e.selected,isDisabled:i,groupId:h,customProperties:e.customProperties,placeholder:e.placeholder})}))):this._store.dispatch((0,c.addGroup)({value:i.label,id:i.id,active:!1,disabled:i.disabled}))},e.prototype._getTemplate=function(e){for(var t,i=[],r=1;r0?this.element.scrollTop+o-r:e.offsetTop;requestAnimationFrame((function(){i._animateScroll(a,t)}))}},e.prototype._scrollDown=function(e,t,i){var n=(i-e)/t,r=n>1?n:1;this.element.scrollTop=e+r},e.prototype._scrollUp=function(e,t,i){var n=(e-i)/t,r=n>1?n:1;this.element.scrollTop=e-r},e.prototype._animateScroll=function(e,t){var i=this,r=n.SCROLLING_SPEED,s=this.element.scrollTop,o=!1;t>0?(this._scrollDown(s,r,e),se&&(o=!0)),o&&requestAnimationFrame((function(){i._animateScroll(e,t)}))},e}();t.default=r},730:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0});var n=i(799),r=function(){function e(e){var t=e.element,i=e.classNames;if(this.element=t,this.classNames=i,!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement))throw new TypeError("Invalid element passed");this.isDisabled=!1}return Object.defineProperty(e.prototype,"isActive",{get:function(){return"active"===this.element.dataset.choice},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"dir",{get:function(){return this.element.dir},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"value",{get:function(){return this.element.value},set:function(e){this.element.value=e},enumerable:!1,configurable:!0}),e.prototype.conceal=function(){this.element.classList.add(this.classNames.input),this.element.hidden=!0,this.element.tabIndex=-1;var e=this.element.getAttribute("style");e&&this.element.setAttribute("data-choice-orig-style",e),this.element.setAttribute("data-choice","active")},e.prototype.reveal=function(){this.element.classList.remove(this.classNames.input),this.element.hidden=!1,this.element.removeAttribute("tabindex");var e=this.element.getAttribute("data-choice-orig-style");e?(this.element.removeAttribute("data-choice-orig-style"),this.element.setAttribute("style",e)):this.element.removeAttribute("style"),this.element.removeAttribute("data-choice"),this.element.value=this.element.value},e.prototype.enable=function(){this.element.removeAttribute("disabled"),this.element.disabled=!1,this.isDisabled=!1},e.prototype.disable=function(){this.element.setAttribute("disabled",""),this.element.disabled=!0,this.isDisabled=!0},e.prototype.triggerEvent=function(e,t){(0,n.dispatchEvent)(this.element,e,t)},e}();t.default=r},541:function(e,t,i){var n,r=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var o=function(e){function t(t){var i=t.element,n=t.classNames,r=t.delimiter,s=e.call(this,{element:i,classNames:n})||this;return s.delimiter=r,s}return r(t,e),Object.defineProperty(t.prototype,"value",{get:function(){return this.element.value},set:function(e){this.element.setAttribute("value",e),this.element.value=e},enumerable:!1,configurable:!0}),t}(s(i(730)).default);t.default=o},982:function(e,t,i){var n,r=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var o=function(e){function t(t){var i=t.element,n=t.classNames,r=t.template,s=e.call(this,{element:i,classNames:n})||this;return s.template=r,s}return r(t,e),Object.defineProperty(t.prototype,"placeholderOption",{get:function(){return this.element.querySelector('option[value=""]')||this.element.querySelector("option[placeholder]")},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"optionGroups",{get:function(){return Array.from(this.element.getElementsByTagName("OPTGROUP"))},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"options",{get:function(){return Array.from(this.element.options)},set:function(e){var t=this,i=document.createDocumentFragment();e.forEach((function(e){return n=e,r=t.template(n),void i.appendChild(r);var n,r})),this.appendDocFragment(i)},enumerable:!1,configurable:!0}),t.prototype.appendDocFragment=function(e){this.element.innerHTML="",this.element.appendChild(e)},t}(s(i(730)).default);t.default=o},883:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.SCROLLING_SPEED=t.SELECT_MULTIPLE_TYPE=t.SELECT_ONE_TYPE=t.TEXT_TYPE=t.KEY_CODES=t.ACTION_TYPES=t.EVENTS=void 0,t.EVENTS={showDropdown:"showDropdown",hideDropdown:"hideDropdown",change:"change",choice:"choice",search:"search",addItem:"addItem",removeItem:"removeItem",highlightItem:"highlightItem",highlightChoice:"highlightChoice",unhighlightItem:"unhighlightItem"},t.ACTION_TYPES={ADD_CHOICE:"ADD_CHOICE",FILTER_CHOICES:"FILTER_CHOICES",ACTIVATE_CHOICES:"ACTIVATE_CHOICES",CLEAR_CHOICES:"CLEAR_CHOICES",ADD_GROUP:"ADD_GROUP",ADD_ITEM:"ADD_ITEM",REMOVE_ITEM:"REMOVE_ITEM",HIGHLIGHT_ITEM:"HIGHLIGHT_ITEM",CLEAR_ALL:"CLEAR_ALL",RESET_TO:"RESET_TO",SET_IS_LOADING:"SET_IS_LOADING"},t.KEY_CODES={BACK_KEY:46,DELETE_KEY:8,ENTER_KEY:13,A_KEY:65,ESC_KEY:27,UP_KEY:38,DOWN_KEY:40,PAGE_UP_KEY:33,PAGE_DOWN_KEY:34},t.TEXT_TYPE="text",t.SELECT_ONE_TYPE="select-one",t.SELECT_MULTIPLE_TYPE="select-multiple",t.SCROLLING_SPEED=4},789:function(e,t,i){Object.defineProperty(t,"__esModule",{value:!0}),t.DEFAULT_CONFIG=t.DEFAULT_CLASSNAMES=void 0;var n=i(799);t.DEFAULT_CLASSNAMES={containerOuter:"choices",containerInner:"choices__inner",input:"choices__input",inputCloned:"choices__input--cloned",list:"choices__list",listItems:"choices__list--multiple",listSingle:"choices__list--single",listDropdown:"choices__list--dropdown",item:"choices__item",itemSelectable:"choices__item--selectable",itemDisabled:"choices__item--disabled",itemChoice:"choices__item--choice",placeholder:"choices__placeholder",group:"choices__group",groupHeading:"choices__heading",button:"choices__button",activeState:"is-active",focusState:"is-focused",openState:"is-open",disabledState:"is-disabled",highlightedState:"is-highlighted",selectedState:"is-selected",flippedState:"is-flipped",loadingState:"is-loading",noResults:"has-no-results",noChoices:"has-no-choices"},t.DEFAULT_CONFIG={items:[],choices:[],silent:!1,renderChoiceLimit:-1,maxItemCount:-1,addItems:!0,addItemFilter:null,removeItems:!0,removeItemButton:!1,editItems:!1,allowHTML:!0,duplicateItemsAllowed:!0,delimiter:",",paste:!0,searchEnabled:!0,searchChoices:!0,searchFloor:1,searchResultLimit:4,searchFields:["label","value"],position:"auto",resetScrollPosition:!0,shouldSort:!0,shouldSortItems:!1,sorter:n.sortByAlpha,placeholder:!0,placeholderValue:null,searchPlaceholderValue:null,prependValue:null,appendValue:null,renderSelectedChoices:"auto",loadingText:"Loading...",noResultsText:"No results found",noChoicesText:"No choices to choose from",itemSelectText:"Press to select",uniqueItemText:"Only unique values can be added",customAddItemText:"Only values matching specific conditions can be added",addItemText:function(e){return'Press Enter to add "'.concat((0,n.sanitise)(e),'"')},maxItemText:function(e){return"Only ".concat(e," values can be added")},valueComparer:function(e,t){return e===t},fuseOptions:{includeScore:!0},labelId:"",callbackOnInit:null,callbackOnCreateTemplates:null,classNames:t.DEFAULT_CLASSNAMES}},18:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},978:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},948:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},359:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},285:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},533:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},187:function(e,t,i){var n=this&&this.__createBinding||(Object.create?function(e,t,i,n){void 0===n&&(n=i);var r=Object.getOwnPropertyDescriptor(t,i);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[i]}}),Object.defineProperty(e,n,r)}:function(e,t,i,n){void 0===n&&(n=i),e[n]=t[i]}),r=this&&this.__exportStar||function(e,t){for(var i in e)"default"===i||Object.prototype.hasOwnProperty.call(t,i)||n(t,e,i)};Object.defineProperty(t,"__esModule",{value:!0}),r(i(18),t),r(i(978),t),r(i(948),t),r(i(359),t),r(i(285),t),r(i(533),t),r(i(287),t),r(i(132),t),r(i(837),t),r(i(598),t),r(i(369),t),r(i(37),t),r(i(47),t),r(i(923),t),r(i(876),t)},287:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},132:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},837:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},598:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},37:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},369:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},47:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},923:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},876:function(e,t){Object.defineProperty(t,"__esModule",{value:!0})},799:function(e,t){var i;Object.defineProperty(t,"__esModule",{value:!0}),t.parseCustomProperties=t.diff=t.cloneObject=t.existsInArray=t.dispatchEvent=t.sortByScore=t.sortByAlpha=t.strToEl=t.sanitise=t.isScrolledIntoView=t.getAdjacentEl=t.wrap=t.isType=t.getType=t.generateId=t.generateChars=t.getRandomNumber=void 0,t.getRandomNumber=function(e,t){return Math.floor(Math.random()*(t-e)+e)},t.generateChars=function(e){return Array.from({length:e},(function(){return(0,t.getRandomNumber)(0,36).toString(36)})).join("")},t.generateId=function(e,i){var n=e.id||e.name&&"".concat(e.name,"-").concat((0,t.generateChars)(2))||(0,t.generateChars)(4);return n=n.replace(/(:|\.|\[|\]|,)/g,""),"".concat(i,"-").concat(n)},t.getType=function(e){return Object.prototype.toString.call(e).slice(8,-1)},t.isType=function(e,i){return null!=i&&(0,t.getType)(i)===e},t.wrap=function(e,t){return void 0===t&&(t=document.createElement("div")),e.parentNode&&(e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t)),t.appendChild(e)},t.getAdjacentEl=function(e,t,i){void 0===i&&(i=1);for(var n="".concat(i>0?"next":"previous","ElementSibling"),r=e[n];r;){if(r.matches(t))return r;r=r[n]}return r},t.isScrolledIntoView=function(e,t,i){return void 0===i&&(i=1),!!e&&(i>0?t.scrollTop+t.offsetHeight>=e.offsetTop+e.offsetHeight:e.offsetTop>=t.scrollTop)},t.sanitise=function(e){return"string"!=typeof e?e:e.replace(/&/g,"&").replace(/>/g,">").replace(/-1?e.map((function(e){var t=e;return t.id===parseInt("".concat(o.choiceId),10)&&(t.selected=!0),t})):e;case"REMOVE_ITEM":var a=n;return a.choiceId&&a.choiceId>-1?e.map((function(e){var t=e;return t.id===parseInt("".concat(a.choiceId),10)&&(t.selected=!1),t})):e;case"FILTER_CHOICES":var c=n;return e.map((function(e){var t=e;return t.active=c.results.some((function(e){var i=e.item,n=e.score;return i.id===t.id&&(t.score=n,!0)})),t}));case"ACTIVATE_CHOICES":var l=n;return e.map((function(e){var t=e;return t.active=l.active,t}));case"CLEAR_CHOICES":return t.defaultState;default:return e}}},871:function(e,t){var i=this&&this.__spreadArray||function(e,t,i){if(i||2===arguments.length)for(var n,r=0,s=t.length;r0?"treeitem":"option"),Object.assign(E.dataset,{choice:"",id:d,value:p,selectText:i}),g?(E.classList.add(h),E.dataset.choiceDisabled="",E.setAttribute("aria-disabled","true")):(E.classList.add(c),E.dataset.choiceSelectable=""),E},input:function(e,t){var i=e.classNames,n=i.input,r=i.inputCloned,s=Object.assign(document.createElement("input"),{type:"search",name:"search_terms",className:"".concat(n," ").concat(r),autocomplete:"off",autocapitalize:"off",spellcheck:!1});return s.setAttribute("role","textbox"),s.setAttribute("aria-autocomplete","list"),s.setAttribute("aria-label",t),s},dropdown:function(e){var t=e.classNames,i=t.list,n=t.listDropdown,r=document.createElement("div");return r.classList.add(i,n),r.setAttribute("aria-expanded","false"),r},notice:function(e,t,i){var n,r=e.allowHTML,s=e.classNames,o=s.item,a=s.itemChoice,c=s.noResults,l=s.noChoices;void 0===i&&(i="");var h=[o,a];return"no-choices"===i?h.push(l):"no-results"===i&&h.push(c),Object.assign(document.createElement("div"),((n={})[r?"innerHTML":"innerText"]=t,n.className=h.join(" "),n))},option:function(e){var t=e.label,i=e.value,n=e.customProperties,r=e.active,s=e.disabled,o=new Option(t,i,!1,r);return n&&(o.dataset.customProperties="".concat(n)),o.disabled=!!s,o}};t.default=i},996:function(e){var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===i}(e)}(e)},i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function n(e,t){return!1!==t.clone&&t.isMergeableObject(e)?a((i=e,Array.isArray(i)?[]:{}),e,t):e;var i}function r(e,t,i){return e.concat(t).map((function(e){return n(e,i)}))}function s(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return e.propertyIsEnumerable(t)})):[]}(e))}function o(e,t){try{return t in e}catch(e){return!1}}function a(e,i,c){(c=c||{}).arrayMerge=c.arrayMerge||r,c.isMergeableObject=c.isMergeableObject||t,c.cloneUnlessOtherwiseSpecified=n;var l=Array.isArray(i);return l===Array.isArray(e)?l?c.arrayMerge(e,i,c):function(e,t,i){var r={};return i.isMergeableObject(e)&&s(e).forEach((function(t){r[t]=n(e[t],i)})),s(t).forEach((function(s){(function(e,t){return o(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,s)||(o(e,s)&&i.isMergeableObject(t[s])?r[s]=function(e,t){if(!t.customMerge)return a;var i=t.customMerge(e);return"function"==typeof i?i:a}(s,i)(e[s],t[s],i):r[s]=n(t[s],i))})),r}(e,i,c):n(i,c)}a.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,i){return a(e,i,t)}),{})};var c=a;e.exports=c},221:function(e,t,i){function n(e){return Array.isArray?Array.isArray(e):"[object Array]"===l(e)}function r(e){return"string"==typeof e}function s(e){return"number"==typeof e}function o(e){return"object"==typeof e}function a(e){return null!=e}function c(e){return!e.trim().length}function l(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}i.r(t),i.d(t,{default:function(){return R}});const h=Object.prototype.hasOwnProperty;class u{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach((e=>{let i=d(e);t+=i.weight,this._keys.push(i),this._keyMap[i.id]=i,t+=i.weight})),this._keys.forEach((e=>{e.weight/=t}))}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function d(e){let t=null,i=null,s=null,o=1,a=null;if(r(e)||n(e))s=e,t=p(e),i=f(e);else{if(!h.call(e,"name"))throw new Error("Missing name property in key");const n=e.name;if(s=n,h.call(e,"weight")&&(o=e.weight,o<=0))throw new Error((e=>`Property 'weight' in key '${e}' must be a positive integer`)(n));t=p(n),i=f(n),a=e.getFn}return{path:t,id:i,weight:o,src:s,getFn:a}}function p(e){return n(e)?e:e.split(".")}function f(e){return n(e)?e.join("."):e}var m={isCaseSensitive:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:(e,t)=>e.score===t.score?e.idx{if(a(e))if(t[u]){const d=e[t[u]];if(!a(d))return;if(u===t.length-1&&(r(d)||s(d)||function(e){return!0===e||!1===e||function(e){return o(e)&&null!==e}(e)&&"[object Boolean]"==l(e)}(d)))i.push(function(e){return null==e?"":function(e){if("string"==typeof e)return e;let t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}(d));else if(n(d)){c=!0;for(let e=0,i=d.length;e{this._keysMap[e.id]=t}))}create(){!this.isCreated&&this.docs.length&&(this.isCreated=!0,r(this.docs[0])?this.docs.forEach(((e,t)=>{this._addString(e,t)})):this.docs.forEach(((e,t)=>{this._addObject(e,t)})),this.norm.clear())}add(e){const t=this.size();r(e)?this._addString(e,t):this._addObject(e,t)}removeAt(e){this.records.splice(e,1);for(let t=e,i=this.size();t{let o=t.getFn?t.getFn(e):this.getFn(e,t.path);if(a(o))if(n(o)){let e=[];const t=[{nestedArrIndex:-1,value:o}];for(;t.length;){const{nestedArrIndex:i,value:s}=t.pop();if(a(s))if(r(s)&&!c(s)){let t={v:s,i:i,n:this.norm.get(s)};e.push(t)}else n(s)&&s.forEach(((e,i)=>{t.push({nestedArrIndex:i,value:e})}))}i.$[s]=e}else if(r(o)&&!c(o)){let e={v:o,n:this.norm.get(o)};i.$[s]=e}})),this.records.push(i)}toJSON(){return{keys:this.keys,records:this.records}}}function _(e,t,{getFn:i=m.getFn,fieldNormWeight:n=m.fieldNormWeight}={}){const r=new g({getFn:i,fieldNormWeight:n});return r.setKeys(e.map(d)),r.setSources(t),r.create(),r}function y(e,{errors:t=0,currentLocation:i=0,expectedLocation:n=0,distance:r=m.distance,ignoreLocation:s=m.ignoreLocation}={}){const o=t/e.length;if(s)return o;const a=Math.abs(n-i);return r?o+a/r:a?1:o}const E=32;function b(e){let t={};for(let i=0,n=e.length;i{this.chunks.push({pattern:e,alphabet:b(e),startIndex:t})},h=this.pattern.length;if(h>E){let e=0;const t=h%E,i=h-t;for(;e{const{isMatch:f,score:v,indices:g}=function(e,t,i,{location:n=m.location,distance:r=m.distance,threshold:s=m.threshold,findAllMatches:o=m.findAllMatches,minMatchCharLength:a=m.minMatchCharLength,includeMatches:c=m.includeMatches,ignoreLocation:l=m.ignoreLocation}={}){if(t.length>E)throw new Error("Pattern length exceeds max of 32.");const h=t.length,u=e.length,d=Math.max(0,Math.min(n,u));let p=s,f=d;const v=a>1||c,g=v?Array(u):[];let _;for(;(_=e.indexOf(t,f))>-1;){let e=y(t,{currentLocation:_,expectedLocation:d,distance:r,ignoreLocation:l});if(p=Math.min(e,p),f=_+h,v){let e=0;for(;e=c;s-=1){let o=s-1,a=i[e.charAt(o)];if(v&&(g[o]=+!!a),_[s]=(_[s+1]<<1|1)&a,n&&(_[s]|=(b[s+1]|b[s])<<1|1|b[s+1]),_[s]&I&&(S=y(t,{errors:n,currentLocation:o,expectedLocation:d,distance:r,ignoreLocation:l}),S<=p)){if(p=S,f=o,f<=d)break;c=Math.max(1,2*d-f)}}if(y(t,{errors:n+1,currentLocation:d,expectedLocation:d,distance:r,ignoreLocation:l})>p)break;b=_}const C={isMatch:f>=0,score:Math.max(.001,S)};if(v){const e=function(e=[],t=m.minMatchCharLength){let i=[],n=-1,r=-1,s=0;for(let o=e.length;s=t&&i.push([n,r]),n=-1)}return e[s-1]&&s-n>=t&&i.push([n,s-1]),i}(g,a);e.length?c&&(C.indices=e):C.isMatch=!1}return C}(e,t,d,{location:n+p,distance:r,threshold:s,findAllMatches:o,minMatchCharLength:a,includeMatches:i,ignoreLocation:c});f&&(u=!0),h+=v,f&&g&&(l=[...l,...g])}));let d={isMatch:u,score:u?h/this.chunks.length:1};return u&&i&&(d.indices=l),d}}class O{constructor(e){this.pattern=e}static isMultiMatch(e){return I(e,this.multiRegex)}static isSingleMatch(e){return I(e,this.singleRegex)}search(){}}function I(e,t){const i=e.match(t);return i?i[1]:null}class C extends O{constructor(e,{location:t=m.location,threshold:i=m.threshold,distance:n=m.distance,includeMatches:r=m.includeMatches,findAllMatches:s=m.findAllMatches,minMatchCharLength:o=m.minMatchCharLength,isCaseSensitive:a=m.isCaseSensitive,ignoreLocation:c=m.ignoreLocation}={}){super(e),this._bitapSearch=new S(e,{location:t,threshold:i,distance:n,includeMatches:r,findAllMatches:s,minMatchCharLength:o,isCaseSensitive:a,ignoreLocation:c})}static get type(){return"fuzzy"}static get multiRegex(){return/^"(.*)"$/}static get singleRegex(){return/^(.*)$/}search(e){return this._bitapSearch.searchIn(e)}}class T extends O{constructor(e){super(e)}static get type(){return"include"}static get multiRegex(){return/^'"(.*)"$/}static get singleRegex(){return/^'(.*)$/}search(e){let t,i=0;const n=[],r=this.pattern.length;for(;(t=e.indexOf(this.pattern,i))>-1;)i=t+r,n.push([t,i-1]);const s=!!n.length;return{isMatch:s,score:s?0:1,indices:n}}}const L=[class extends O{constructor(e){super(e)}static get type(){return"exact"}static get multiRegex(){return/^="(.*)"$/}static get singleRegex(){return/^=(.*)$/}search(e){const t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},T,class extends O{constructor(e){super(e)}static get type(){return"prefix-exact"}static get multiRegex(){return/^\^"(.*)"$/}static get singleRegex(){return/^\^(.*)$/}search(e){const t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},class extends O{constructor(e){super(e)}static get type(){return"inverse-prefix-exact"}static get multiRegex(){return/^!\^"(.*)"$/}static get singleRegex(){return/^!\^(.*)$/}search(e){const t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},class extends O{constructor(e){super(e)}static get type(){return"inverse-suffix-exact"}static get multiRegex(){return/^!"(.*)"\$$/}static get singleRegex(){return/^!(.*)\$$/}search(e){const t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},class extends O{constructor(e){super(e)}static get type(){return"suffix-exact"}static get multiRegex(){return/^"(.*)"\$$/}static get singleRegex(){return/^(.*)\$$/}search(e){const t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}},class extends O{constructor(e){super(e)}static get type(){return"inverse-exact"}static get multiRegex(){return/^!"(.*)"$/}static get singleRegex(){return/^!(.*)$/}search(e){const t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},C],w=L.length,A=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/,M=new Set([C.type,T.type]);const P=[];function x(e,t){for(let i=0,n=P.length;i!(!e.$and&&!e.$or),j=e=>({[N]:Object.keys(e).map((t=>({[t]:e[t]})))});function F(e,t,{auto:i=!0}={}){const s=e=>{let a=Object.keys(e);const c=(e=>!!e.$path)(e);if(!c&&a.length>1&&!D(e))return s(j(e));if((e=>!n(e)&&o(e)&&!D(e))(e)){const n=c?e.$path:a[0],s=c?e.$val:e[n];if(!r(s))throw new Error((e=>`Invalid value for key ${e}`)(n));const o={keyId:f(n),pattern:s};return i&&(o.searcher=x(s,t)),o}let l={children:[],operator:a[0]};return a.forEach((t=>{const i=e[t];n(i)&&i.forEach((e=>{l.children.push(s(e))}))})),l};return D(e)||(e=j(e)),s(e)}function k(e,t){const i=e.matches;t.matches=[],a(i)&&i.forEach((e=>{if(!a(e.indices)||!e.indices.length)return;const{indices:i,value:n}=e;let r={indices:i,value:n};e.key&&(r.key=e.key.src),e.idx>-1&&(r.refIndex=e.idx),t.matches.push(r)}))}function K(e,t){t.score=e.score}class R{constructor(e,t={},i){this.options={...m,...t},this.options.useExtendedSearch,this._keyStore=new u(this.options.keys),this.setCollection(e,i)}setCollection(e,t){if(this._docs=e,t&&!(t instanceof g))throw new Error("Incorrect 'index' type");this._myIndex=t||_(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}add(e){a(e)&&(this._docs.push(e),this._myIndex.add(e))}remove(e=(()=>!1)){const t=[];for(let i=0,n=this._docs.length;i{let i=1;e.matches.forEach((({key:e,norm:n,score:r})=>{const s=e?e.weight:null;i*=Math.pow(0===r&&s?Number.EPSILON:r,(s||1)*(t?1:n))})),e.score=i}))}(l,{ignoreFieldNorm:c}),o&&l.sort(a),s(t)&&t>-1&&(l=l.slice(0,t)),function(e,t,{includeMatches:i=m.includeMatches,includeScore:n=m.includeScore}={}){const r=[];return i&&r.push(k),n&&r.push(K),e.map((e=>{const{idx:i}=e,n={item:t[i],refIndex:i};return r.length&&r.forEach((t=>{t(e,n)})),n}))}(l,this._docs,{includeMatches:i,includeScore:n})}_searchStringList(e){const t=x(e,this.options),{records:i}=this._myIndex,n=[];return i.forEach((({v:e,i:i,n:r})=>{if(!a(e))return;const{isMatch:s,score:o,indices:c}=t.searchIn(e);s&&n.push({item:e,idx:i,matches:[{score:o,value:e,norm:r,indices:c}]})})),n}_searchLogical(e){const t=F(e,this.options),i=(e,t,n)=>{if(!e.children){const{keyId:i,searcher:r}=e,s=this._findMatches({key:this._keyStore.get(i),value:this._myIndex.getValueForItemAtKeyId(t,i),searcher:r});return s&&s.length?[{idx:n,item:t,matches:s}]:[]}const r=[];for(let s=0,o=e.children.length;s{if(a(e)){let o=i(t,e,n);o.length&&(r[n]||(r[n]={idx:n,item:e,matches:[]},s.push(r[n])),o.forEach((({matches:e})=>{r[n].matches.push(...e)})))}})),s}_searchObjectList(e){const t=x(e,this.options),{keys:i,records:n}=this._myIndex,r=[];return n.forEach((({$:e,i:n})=>{if(!a(e))return;let s=[];i.forEach(((i,n)=>{s.push(...this._findMatches({key:i,value:e[n],searcher:t}))})),s.length&&r.push({idx:n,item:e,matches:s})})),r}_findMatches({key:e,value:t,searcher:i}){if(!a(t))return[];let r=[];if(n(t))t.forEach((({v:t,i:n,n:s})=>{if(!a(t))return;const{isMatch:o,score:c,indices:l}=i.searchIn(t);o&&r.push({score:c,key:e,value:t,idx:n,norm:s,indices:l})}));else{const{v:n,n:s}=t,{isMatch:o,score:a,indices:c}=i.searchIn(n);o&&r.push({score:a,key:e,value:n,norm:s,indices:c})}return r}}R.version="6.6.2",R.createIndex=_,R.parseIndex=function(e,{getFn:t=m.getFn,fieldNormWeight:i=m.fieldNormWeight}={}){const{keys:n,records:r}=e,s=new g({getFn:t,fieldNormWeight:i});return s.setKeys(n),s.setIndexRecords(r),s},R.config=m,R.parseQuery=F,function(...e){P.push(...e)}(class{constructor(e,{isCaseSensitive:t=m.isCaseSensitive,includeMatches:i=m.includeMatches,minMatchCharLength:n=m.minMatchCharLength,ignoreLocation:r=m.ignoreLocation,findAllMatches:s=m.findAllMatches,location:o=m.location,threshold:a=m.threshold,distance:c=m.distance}={}){this.query=null,this.options={isCaseSensitive:t,includeMatches:i,minMatchCharLength:n,findAllMatches:s,ignoreLocation:r,location:o,threshold:a,distance:c},this.pattern=t?e:e.toLowerCase(),this.query=function(e,t={}){return e.split("|").map((e=>{let i=e.trim().split(A).filter((e=>e&&!!e.trim())),n=[];for(let e=0,r=i.length;e\\_\s]* will always evaluate to true.#' diff --git a/readme.txt b/readme.txt index 3cc1604..86e6806 100644 --- a/readme.txt +++ b/readme.txt @@ -4,15 +4,15 @@ Tags: seo, tags, taxonomy, sitemap, archives Requires at least: 6.2 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 1.5.1 +Stable tag: 2.0 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.html -Hide low-value WordPress tag archives until a tag has enough posts to be useful for visitors and search engines. +Manage your site's tags: set minimum post counts, merge terms across taxonomies, and create redirects when deleting or merging terms. == Description == -Fewer Tags helps you clean up WordPress tag archives by setting a minimum number of posts a tag needs before it becomes live on your site. +Fewer Tags helps you clean up WordPress tag archives by setting a minimum number of posts a tag needs before it becomes live on your site. As of version 2.0, it also lets you merge terms across taxonomies and create redirects when you merge or delete terms — all the functionality previously in Fewer Tags Pro is now included for free. Many WordPress sites collect lots of tags with only one or two posts. That creates thin archive pages, extra URLs for search engines to crawl, and taxonomy pages that often add little value for visitors. Fewer Tags solves that by hiding low-volume tag archives until they have enough posts to be useful. @@ -30,6 +30,17 @@ When a tag has fewer than the configured number of posts: This helps reduce crawl waste while keeping stronger tag archives available. +== Merge terms == + +* Merge any tag, category, or custom taxonomy term into another — even across taxonomies. +* All posts from the source term are moved to the target term. +* A 301 redirect is automatically created from the old term archive to the new one. + +== Redirect on delete == + +* When you delete a term, Fewer Tags prompts you to create a redirect to the homepage or any other URL. +* Redirects are created via the [Redirection plugin](https://wordpress.org/plugins/redirection/) or Yoast SEO Premium. + == Why site owners use Fewer Tags == * Reduce thin, low-value tag archive pages. @@ -42,7 +53,7 @@ See [our research on tag usage in WordPress](https://fewertags.com/research/) if Watch the free plugin walkthrough: https://www.youtube.com/watch?v=KItn1X1qMas -If you want help improving your tag strategy more broadly, take a look at [Fewer Tags Pro](https://fewertags.com/). +You can learn more about Fewer Tags on [progressplanner.com](https://progressplanner.com/plugins/fewer-tags/)! == Installation == @@ -50,7 +61,8 @@ If you want help improving your tag strategy more broadly, take a look at [Fewer 2. Activate the plugin. 3. Go to Settings → Reading. 4. Set the minimum number of posts a tag needs before it becomes live on your site. -5. Save your changes. +5. For the merge and redirect-on-delete features, install and activate the [Redirection plugin](https://wordpress.org/plugins/redirection/) or Yoast SEO Premium. +6. Save your changes. == Frequently Asked Questions == @@ -60,7 +72,7 @@ Yes. If your site already has many low-value WordPress tags, Fewer Tags will sup = Should I noindex my tag pages too? = -Usually no. Tag archives can still be useful when they collect enough related content. Fewer Tags is designed to keep useful tag archives live while hiding weak ones. +Usually no. Tag archives can still be useful when they collect enough related content. Fewer Tags is designed to keep useful tag archives live while hiding weak ones. What you could (and should) do instead is add descriptions to those tag pages. = Where do I change the minimum number of posts for a tag? = @@ -70,13 +82,13 @@ Go to Settings → Reading in your WordPress admin and adjust the minimum post c Fewer Tags excludes low-volume tags from WordPress core XML sitemaps, Yoast SEO XML sitemaps, and Slim SEO XML sitemaps. -= How can I report security bugs? = += Do I need the Redirection plugin or Yoast SEO Premium? = -You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team helps validate, triage, and handle security vulnerabilities. [Report a security vulnerability](https://patchstack.com/database/vdp/fewer-tags). +For the merge and redirect-on-delete features to create actual redirects, you need either the [Redirection plugin](https://wordpress.org/plugins/redirection/) (free) or Yoast SEO Premium installed and activated. Without either plugin, merging and deleting will still work, but no redirects will be created. -= Can you help me clean up tags beyond this plugin? = += How can I report security bugs? = -Yes. If you want more hands-on help reducing and improving tags on your site, have a look at [Fewer Tags Pro](https://fewertags.com/). +You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team helps validate, triage, and handle security vulnerabilities. [Report a security vulnerability](https://patchstack.com/database/vdp/fewer-tags). == Screenshots == @@ -86,6 +98,17 @@ Yes. If you want more hands-on help reducing and improving tags on your site, ha == Changelog == += 2.0 = + +Major release: all Fewer Tags Pro functionality is now included in the free plugin. + +New features: + +* Merge terms — merge any tag, category, or custom taxonomy term into another, including cross-taxonomy merges. +* Redirect on delete — when you delete a term, get prompted to create a redirect to the homepage or any other URL. +* Redirect creation via Redirection plugin or Yoast SEO Premium. +* Admin notice when neither Redirection nor Yoast SEO Premium is installed, with install/activate buttons. + = 1.5.1 = * Fix: Prevent incorrect `keywords` output types in Yoast SEO schema in one edge case. @@ -124,5 +147,5 @@ Yes. If you want more hands-on help reducing and improving tags on your site, ha == Upgrade Notice == -= 1.5.1 = -Fixes an edge case in Yoast SEO schema output. += 2.0 = +All Fewer Tags Pro features — term merging and redirect-on-delete — are now included in the free plugin. diff --git a/src/class-admin-ajax.php b/src/class-admin-ajax.php new file mode 100644 index 0000000..8d722f4 --- /dev/null +++ b/src/class-admin-ajax.php @@ -0,0 +1,254 @@ +options = Option::get_instance(); + + // While our code here isn't AJAX, these actions run in AJAX context. + \add_action( 'pre_delete_term', [ $this, 'pre_delete_term' ], 10, 2 ); + \add_action( 'delete_term', [ $this, 'redirect_term_on_deletion' ], 10, 4 ); + + // Our AJAX functions. + \add_action( 'wp_ajax_fewer_tags_redirect_url', [ $this, 'redirect_url_action' ] ); + \add_action( 'wp_ajax_fewer_tags_merge_terms', [ $this, 'merge_terms' ] ); + \add_action( 'wp_ajax_fewer_tags_get_just_deleted_term', [ $this, 'get_just_deleted_term' ] ); + \add_action( 'wp_ajax_fewer_tags_dismiss_notice', [ $this, 'dismiss_notice' ] ); + } + + /** + * Stores an option to show a nag to redirect a tag when it's deleted. + * + * @param int $term The term ID. + * @param int $tt_id The term-taxonomy ID. + * @param string $taxonomy The taxonomy of the deleted term. + * @param \WP_Term $deleted_term The deleted term. + * + * @return void + */ + public function redirect_term_on_deletion( $term, $tt_id, $taxonomy, $deleted_term ) { + $terms_to_redirect = $this->options->get( 'terms_to_redirect' ); + if ( ! is_array( $terms_to_redirect ) ) { + $terms_to_redirect = []; + } + // Use the permalink captured in pre_delete_term, since the term is already deleted at this point. + $permalink = $this->options->get( 'just_deleted_term_permalink' ); + $terms_to_redirect[ $taxonomy ][ $deleted_term->slug ] = [ + 'object' => $deleted_term, + 'permalink' => $permalink, + ]; + $this->options->set( 'terms_to_redirect', $terms_to_redirect ); + } + + /** + * Before deleting a term, store it in the options so we can offer to redirect it. + * + * @param int $term_id The term ID. + * @param string $taxonomy The taxonomy name. + * + * @return void + */ + public function pre_delete_term( $term_id, $taxonomy ) { + $deleted_term = \get_term( $term_id, $taxonomy ); + $this->options->set( 'just_deleted_term', $deleted_term ); + $this->options->set( 'just_deleted_term_permalink', \get_term_link( $deleted_term, $taxonomy ) ); + } + + /** + * Retrieve a just deleted term. + * + * @return void + */ + public function get_just_deleted_term() { + \check_ajax_referer( 'fewer_tags_just_deleted_term' ); + + if ( ! \current_user_can( 'manage_categories' ) ) { + \wp_send_json_error( [ 'msg' => __( 'You do not have permission to do this.', 'fewer-tags' ) ] ); + } + + $just_deleted_term = $this->options->get( 'just_deleted_term' ); + if ( ! $just_deleted_term instanceof \WP_Term ) { + \wp_send_json_error( [ 'msg' => __( 'No deleted term found.', 'fewer-tags' ) ] ); + } + + $msg = Helper::redirect_term_notice( $just_deleted_term->slug, $just_deleted_term->name, $just_deleted_term->taxonomy ); + + \wp_send_json_success( $msg ); + } + + /** + * Dismiss a notice. + * + * @return void + */ + public function dismiss_notice() { + \check_ajax_referer( 'fewer_tags_dismiss_notice' ); + + if ( ! \current_user_can( 'manage_categories' ) ) { + \wp_send_json_error( [ 'msg' => __( 'You do not have permission to do this.', 'fewer-tags' ) ] ); + } + + if ( ! isset( $_POST['id'] ) || ! isset( $_POST['taxonomy'] ) ) { + \wp_send_json_error( [ 'msg' => __( 'Invalid data.', 'fewer-tags' ) ] ); + } + + $slug = \str_replace( 'fewer-tags-dismiss-', '', trim( \wp_strip_all_tags( \wp_unslash( $_POST['id'] ) ) ) ); + $taxonomy = trim( \wp_strip_all_tags( \wp_unslash( $_POST['taxonomy'] ) ) ); + + $this->remove_term_from_terms_to_redirect( $slug, $taxonomy ); + + \wp_send_json_success( $slug ); + } + + /** + * Create a redirect. + * + * @return void + */ + public function redirect_url_action() { + \check_ajax_referer( 'fewer_tags_redirect_url' ); + + if ( ! \current_user_can( 'manage_categories' ) ) { + \wp_send_json_error( [ 'msg' => __( 'You do not have permission to do this.', 'fewer-tags' ) ] ); + } + + if ( ! isset( $_POST['slug'] ) || ! isset( $_POST['target'] ) || ! isset( $_POST['taxonomy'] ) ) { + \wp_send_json_error( + [ + 'msg' => __( 'Invalid data.', 'fewer-tags' ), + ] + ); + } + $slug = trim( \wp_strip_all_tags( \wp_unslash( $_POST['slug'] ) ) ); + $taxonomy = trim( \wp_strip_all_tags( \wp_unslash( $_POST['taxonomy'] ) ) ); + $target = trim( \wp_strip_all_tags( \wp_unslash( $_POST['target'] ) ) ); + + $terms_to_redirect = $this->options->get( 'terms_to_redirect' ); + $term = $terms_to_redirect[ $taxonomy ][ $slug ]['object']; + $term_url = $terms_to_redirect[ $taxonomy ][ $slug ]['permalink']; + + $redirects = new Redirects(); + $redirects->create_redirect_from_slug( $slug, $taxonomy, $target ); + + \wp_send_json_success( + [ + 'slug' => $term->slug, + // translators: %1$s is the just redirected term name. + 'msg' => sprintf( __( 'Redirect for %1$s created!', 'fewer-tags' ), '' . \esc_html( $term->name ) . '' ), + ] + ); + } + + /** + * Merges two tags, source and target, into target, and redirects the source tag to the target tag. + * + * @return void + */ + public function merge_terms() { + \check_ajax_referer( 'fewer_tags_merge_terms' ); + + if ( ! \current_user_can( 'manage_categories' ) ) { + \wp_send_json_error( [ 'msg' => __( 'You do not have permission to do this.', 'fewer-tags' ) ] ); + } + + if ( ! isset( $_POST['source_id'] ) || ! isset( $_POST['target_id'] ) || ! isset( $_POST['source_taxonomy'] ) || ! isset( $_POST['target_taxonomy'] ) ) { + \wp_send_json_error( [ 'msg' => __( 'Invalid data.', 'fewer-tags' ) ] ); + } + $source_term_id = intval( $_POST['source_id'] ); + $target_term_id = intval( $_POST['target_id'] ); + $source_taxonomy = \wp_strip_all_tags( \wp_unslash( $_POST['source_taxonomy'] ) ); + $target_taxonomy = \wp_strip_all_tags( \wp_unslash( $_POST['target_taxonomy'] ) ); + + $source_term = \get_term( $source_term_id, $source_taxonomy ); + $source_url = $this->make_relative_url( \get_term_link( $source_term_id, $source_taxonomy ) ); + $target_url = $this->make_relative_url( \get_term_link( $target_term_id, $target_taxonomy ) ); + + // The default term of a taxonomy cannot be deleted, so we remove it from each post manually after merging. + $default_taxonomy_term_id = (int) \get_option( 'default_' . $source_taxonomy, 0 ); + + // Get all posts with the source tag and add the target tag to them. + $posts = \get_objects_in_term( $source_term_id, $source_taxonomy ); + foreach ( $posts as $post_id ) { + \wp_set_post_terms( (int) $post_id, [ $target_term_id ], $target_taxonomy, true ); + if ( $default_taxonomy_term_id && $source_term_id === $default_taxonomy_term_id ) { + \wp_remove_object_terms( (int) $post_id, $default_taxonomy_term_id, $source_taxonomy ); + } + } + // Remove our term deletion functionality, as now we have merged the tags and thus know where to redirect. + \remove_action( 'delete_term', [ $this, 'redirect_term_on_deletion' ], 10 ); + \remove_action( 'pre_delete_term', [ $this, 'pre_delete_term' ], 10 ); + + \wp_delete_term( $source_term_id, $source_taxonomy ); + + $redirects = new Redirects(); + $redirects->create_redirect( $source_url, $target_url ); + + // We're grabbing this after we've moved the new posts into the term, so we get the correct count. + $target_term = \get_term( $target_term_id, $target_taxonomy ); + + // translators: %1$s is the source tag name, %2$s is the target tag name. + $msg = sprintf( \esc_html__( '%1$s merged into %2$s!', 'fewer-tags' ), '' . \esc_html( $source_term->name ) . '', '' . \esc_html( $target_term->name ) . '' ); + if ( $source_taxonomy !== $target_taxonomy ) { + // translators: %1$s is the source tag name, %2$s is the target tag name, %3$s is the source taxonomy, %4$s is the target taxonomy. + $msg = sprintf( \esc_html__( '%1$s (%3$s) merged into %2$s (%4$s)!', 'fewer-tags' ), '' . \esc_html( $source_term->name ) . '', '' . \esc_html( $target_term->name ) . '', \esc_html( $source_taxonomy ), \esc_html( $target_taxonomy ) ); + } + \wp_send_json_success( + [ + 'source_id' => $source_term_id, + 'target_id' => $target_term_id, + 'target_count' => $target_term->count, + 'msg' => $msg, + ] + ); + } + + /** + * Removes a tag from the "tags to redirect" option. + * + * @param string $slug The slug of the tag to remove. + * @param string $taxonomy The taxonomy of the tag to remove. + * + * @return void + */ + private function remove_term_from_terms_to_redirect( $slug, $taxonomy ) { + $terms_to_redirect = $this->options->get( 'terms_to_redirect' ); + unset( $terms_to_redirect[ $taxonomy ][ $slug ] ); + $this->options->set( 'terms_to_redirect', $terms_to_redirect ); + } + + /** + * Take an absolute URL and make it relative. + * + * @param string $url The URL to make relative. + * + * @return string The relative URL. + */ + private function make_relative_url( $url ) { + if ( strpos( $url, 'http' ) === 0 ) { + $url_parts = \wp_parse_url( $url ); + if ( is_array( $url_parts ) && isset( $url_parts['path'] ) ) { + return $url_parts['path']; + } + } + return $url; + } +} diff --git a/src/class-admin.php b/src/class-admin.php index f249fab..42d65ff 100644 --- a/src/class-admin.php +++ b/src/class-admin.php @@ -7,8 +7,6 @@ namespace FewerTags; -use FewerTags\Plugin; - /** * FewerTags Admin Class */ @@ -21,6 +19,7 @@ class Admin { */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'register_settings' ] ); + \add_action( 'admin_notices', [ $this, 'redirect_tool_notice' ] ); \add_filter( 'manage_edit-post_tag_columns', [ $this, 'add_tag_columns' ] ); \add_filter( 'manage_post_tag_custom_column', [ $this, 'manage_tag_columns' ], 10, 3 ); \add_filter( 'post_tag_row_actions', [ $this, 'remove_view_action' ], 10, 2 ); @@ -40,14 +39,21 @@ public function register_settings() { ); \add_settings_field( - Plugin::$option_name, + 'fewer_tags_min_posts_count', __( 'Tags need to have', 'fewer-tags' ), [ $this, 'display_setting' ], 'reading', 'fewer_tags_section' ); - \register_setting( 'reading', Plugin::$option_name ); + \register_setting( + 'reading', + 'fewer_tags', + [ + 'type' => 'array', + 'sanitize_callback' => [ $this, 'sanitize_setting' ], + ] + ); } /** @@ -67,8 +73,8 @@ public function display_section() { public function display_setting() { ?> base !== 'edit-tags' ) { + return; + } + + if ( Helper::determine_redirect_tool() !== false ) { + return; + } + ?> +
+

+ + Redirection' + ); + ?> +

+

+ 'activate', + 'plugin' => 'redirection/redirection.php', + ], + \admin_url( 'plugins.php' ) + ), + 'activate-plugin_redirection/redirection.php' + ) : \wp_nonce_url( + \add_query_arg( + [ + 'action' => 'install-plugin', + 'plugin' => 'redirection', + ], + \admin_url( 'update.php' ) + ), + 'install-plugin_redirection' + ); + ?> + + + +

+
+ '; + $notice .= '

'; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in function. + $notice .= self::get_redirect_message( $slug, $taxonomy ); + $notice .= '

'; + $notice .= ''; + $notice .= ''; + + return $notice; + } + + /** + * Determines which redirect tool to use. + * + * @return string|false The redirect tool to use. + */ + public static function determine_redirect_tool() { + if ( \defined( 'REDIRECTION_DB_VERSION' ) ) { + return 'redirection'; + } + if ( \defined( 'WPSEO_PREMIUM_VERSION' ) ) { + return 'yoast'; + } + return false; + } + + /** + * Gets the message to redirect a tag when it's deleted. + * + * @param string $slug The slug of the tag to redirect. + * @param string $taxonomy The taxonomy of the term to redirect. + * + * @return string The message to redirect a tag when it's deleted. + */ + public static function get_redirect_message( $slug, $taxonomy ) { + $options = Option::get_instance(); + + $terms_to_redirect = $options->get( 'terms_to_redirect' ); + + if ( ! isset( $terms_to_redirect[ $taxonomy ][ $slug ]['object'] ) || ! $terms_to_redirect[ $taxonomy ][ $slug ]['object'] instanceof \WP_Term ) { + return ''; + } + + $term = $terms_to_redirect[ $taxonomy ][ $slug ]['object']; + $term_url = isset( $terms_to_redirect[ $taxonomy ][ $slug ]['permalink'] ) ? $terms_to_redirect[ $taxonomy ][ $slug ]['permalink'] : ''; + + $labels = \get_taxonomy_labels( \get_taxonomy( $taxonomy ) ); + + $msg = '' . \esc_html__( 'Fewer Tags notice', 'fewer-tags' ) . '
'; + // translators: %1$s is the tag name, %2$s is the tag slug. + $msg .= sprintf( \esc_html__( 'You\'ve deleted the %2$s "%1$s", let\'s redirect it?', 'fewer-tags' ), '' . \esc_html( $term->name ) . '', strtolower( $labels->singular_name ) ); + + $tool = self::determine_redirect_tool(); + if ( $tool === 'redirection' ) { + $msg .= ' ' . \esc_html__( 'We can use your Redirection plugin to do that.', 'fewer-tags' ); + } + if ( $tool === 'yoast' ) { + $msg .= ' ' . \esc_html__( 'We can use your Yoast SEO Premium plugin to do that.', 'fewer-tags' ); + } + + $msg .= ''; + + return $msg; + } +} diff --git a/src/class-option.php b/src/class-option.php new file mode 100644 index 0000000..05eaf7a --- /dev/null +++ b/src/class-option.php @@ -0,0 +1,72 @@ +options = \get_option( $this->option_name, [] ); + } + + /** + * Option getter. + * + * @param string $key The option key to get. + * + * @return mixed|null + */ + public function get( $key ) { + if ( isset( $this->options[ $key ] ) ) { + return $this->options[ $key ]; + } + return null; + } + + /** + * Option setter. + * + * @param string $key The option key to set. + * @param mixed $value The value to set. + * + * @return void + */ + public function set( $key, $value ): void { + $this->options[ $key ] = $value; + \update_option( $this->option_name, $this->options ); + } +} diff --git a/src/class-plugin.php b/src/class-plugin.php index cf2146e..611a155 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -13,18 +13,18 @@ class Plugin { /** - * The option name. + * Default value for the minimum number of posts a tag should have to not be redirected to the homepage. * - * @var string + * @var int */ - public static $option_name = 'fewer_tags'; + public static $min_posts_count; /** - * Default value for the minimum number of posts a tag should have to not be redirected to the homepage. + * The option class. * - * @var int + * @var Option */ - public static $min_posts_count; + public $options; /** * Register plugin hooks. @@ -42,12 +42,37 @@ public function register_hooks() { * @return void */ public function init() { - self::$min_posts_count = (int) get_option( static::$option_name, 10 ); + self::$min_posts_count = (int) ( Option::get_instance()->get( 'min_posts_count' ) ?? 10 ); if ( is_admin() ) { + if ( \wp_doing_ajax() ) { + new Admin_Ajax(); + return; + } + $admin = new Admin(); $admin->register_hooks(); + $this->options = Option::get_instance(); + + \add_action( 'admin_notices', [ $this, 'do_notices' ] ); + \add_action( 'admin_footer', [ $this, 'output_modal' ] ); + \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + \add_filter( 'tag_row_actions', [ $this, 'add_merge_action' ], 10, 2 ); + \add_filter( 'category_row_actions', [ $this, 'add_merge_action' ], 10, 2 ); + + // Add merge action for custom taxonomies. + $taxonomies = \get_taxonomies( + [ + 'public' => true, + '_builtin' => false, + ], + 'names' + ); + foreach ( $taxonomies as $taxonomy ) { + \add_filter( "{$taxonomy}_row_actions", [ $this, 'add_merge_action' ], 10, 2 ); + } + // Detect if we're running on the playground, if so, load our playground specific class. if ( defined( 'IS_PLAYGROUND_PREVIEW' ) && IS_PLAYGROUND_PREVIEW ) { $playground = new Playground(); @@ -67,10 +92,239 @@ public function init() { * @return void */ public function migrate_option() { - $old_option = get_option( 'joost_min_posts_count' ); - if ( $old_option ) { - update_option( static::$option_name, $old_option ); + // 1. Legacy: joost_min_posts_count → min_posts_count key. + $legacy = get_option( 'joost_min_posts_count' ); + if ( false !== $legacy ) { + $current = get_option( 'fewer_tags', [] ); + if ( ! is_array( $current ) ) { + $current = [ 'min_posts_count' => (int) $current ]; + } + if ( ! isset( $current['min_posts_count'] ) ) { + $current['min_posts_count'] = (int) $legacy; + } + update_option( 'fewer_tags', $current ); delete_option( 'joost_min_posts_count' ); } + + // 2. Old free version: fewer_tags is a scalar integer. + $current = get_option( 'fewer_tags', [] ); + if ( ! is_array( $current ) ) { + $current = [ 'min_posts_count' => (int) $current ]; + update_option( 'fewer_tags', $current ); + } + + // 3. Old pro data: merge fewer_tags_pro into fewer_tags. + $pro = get_option( 'fewer_tags_pro' ); + if ( false !== $pro && is_array( $pro ) ) { + $current = get_option( 'fewer_tags', [] ); + if ( ! is_array( $current ) ) { + $current = [ 'min_posts_count' => (int) $current ]; + } + // Merge pro data, existing keys in $current take precedence. + $current = array_merge( $pro, $current ); + update_option( 'fewer_tags', $current ); + delete_option( 'fewer_tags_pro' ); + } + } + + /** + * Adds a 'Merge' action to the term actions list. + * + * @param array $actions An array of actions to be performed on the term. + * @param \WP_Term $term The term object. + * + * @return array The modified actions array. + */ + public function add_merge_action( $actions, $term ) { + $taxonomy = \get_taxonomy( $term->taxonomy ); + + // Add a 'Merge' action link. + $actions['merge'] = \sprintf( + '%5$s', + $term->term_id, + $term->name, + $term->taxonomy, + // translators: %s is the name of the tag. + sprintf( __( 'Merge %1$s', 'fewer-tags' ), strtolower( $taxonomy->labels->name ) ), + __( 'Merge', 'fewer-tags' ) + ); + + return $actions; + } + + /** + * Enqueue scripts and styles. + * + * @return void + */ + public function enqueue_scripts() { + if ( ! $this->is_terms_screen() ) { + return; + } + $taxonomy = \get_current_screen()->taxonomy; + \add_thickbox(); + \wp_enqueue_style( 'fewer-tags-choices', \plugins_url( 'js/vendor/choices.min.css', FEWER_TAGS_FILE ), [], '10.2.0' ); + \wp_add_inline_style( + 'fewer-tags-choices', + ' + #merge-tags-modal { + display: none; + } + #fewer-tags-merge-form br, #fewer-tags-merge-form p { + clear: both; + } + #fewer-tags-note { + display: none; + border: 1px solid #c3c4c7; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + border-left: 4px solid #d63638; + margin: 5px 0 15px 0; + padding: 0px 8px; + background-color: #fff; + } + #fewer-tags-note p { + padding: 0; + } + #fewer-tags-merge-form label { + display: block; + float: left; + width: 150px; + margin-bottom: 35px; + } + #fewer-tags-merge-form label.choices_label { + line-height: 40px; + margin-bottom: 35px; + } + #fewer-tags-source-term-name { + float: left; + margin-bottom: 5px; + width: 375px; + } + #TB_ajaxContent { + overflow: visible !important; + } + div.choices { + display: inline-block !important; + width: 375px !important; + box-sizing: border-box !important; + margin-bottom: 10px !important; + } + .choices__inner { + background-color: inherit !important; + min-height: 40px !important; + box-sizing: border-box !important; + border-radius: 5px !important; + } + .choices__list--dropdown .choices__item--selectable, .choices__list[aria-expanded] .choices__item--selectable { + padding-right: 10px !important; + } + .choices__list--dropdown .choices__item--selectable::after, .choices__list[aria-expanded] .choices__item--selectable::after { + display: none !important; + } + .choices__item--selectable span { + display: inline-block !important; + float: right; + color: #666; + }' + ); + \wp_enqueue_script( 'fewer-tags-choices', \plugins_url( 'js/vendor/choices.min.js', FEWER_TAGS_FILE ), [], '10.2.0', true ); + \wp_enqueue_script( 'fewer-tags', \plugins_url( 'js/fewer-tags.js', FEWER_TAGS_FILE ), [ 'fewer-tags-choices', 'wp-api' ], '2.0', true ); + \wp_add_inline_script( + 'fewer-tags', + 'const fewerTags = ' . \wp_json_encode( + [ + 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), + 'deleteTermNonce' => \wp_create_nonce( 'fewer_tags_just_deleted_term' ), + 'dismissText' => __( 'Dismiss this notice.', 'fewer-tags' ), + 'restAPInonce' => \wp_create_nonce( 'wp_rest' ), + 'defaultTermId' => (int) \get_option( 'default_' . $taxonomy, 0 ), + ] + ), + 'after' + ); + } + + /** + * Outputs the modal. + * + * @return void + */ + public function output_modal() { + if ( ! $this->is_terms_screen() ) { + return; + } + $screen = \get_current_screen(); + $taxonomy = \get_taxonomy( $screen->taxonomy ); + + ?> +
+
+ +

labels->name ) ) ); ?>

+

+ + + + + + +
+ + + +
+ + + + +

labels->singular_name ) ) ); ?>

+ + +
+
+ base === 'edit-tags' ); + } + + /** + * Displays a notice to redirect a tag when it's deleted. + * + * @return void + */ + public function do_notices() { + if ( ! $this->is_terms_screen() ) { + return; + } + + $taxonomy = \get_current_screen()->taxonomy; + $terms_to_redirect = $this->options->get( 'terms_to_redirect' ); + if ( ! isset( $terms_to_redirect[ $taxonomy ] ) ) { + return; + } + $terms_to_redirect = $terms_to_redirect[ $taxonomy ]; + + if ( ! empty( $terms_to_redirect ) && count( $terms_to_redirect ) > 0 ) { + foreach ( $terms_to_redirect as $slug => $term_array ) { + if ( ! isset( $term_array['object'] ) || ! $term_array['object'] instanceof \WP_Term ) { + continue; + } + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped in function. + echo Helper::redirect_term_notice( $slug, $term_array['object']->name, $taxonomy ); + } + } } } diff --git a/src/class-redirects.php b/src/class-redirects.php new file mode 100644 index 0000000..ef3b218 --- /dev/null +++ b/src/class-redirects.php @@ -0,0 +1,151 @@ +options = Option::get_instance(); + } + + /** + * Create a redirect from a slug and a target URL. + * + * @param string $slug The slug of the term we're redirecting. + * @param string $taxonomy The taxonomy of the term we're redirecting. + * @param string $target_url The target URL. + * + * @return boolean Whether the redirect was created successfully. + */ + public function create_redirect_from_slug( $slug, $taxonomy, $target_url ) { + $terms_to_redirect = $this->options->get( 'terms_to_redirect' ); + if ( ! isset( $terms_to_redirect[ $taxonomy ][ $slug ] ) ) { + return false; + } + + $response = $this->create_redirect( $terms_to_redirect[ $taxonomy ][ $slug ]['permalink'], $target_url ); + if ( ! $response ) { + return false; + } + + unset( $terms_to_redirect[ $taxonomy ][ $slug ] ); + $this->options->set( 'terms_to_redirect', $terms_to_redirect ); + return true; + } + + /** + * Creates a Redirection 'Fewer Tags' group. + * + * @return object|false The group object or false on failure. + */ + private function create_redirection_group() { + if ( ! class_exists( '\Red_Group' ) ) { + return false; + } + + return \Red_Group::create( + 'Fewer Tags', + 1, // Means they're WordPress based redirects, the default. + ); + } + + /** + * Create a redirect. + * + * @param string $source_url Source URL. + * @param string $target_url Target URL. + * + * @return boolean Whether the redirect was created successfully. + */ + public function create_redirect( $source_url, $target_url ) { + $tool = Helper::determine_redirect_tool(); + if ( ! $tool ) { + return false; + } + + $response = false; + switch ( $tool ) { + case 'redirection': + $response = $this->create_redirection_redirect( $source_url, $target_url ); + break; + case 'yoast': + $response = $this->create_yoast_redirect( $source_url, $target_url ); + break; + } + return $response; + } + + /** + * Create a Redirection redirect. + * + * @param string $source_url The slug of the tag to redirect. + * @param string $target_url The target URL to redirect to. + * + * @return bool Whether the redirect was created successfully. + */ + public function create_redirection_redirect( $source_url, $target_url ) { + $redirection_group = $this->options->get( 'redirection_group' ); + if ( empty( $redirection_group ) || ! is_int( $redirection_group ) ) { + $response = $this->create_redirection_group(); + if ( false !== $response ) { + $redirection_group = $response->get_id(); + $this->options->set( 'redirection_group', $redirection_group ); + } + } + + $redirect = \Red_Item::create( + [ + 'url' => $source_url, + 'action_code' => 301, + 'action_data' => [ 'url' => $target_url ], + 'action_type' => 'url', + 'match_type' => 'url', + 'group_id' => $this->options->get( 'redirection_group' ), + ] + ); + if ( ! \is_wp_error( $redirect ) ) { + return true; + } + return false; + } + + /** + * Create a Yoast redirect. + * + * @param string $source_url The slug of the tag to redirect. + * @param string $target_url The target URL to redirect to. + * + * @return boolean Whether the redirect was created successfully. + */ + public function create_yoast_redirect( $source_url, $target_url ) { + $redirect_option = new \WPSEO_Redirect_Option(); + + // Add the redirect to Yoast SEO table. + $redirect_object = new \WPSEO_Redirect( $source_url, $target_url, 301, 'plain' ); + $redirect_option->add( $redirect_object ); + $redirect_option->save(); + + // Apply the redirect. + $manager = new \WPSEO_Redirect_Manager(); + $manager->export_redirects(); + + return true; + } +} diff --git a/tests/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh index 2a4c531..79b57d8 100755 --- a/tests/bin/install-wp-tests.sh +++ b/tests/bin/install-wp-tests.sh @@ -41,7 +41,7 @@ WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} download() { if [ `which curl` ]; then - curl -s "$1" > "$2"; + curl -sL "$1" > "$2"; elif [ `which wget` ]; then wget -nv -O "$2" "$1" fi @@ -130,8 +130,30 @@ install_test_suite() { # set up testing suite mkdir -p $WP_TESTS_DIR rm -rf $WP_TESTS_DIR/{includes,data} - svn export --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes - svn export --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + + # Use GitHub zip downloads instead of svn export. + local GITHUB_TAG=${WP_TESTS_TAG#tags/} + local GITHUB_TAG=${GITHUB_TAG#branches/} + local WP_TESTS_ZIP_URL="" + # Try as a tag first (most common for versioned releases), then as a branch. + for ref_type in tags heads; do + local test_url="https://github.com/WordPress/wordpress-develop/archive/refs/${ref_type}/${GITHUB_TAG}.zip" + local http_code=$(curl -sL -o /dev/null -w "%{http_code}" "$test_url") + if [ "$http_code" = "200" ]; then + WP_TESTS_ZIP_URL="$test_url" + break + fi + done + if [ -z "$WP_TESTS_ZIP_URL" ]; then + echo "Could not find WordPress test suite for version ${GITHUB_TAG}" + exit 1 + fi + download "$WP_TESTS_ZIP_URL" "$TMPDIR/wp-tests.zip" + unzip -q -o "$TMPDIR/wp-tests.zip" -d "$TMPDIR/wp-tests" + local WP_TESTS_EXTRACTED_DIR=$(find "$TMPDIR/wp-tests" -maxdepth 1 -mindepth 1 -type d | head -1) + cp -r "${WP_TESTS_EXTRACTED_DIR}/tests/phpunit/includes" "$WP_TESTS_DIR/includes" + cp -r "${WP_TESTS_EXTRACTED_DIR}/tests/phpunit/data" "$WP_TESTS_DIR/data" + rm -rf "$TMPDIR/wp-tests" "$TMPDIR/wp-tests.zip" fi if [ ! -f wp-tests-config.php ]; then diff --git a/tests/phpunit/test-class-admin.php b/tests/phpunit/test-class-admin.php index e291b5d..6efe52c 100644 --- a/tests/phpunit/test-class-admin.php +++ b/tests/phpunit/test-class-admin.php @@ -86,7 +86,7 @@ public function test_register_settings() { global $wp_settings_sections, $wp_settings_fields; $this->assertArrayHasKey( 'fewer_tags_section', $wp_settings_sections['reading'] ); - $this->assertArrayHasKey( Plugin::$option_name, $wp_settings_fields['reading']['fewer_tags_section'] ); + $this->assertArrayHasKey( 'fewer_tags_min_posts_count', $wp_settings_fields['reading']['fewer_tags_section'] ); } /** @@ -112,8 +112,8 @@ public function test_display_setting() { self::$class_instance->display_setting(); $output = ob_get_clean(); - $this->assertStringContainsString( 'name="' . Plugin::$option_name . '"', $output ); - $this->assertStringContainsString( 'id="' . Plugin::$option_name . '"', $output ); + $this->assertStringContainsString( 'name="fewer_tags[min_posts_count]"', $output ); + $this->assertStringContainsString( 'id="fewer_tags_min_posts_count"', $output ); $this->assertStringContainsString( 'type="number"', $output ); $this->assertStringContainsString( 'min="1"', $output ); $this->assertStringContainsString( 'value="5"', $output ); // This is tied to the value of Plugin::$min_posts_count. diff --git a/tests/phpunit/test-fewer-tags.php b/tests/phpunit/test-fewer-tags.php index 08524ba..9e8d469 100644 --- a/tests/phpunit/test-fewer-tags.php +++ b/tests/phpunit/test-fewer-tags.php @@ -28,7 +28,7 @@ class FewerTags_Test extends \WP_UnitTestCase { public static function set_up_before_class() { parent::set_up_before_class(); - update_option( Plugin::$option_name, 10 ); + update_option( 'fewer_tags', [ 'min_posts_count' => 10 ] ); self::$class_instance = new Plugin(); }