From 06d7647859860ce0f5ee3c7bbe8cf1e3e77d95a0 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Mon, 10 Mar 2025 12:33:38 +0100 Subject: [PATCH 01/15] Update assets on .org --- .wordpress-org/banner-1544x500.png | Bin 21426 -> 13819 bytes .wordpress-org/banner-772x250.png | Bin 29231 -> 6150 bytes .wordpress-org/icon-128x128.png | Bin 1834 -> 1091 bytes .wordpress-org/icon-256x256.png | Bin 3753 -> 2751 bytes .wordpress-org/icon.svg | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png index be6728bb11d0fcce70069e874bb61a4fb72b5144..45bee9d120e9d85213afb6eb21ac360d8f80366f 100644 GIT binary patch literal 13819 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

KX(1{@MBgg=NpBk_c?c|C{ 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 literal 21426 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 BJAeQH literal 29231 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 diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png index dc0e43eb367f8cd2888e3cad29389519e8fd3647..3114a18a240ccf90b3a3e79f1421530a9b9b0670 100644 GIT binary patch literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&T?+6CaRt&qV4;~~p_vOs8W2W~ zIatI}BO4^H1r-Fc%`~!u-P?f3QZolgezA{(BD=V+_Hm!>V{*B>d$rU`+MkFrEEaTeD#y{%%|QI|81Ye5Sh5|G5;_EBV~gr!#BT$%fmz#C$%>q%AqLCbfBI8S9N_EtM-xE-u_xmlOWt#`P zHyK=C*&2p1=e&&-zkA0>G|}$dV!^xFQ#T#l$g-Q|>*lhxyO~837T3o%uI%Ft zTQNWH08`66mMV#wM!u^X4>qlDsAv8r8jx`Lqt65V35Rz*-aKi)1D|oymST@5dJ8N} z@(PZ*{9}^RxpiOi7k9>u4F|7k1ukK-JY)6l|34AKZu`5JXHAiFKdy_!wPwj zA=@6uHtcuk(>ANf5pva7aA2~=E`_cjhl8isCM$G>I5aYAMz95LVdAng_)}lb_?wMe zf&2IDAm+NVzx7gkuib6v`LNBAS%k0p`YO{+Rn>lXn0T4K>t9!0zgUEU`}R8i!x6f% i4v7o_pd_lYE1uD2*I(8pB20ThWr3%wpUXO@geCyH1O8zE delta 1830 zcmV+>2if?;2&xW{8Gi-<006^2Vaosj2JuNmK~#9!?VSaX<2V+EwamPGyi5%Pv7KdR zhFJD8GY#`3*W+VmZW?B0Za8L!ILwEV;r!tedw!-OmT`{Rlr!Qx=eScC!+6UE+^NC6)JwarD z<$$ywQr1=0WV)%}K8k;~_^@0NUq-5&jH7V-pvp=2qz_7B_FU&U7pvJm0Rg5+E`VR* zWFA+ueF6eBNi6`y>Hl2W+XoN;KmY&%00aOK06+i$0RRN}KLG*&2ml}efB*mj00;me z0Du4h0>}meKz{%TP?LUE(e^>*fd1p9FUqXx|B#~XgNmBW+mZ{gg?9WX+CDJedFK@I zA^k$$I$i0#;{U&^s7XJee*1?4PGF96Vo_1QeMJBO z00000z!5!IyT1Oy=YRIIi;KyV4Grfg)qW8yyTv{udw<)L=Xf{0cy5pdhJ7AoQR3p5iWMZoE+Q5OQix2ECBEF39#`OU>Zg zM^tT}@H&{E5rx?#x$uYF7Ocma3fPUcO>zrBseMk>_KBQs4x_@9ID6bAf0UIM2tYk` zDn=OupzO};sfStlfB&3E5@GR@%SbiSZO^8{P-3F}rS3G@MUUfYIZ92D7KhRLEW zo^IX@0p#2Q28#F*$QCzg-^juS0t63&B7c$1Zo<3)0)#siJUT)GP&Vat+hAdVD}Xg2 zVggV$22;~T0|EHd$O%Bv_y#Y@HC8OR1z0m9C_s?)u9oV^iUALUtT_=DfU-WT^9~XP zKqpZBnCtS%R6EvouqZTVSf7o!072Gz9eFit8(0=j-<+5TKw0Byb`>iaYzzrT0)OLU zBY|eEb`4&^3I|)m@*Pqi3s(2UA+F9>Ws~;x*c{ph=+a4-&=}ngR{p1B>N-|9fI3ms znTn4D)(Tp{$_fY2;tv{D#pp_~+%ulOniUS9i4~e)iH!sXUzWRpU&RUs&=hNoB%mzK zcCfExpm1^tJ3$X!Vl+Bf{GY_rSAQ_2@%=@XbK6@LU6FIfP; zM^n>7EF^LUzbQTvn2oI5h8&14lkd@A_)Wt<_TW-pCr(%Il-GqBaJA~6zOXMo98{w6vor}OQNU;3NJpN z9xUdFJiVId_mnKao_`+A%@?b`7twm95DA1V6Y2fNEG~S|+Ki^>yfvyC{ zo-s6s(Q)-GZglR diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png index 3c92dfd50f0e75dad2d88dd054fc0b79d7d3e4fe..c76a8bf9c52f06bf133da17603794c7da926b46b 100644 GIT binary patch literal 2751 zcma)8dpOhkAOCLHm`zGlzs|HM<<<*BytiFxy{_|_Zgk@Jip&xzwh&Y-h01a@7MeKJm2rLOm{aj2DJ?Z zK@i5-$=(BkV8DbyvQpqD;?5B&1W7^eu2cu$8X9tNdXad2rnx=~hh^jRnU;ES2Xv#& z4Or&buYw1L0UM8H0+2-F0Ghcz8$@viOi2jKa^A%ZObE)|$O78lKAvKqL2=|#9L~_}&x*~0XYA;6mVqK$x`^a6Z{pf3m z%oF?;tOBQO{ijL3(*(wh6@7-pn8pXWxn)uu*VD;d3YkkKb3L3hXRZ9_h<-vM{eNVR zNT)ib1Dd3rBUku0M`TT>lFv$TvILwZ(7_lRF2#vEN1zLV5a}Kui_E3bex6-V^gfh9 za|V3+jE%2{QwD{cE+&ySIszLoJ)8hP{qGt4#8zG#_8h@aNb(mFf&ABmvjjgd;a8C0 zKSu(#gfmN|i)_}{v0JY|@E1x#-~wJ>?1RNLnq)#Ey$yo?&h?wI@fQIyXhig%TGx_g zLE^d|$J6JmeK&f6;u|=)*eoauc6Vk;Y$9E3NZTj~4g$!a%YD_@e~t!_^*czI5*h)x z{$Mo@=V2m?FA?Ut-mqCENo;;%L`hr}zy(Q6zQkqmlURa8R^eCvS0+DkJv3|Wo5(In zV)19zuRSZd7-$4ev~G~-v*0EK2?y{KSc0S|P)ZON#{*{s^+A^ewgdrlCPZIP0KFnh zPO|ubV0GzOtK`Lq^>Cv=h`r=*UCb~#MbqNNd$^z+>4?IWfclR}^)=6|nCr=vzsQL$ zwJHzOv0bq8u||}R{_agOO53*Y(9qKUMrY@)-Mac)FehJ%2hZ=rOj_7@tCjIZ>NPcqV{Lls2$wB9H_xsCMzX;Cs z%`DURf9hr7dQ>~l`@Y(`N$T-S*w~9WOZmFziZ?sOSzff)H$HdmCy3Z2U#_g~#rSh{0pqjLcBEc=7mN>84vt zw4DOtL*5r{b@1i*`ox}Gwc!nh_PJ_Bn)1A|m=8jW&)0VDc@thfDPVO58BPjv;)Z`! zogY*fK5iAZ96FMX$6QaTyPf#1G^MT~(HiK!MpRfAgxJS{p&5{rTJ!&Fl5Qg|y(ILt zHr4?BqA?~+t0Kwt=7oFb=a~fKzX*Mw3lfjYM{7lz+>LFBPt=dVrH2ku3k)ka^3ZS7v1ZG zYy^w)2M3O@>MC-oCVJvbYgliA9B0MFE>nvr%aju{DGK*bO?+t@2syj-q^0S*4y4+| z=%iEt7EKHPNefMVBU9Am;O9jl>~bRj}1=2mW^0j;Mk2h zFx|=rE19zUGf}#=Rr{I~4(^kM-Eh6074}plBO4^uy_tcyMQ^TvE-;G&b=S9d*fP4!c+AUlp}_?|qr4X655=<98>Qg;MyMddgGNx8bWvR?>ozg16)XpwTg8C>99 zI|Yru=$&eVR%N*PVi)&cp4(wGFP*!|Y0N~N?+^(%^-9k*F8ABY6}j$@^F2jy*vdW- z7Z9lAzD^{Nxk7lv#ZjGM=Q?gL9^Rc zZ-&`1W@-u7McK)$hJ@_s!Pr&z+pE86Rq3%)KN(Oq=OH6f_hCpKOJ*@syYvOJTyL{% zQ&H^#9QnRHUIEdK%q<=1`JVB)q=lTTDL?#aT1oFP9$T4j+MhvoE5Z-Ix^P!lxkvW! zPnM@U>jkQcz9Y(8f5*ye)+LNy&3ZPX*w-1b_4i2KzJ?gr=QK4c!@}3{#Kcwp@m!QE zKkY%(#F7aO_r9#z!=+BH*3AG}Cqq`5E9Rs}edfk+TK1JS$F?KuFd4d?W@AtCk|EbJ z0ed1xfrGz$>y3s0-mZG&*VZY>snC&E(;|dI&8{t7(!HhyeP~mk8Z2{y7U8#jd^ST9 zNgc+X>dH{HH>&YZ^%`l?{I(>vXGo6{^v-a!3KPMgKxxX^xp@Vu&|slT`*ze@xHjL| znqt0LBtx>_YyE4GArsAM&vnendWs}b$pUWLkd(G=ciFV!B6POt(3LNb#-Ou@4vZa& z27Ww^VR2^@C+!sa4SYcxLSRBdEy~Xr!?nY-?Jhi2A;@t`v)q64M+`_eKfgt#-B(1& zM+ou2+M34b2|<0T2zf@KuU4w6-1*?SA@!V=Q^W0xG;bB}O;O6(O6ucQfhXI>}m4e7

y8LP( z7MY+>0#6ZWzt4-|s801L#!cLj{?f{l=n3ti ziDT8!fbE$!jC?0s%0y2&<;QmTC z{?qZ1i|DnI;L~c4o7NV`jUV}*UN#i==1=KHI9jf}*4(lFjYZGY_7k~Tl7FJ$kv);~ Us0}J1*+?8`2RHi)TgDIn0gU|P-T(jq literal 3753 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 diff --git a/.wordpress-org/icon.svg b/.wordpress-org/icon.svg index 2cb995a..f096e07 100644 --- a/.wordpress-org/icon.svg +++ b/.wordpress-org/icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 28460341e3b274c724b2808bb809a009cf5d2cb6 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Mon, 10 Mar 2025 12:34:44 +0100 Subject: [PATCH 02/15] Add deploy assets workflow to main branch --- .github/workflows/deploy-assets.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/deploy-assets.yml diff --git a/.github/workflows/deploy-assets.yml b/.github/workflows/deploy-assets.yml new file mode 100644 index 0000000..3141845 --- /dev/null +++ b/.github/workflows/deploy-assets.yml @@ -0,0 +1,16 @@ +name: Plugin asset/readme update +on: + push: + branches: + - main +jobs: + trunk: + name: Push to trunk + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: WordPress.org plugin asset/readme update + uses: 10up/action-wordpress-plugin-asset-update@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} \ No newline at end of file From 0c8f05efa81bb147538e3acea91bfdcedb03f191 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 19 Mar 2026 14:32:00 +0100 Subject: [PATCH 03/15] Add filip and ari --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index e1d5f80..32e56b5 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,5 @@ === Fewer Tags Free === -Contributors: joostdevalk +Contributors: progressplanner,aristath,joostdevalk Tags: tag, tags, seo Requires at least: 6.2 Tested up to: 6.6 From c853e212ebf3338826337bbef3a5dc0662f6b7bb Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 19 Mar 2026 15:03:04 +0100 Subject: [PATCH 04/15] Merge Fewer Tags Pro functionality into free plugin (v2.0) Bring all pro features into the free plugin: term merging (including cross-taxonomy), redirect-on-delete prompts, and redirect creation via Redirection plugin or Yoast SEO Premium. Strip all EDD licensing code. Also upgrades PHPStan to v2.1 for PHP 8.4 compatibility and fixes all resulting static analysis issues. Co-Authored-By: Claude Opus 4.6 (1M context) --- .phpcs.xml.dist | 4 +- composer.json | 6 +- composer.lock | 180 +++++---------- fewer-tags.php | 7 +- js/fewer-tags.js | 460 ++++++++++++++++++++++++++++++++++++++ js/vendor/choices.min.css | 1 + js/vendor/choices.min.js | 2 + phpstan.neon.dist | 4 +- readme.txt | 57 +++-- src/class-admin-ajax.php | 228 +++++++++++++++++++ src/class-admin.php | 56 +++++ src/class-helper.php | 89 ++++++++ src/class-option.php | 72 ++++++ src/class-plugin.php | 215 ++++++++++++++++++ src/class-redirects.php | 150 +++++++++++++ uninstall.php | 3 +- 16 files changed, 1380 insertions(+), 154 deletions(-) create mode 100644 js/fewer-tags.js create mode 100644 js/vendor/choices.min.css create mode 100644 js/vendor/choices.min.js create mode 100644 src/class-admin-ajax.php create mode 100644 src/class-helper.php create mode 100644 src/class-option.php create mode 100644 src/class-redirects.php 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/composer.json b/composer.json index a9c02bf..41c8a63 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,9 @@ "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", + "phpstan/phpstan": "^2.1", + "szepeviktor/phpstan-wordpress": "^2.0", + "phpstan/extension-installer": "^1.4", "phpcompatibility/php-compatibility": "dev-develop as 9.99.99" }, "scripts": { diff --git a/composer.lock b/composer.lock index d145e72..60031a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "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": "a3e3855d91e999e0245dc271ed566fc9", "packages": [], "packages-dev": [ { @@ -705,27 +705,32 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.4.3", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "6105bdab2f26c0204fe90ecc53d5684754550e8f" + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6105bdab2f26c0204fe90ecc53d5684754550e8f", - "reference": "6105bdab2f26c0204fe90ecc53d5684754550e8f", + "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", - "php": "^7.4 || ~8.0.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.10.49", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.11" + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -746,9 +751,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.4.3" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" }, - "time": "2024-02-11T18:56:19+00:00" + "time": "2026-02-03T19:29:21+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -1120,28 +1125,29 @@ }, { "name": "phpstan/extension-installer", - "version": "1.3.1", + "version": "1.4.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/extension-installer.git", - "reference": "f45734bfb9984c6c56c4486b71230355f066a58a" + "reference": "1754f69aa40d7e42ae0fa94c11036a80dcd0c331" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f45734bfb9984c6c56c4486b71230355f066a58a", - "reference": "f45734bfb9984c6c56c4486b71230355f066a58a", + "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.9.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", "extra": { "class": "PHPStan\\ExtensionInstaller\\Plugin" @@ -1156,32 +1162,32 @@ "MIT" ], "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.3.1" + "source": "https://github.com/phpstan/extension-installer/tree/1.4.x" }, - "time": "2023-05-24T08:59:17+00:00" + "time": "2026-02-26T12:27:12+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.66", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd" - }, + "version": "2.2.x-dev", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/94779c987e4ebd620025d9e5fdd23323903950bd", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd", + "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" @@ -1216,13 +1222,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-03-28T16:17:31+00:00" + "time": "2026-03-19T12:50:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2689,113 +2691,39 @@ ], "time": "2024-03-31T21:03:09+00:00" }, - { - "name": "symfony/polyfill-php73", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/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.29.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-01-29T20:11:03+00:00" - }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.4", + "version": "2.x-dev", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "891d0767855a32c886a439efae090408cc1fa156" + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/891d0767855a32c886a439efae090408cc1fa156", - "reference": "891d0767855a32c886a439efae090408cc1fa156", + "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", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" + "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": { @@ -2823,9 +2751,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.4" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/2.x" }, - "time": "2024-03-21T16:32:59+00:00" + "time": "2025-09-14T02:58:22+00:00" }, { "name": "theseer/tokenizer", @@ -3087,7 +3015,7 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/fewer-tags.php b/fewer-tags.php index 32424cf..98f2482 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. + * Description: Manage your site's tags: set minimum post counts, merge terms, and create redirects. Change settings under Settings → Reading. Learn more at fewertags.com. * 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..a2f3c61 --- /dev/null +++ b/js/fewer-tags.js @@ -0,0 +1,460 @@ +/** + * 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 ) => { + elements.forEach( ( element ) => { + let inDom = document.body.contains( element ); + const observer = new MutationObserver( () => { + if ( ! document.body.contains( element ) && inDom ) { + inDom = false; + callback(); + } + } ); + observer.observe( document.body, { 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 = ` +

`; + }, + } ); +} + +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, + }, + } ) ); + + 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. + * + * @return {Promise} The promise of the terms. + */ +async function fewerTagsGetAllTerms( taxonomy ) { + const retrievedTerms = []; + const perPage = 100; // Max allowed by WP REST API + let page = 1; + let hasMore = true; + const termsOutput = []; + + let endpoint = ''; + 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: true, + 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: true, + 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; + fewerTagsGetAllTerms( targetTaxonomy ).then( ( terms ) => { + FTterms = terms; + FTchoices.setChoices( FTterms, 'value', 'label', true ); + document.getElementById( 'fewer-tags-target-term-label' ).innerHTML = '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 ( termID === '1' && element.getAttribute( 'data-term-taxonomy' ) === 'category' ) { + 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 32e56b5..a73423d 100644 --- a/readme.txt +++ b/readme.txt @@ -1,37 +1,46 @@ -=== Fewer Tags Free === +=== Fewer Tags === Contributors: progressplanner,aristath,joostdevalk Tags: tag, tags, seo Requires at least: 6.2 Tested up to: 6.6 Requires PHP: 7.4 -Stable tag: 1.5.1 +Stable tag: 2.0 License: GPL3+ License URI: https://www.gnu.org/licenses/gpl-3.0.en.html -This plugin minimizes the effect of having too many tags by setting a minimum number of posts needed for a tag to be “live” on your site. +Manage your site's tags: set minimum post counts, merge terms across taxonomies, and create redirects when deleting or merging terms. == Description == -One of the most common SEO problems on WordPress sites is that people add too many tags to posts. In fact, [our research shows](https://fewertags.com/research/) that _most_ WordPress sites use tags wrong. The WordPress interface makes it incredibly easy to do so, and with every tag you add, you add another URL to your site for search engines to crawl and index. This plugin minimizes that effect by setting a minimum number of posts needed for a tag to be “live” on your site. +One of the most common SEO problems on WordPress sites is that people add too many tags to posts. In fact, [our research shows](https://fewertags.com/research/) that _most_ WordPress sites use tags wrong. The WordPress interface makes it incredibly easy to do so, and with every tag you add, you add another URL to your site for search engines to crawl and index. -Fewer Tags Free solves this problem for you! It does that by simply not showing tags that have less than 10 posts in them to users and search engines. +Fewer Tags solves this problem for you! It does that by: -Tags that have less than the configured number of posts: +**Minimum post count threshold** + +Tags that have less than the configured number of posts (default: 10): * Are hidden from visitors and search engines on your site, and are redirected to your homepage. * They don't show up in tag listings. -* The are no longer linked in WordPress core's or Yoast SEO or Slim SEO generated XML sitemaps. +* They are no longer linked in WordPress core's or Yoast SEO or Slim SEO generated XML sitemaps. -This positively affects your site's SEO and also leads to less crawling, as you have less useless tag pages. +**Merge terms** -See [this video](https://www.youtube.com/watch?v=KItn1X1qMas) if you want to learn how to use Fewer Tags Free. +* 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. -https://www.youtube.com/watch?v=KItn1X1qMas +**Redirect on delete** -If you want to *truly* solve the tag problem on your site, consider the [Fewer Tags Pro plugin & course](https://www.youtube.com/watch?v=NkF3Y6iIoDk). It'll help you fix the problems with tags on your site very quickly. +* 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. -https://www.youtube.com/watch?v=NkF3Y6iIoDk +This positively affects your site's SEO and also leads to less crawling, as you have fewer useless tag pages. -You can buy Fewer Tags Pro (or read more about it) on [fewertags.com](https://fewertags.com/)! +See [this video](https://www.youtube.com/watch?v=KItn1X1qMas) if you want to learn how to use Fewer Tags. + +https://www.youtube.com/watch?v=KItn1X1qMas + +You can learn more about Fewer Tags on [fewertags.com](https://fewertags.com/)! == Frequently Asked Questions == @@ -43,19 +52,20 @@ So you have a site with a lot of tags that don't add any value? Yes, you can saf No, you should not noindex your tag pages. If your tag pages have more than 10 posts in them, they are valuable ways of getting your site crawled and of combining related content. There's no reason to noindex those pages at that point. What you could (and should) do is add descriptions to those tag pages. -= 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 help validate, triage and handle any 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 get rid of those tags entirely? = += How can I report security bugs? = -Yes, that's why we created [Fewer Tags Pro](https://fewertags.com/)! +You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team help validate, triage and handle any security vulnerabilities. [Report a security vulnerability.](https://patchstack.com/database/vdp/fewer-tags) == Installation == 1. Search for Fewer Tags on the repository. 2. Install the plugin. 3. Optionally: go to Settings → Reading and adjust the default minimum number of posts (the default is 10). -4. You're done. +4. For merge and redirect features, install and activate the [Redirection plugin](https://wordpress.org/plugins/redirection/) or Yoast SEO Premium. +5. You're done. == Screenshots == 1. The Fewer Tags settings on the Settings → Reading screen. @@ -64,6 +74,17 @@ Yes, that's why we created [Fewer Tags Pro](https://fewertags.com/)! == 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 = * Fixes a case where the `keywords` output in the Yoast SEO schema might be of the wrong type. diff --git a/src/class-admin-ajax.php b/src/class-admin-ajax.php new file mode 100644 index 0000000..3f9d829 --- /dev/null +++ b/src/class-admin-ajax.php @@ -0,0 +1,228 @@ +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 = []; + } + $terms_to_redirect[ $taxonomy ][ $deleted_term->slug ] = [ + 'object' => $deleted_term, + 'permalink' => \get_term_link( $deleted_term, $taxonomy ), + ]; + $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 ); + } + + /** + * Retrieve a just deleted term. + * + * @return void + */ + public function get_just_deleted_term() { + \check_ajax_referer( 'fewer_tags_just_deleted_term' ); + + $just_deleted_term = $this->options->get( 'just_deleted_term' ); + $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 ( ! 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 ( ! 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' ), '
' . $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 ( ! 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 ) ); + + // 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 ( $source_taxonomy === 'category' && $source_term_id === 1 ) { + \wp_remove_object_terms( (int) $post_id, 1, 'category' ); + } + } + // 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' ), '' . $source_term->name . '', '' . $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' ), '' . $source_term->name . '', '' . $target_term->name . '', $source_taxonomy, $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..0901089 100644 --- a/src/class-admin.php +++ b/src/class-admin.php @@ -21,6 +21,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 ); @@ -126,4 +127,59 @@ public function remove_view_action( $actions, $tag ) { return $actions; } + + /** + * Display a notice if no redirect tool is available. + * + * @return void + */ + public function redirect_tool_notice() { + if ( Helper::determine_redirect_tool() !== false ) { + return; + } + ?> +
+

+ + +

+

+ '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' ); + + $term = $terms_to_redirect[ $taxonomy ][ $slug ]['object']; + $term_url = $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..f79ab48 --- /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..21155aa 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -26,6 +26,13 @@ class Plugin { */ public static $min_posts_count; + /** + * The option class. + * + * @var Option + */ + public $options; + /** * Register plugin hooks. * @@ -45,9 +52,21 @@ public function init() { self::$min_posts_count = (int) get_option( static::$option_name, 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 ); + // 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(); @@ -73,4 +92,200 @@ public function migrate_option() { delete_option( 'joost_min_posts_count' ); } } + + /** + * 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; + } + \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' ), + ] + ), + '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 ) { + // 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..404bec9 --- /dev/null +++ b/src/class-redirects.php @@ -0,0 +1,150 @@ +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( 'redirect_group' ); + if ( ! empty( $redirection_group ) || ! is_int( $redirection_group ) ) { + $response = $this->create_redirection_group(); + if ( false !== $response ) { + $this->options->set( 'redirection_group', $response->get_id() ); + } + } + + $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/uninstall.php b/uninstall.php index 1586787..73b0b39 100644 --- a/uninstall.php +++ b/uninstall.php @@ -13,5 +13,6 @@ // Backwards-compatibility for older versions of the plugin. delete_option( 'joost_min_posts_count' ); -// Delete the option. +// Delete the options. delete_option( 'fewer_tags' ); +delete_option( 'fewer_tags_pro' ); From 3d0eec40750c0f1659f2c101042219693ed5ea3d Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 19 Mar 2026 15:08:00 +0100 Subject: [PATCH 05/15] Replace svn with GitHub zip downloads in WP test installer The svn command is not available on GitHub Actions runners. Download the WP test suite from GitHub zip archives instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/bin/install-wp-tests.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh index c941f8e..70947a2 100755 --- a/tests/bin/install-wp-tests.sh +++ b/tests/bin/install-wp-tests.sh @@ -108,8 +108,19 @@ 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/} + download "https://github.com/WordPress/wordpress-develop/archive/refs/heads/${GITHUB_TAG}.zip" "$TMPDIR/wp-tests.zip" + if [ ! -s "$TMPDIR/wp-tests.zip" ]; then + download "https://github.com/WordPress/wordpress-develop/archive/refs/tags/${GITHUB_TAG}.zip" "$TMPDIR/wp-tests.zip" + fi + 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 From 07b01096cae0409ffb88ad95b91a8f28b63237a9 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 19 Mar 2026 15:11:56 +0100 Subject: [PATCH 06/15] Fix WP test installer: follow redirects, check HTTP status curl needs -L to follow GitHub redirects. Also check HTTP status code instead of file size to distinguish 404 from a valid download. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/bin/install-wp-tests.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh index 70947a2..9fa546d 100755 --- a/tests/bin/install-wp-tests.sh +++ b/tests/bin/install-wp-tests.sh @@ -19,7 +19,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 @@ -112,10 +112,21 @@ install_test_suite() { # Use GitHub zip downloads instead of svn export. local GITHUB_TAG=${WP_TESTS_TAG#tags/} local GITHUB_TAG=${GITHUB_TAG#branches/} - download "https://github.com/WordPress/wordpress-develop/archive/refs/heads/${GITHUB_TAG}.zip" "$TMPDIR/wp-tests.zip" - if [ ! -s "$TMPDIR/wp-tests.zip" ]; then - download "https://github.com/WordPress/wordpress-develop/archive/refs/tags/${GITHUB_TAG}.zip" "$TMPDIR/wp-tests.zip" + 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" From 345f563e8eb18c8cd5e80760d1b52d966d2cfd09 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Thu, 19 Mar 2026 15:18:02 +0100 Subject: [PATCH 07/15] Set platform PHP 7.4 to ensure lock file compatibility with CI The lock file had doctrine/instantiator 2.2.x-dev requiring PHP 8.4, breaking CI jobs running on PHP 7.4. Setting platform.php ensures composer resolves packages compatible with the minimum PHP version. Co-Authored-By: Claude Opus 4.6 (1M context) --- composer.json | 3 +++ composer.lock | 30 +++++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index c37286f..785b762 100644 --- a/composer.json +++ b/composer.json @@ -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 a7fc88c..6af7183 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "adc8a2d0c41f9b518d5cd8ab32e4be8e", + "content-hash": "c81256e86b3ae98b873a86cbd305b634", "packages": [], "packages-dev": [ { @@ -223,29 +223,30 @@ }, { "name": "doctrine/instantiator", - "version": "2.2.x-dev", + "version": "1.5.x-dev", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "78f5d718c7f52afc31e59434f625b34c52875d72" + "reference": "12be2483e1f0e850b353e26869e4e6c038459501" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/78f5d718c7f52afc31e59434f625b34c52875d72", - "reference": "78f5d718c7f52afc31e59434f625b34c52875d72", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/12be2483e1f0e850b353e26869e4e6c038459501", + "reference": "12be2483e1f0e850b353e26869e4e6c038459501", "shasum": "" }, "require": { - "php": "^8.4" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^14", + "doctrine/coding-standard": "^9 || ^12", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -272,7 +273,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.2.x" + "source": "https://github.com/doctrine/instantiator/tree/1.5.x" }, "funding": [ { @@ -288,7 +289,7 @@ "type": "tidelift" } ], - "time": "2026-01-14T07:50:32+00:00" + "time": "2023-12-09T14:16:53+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -3109,5 +3110,8 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, + "platform-overrides": { + "php": "7.4.33" + }, "plugin-api-version": "2.6.0" } From 784a6a28fc5f21904ac2a14047e561df2d03015d Mon Sep 17 00:00:00 2001 From: Filip Date: Thu, 19 Mar 2026 21:06:26 +0000 Subject: [PATCH 08/15] Fix security, bugs, and UX issues from PR #49 code review - Add capability checks (manage_categories) to all AJAX handlers - Fix XSS: escape slug/taxonomy in HTML attributes and JS href contexts - Fix XSS: escape term names and URLs in AJAX response messages - Fix bug: Redirection group option key mismatch (redirect_group vs redirection_group) causing new group creation on every merge - Fix bug: inverted condition always recreating Redirection group - Fix bug: get_term_link called after term deletion; capture permalink in pre_delete_term hook instead - Add merge action to categories and custom taxonomies, not just tags - Scope redirect tool admin notice to edit-tags screens only - Scope MutationObserver to #the-list instead of document.body - Use rest_base from taxonomy object for REST API endpoints - Set allowHTML: false on Choices.js instances - Fix typo: cannnot -> cannot Co-Authored-By: Claude Opus 4.6 (1M context) --- js/fewer-tags.js | 28 +++++++++++++++++++--------- src/class-admin-ajax.php | 27 +++++++++++++++++++++++---- src/class-admin.php | 5 +++++ src/class-helper.php | 8 +++++--- src/class-plugin.php | 9 ++++++++- src/class-redirects.php | 7 ++++--- 6 files changed, 64 insertions(+), 20 deletions(-) diff --git a/js/fewer-tags.js b/js/fewer-tags.js index a2f3c61..6340082 100644 --- a/js/fewer-tags.js +++ b/js/fewer-tags.js @@ -68,15 +68,19 @@ function fewerTagsDomReady( callback ) { * @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 = document.body.contains( element ); + let inDom = theList.contains( element ); const observer = new MutationObserver( () => { - if ( ! document.body.contains( element ) && inDom ) { + if ( ! theList.contains( element ) && inDom ) { inDom = false; callback(); } } ); - observer.observe( document.body, { childList: true, subtree: true } ); + observer.observe( theList, { childList: true, subtree: true } ); } ); }; @@ -136,6 +140,7 @@ async function fewerTagsGetTaxonomies( currentTaxonomy ) { selected: ( taxonomies[ key ].slug === currentTaxonomy ), customProperties: { post_type: taxonomies[ key ].types, + rest_base: taxonomies[ key ].rest_base, }, } ) ); @@ -152,10 +157,11 @@ 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 ) { +async function fewerTagsGetAllTerms( taxonomy, restBase ) { const retrievedTerms = []; const perPage = 100; // Max allowed by WP REST API let page = 1; @@ -163,7 +169,9 @@ async function fewerTagsGetAllTerms( taxonomy ) { const termsOutput = []; let endpoint = ''; - if ( taxonomy === 'post_tag' ) { + 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`; @@ -242,7 +250,7 @@ fewerTagsGetAllTerms( FTchoicesElement.dataset.taxonomy ).then( ( terms ) => { FTterms = terms; FTchoices = new Choices( FTchoicesElement, { choices: FTterms, - allowHTML: true, + allowHTML: false, position: 'bottom', itemSelectText: '', renderChoiceLimit: -1, @@ -282,7 +290,7 @@ fewerTagsDomReady( () => { FTtaxonomies = taxonomies; new Choices( FTtaxonomyElement, { choices: FTtaxonomies, - allowHTML: true, + allowHTML: false, position: 'bottom', itemSelectText: '', renderChoiceLimit: -1, @@ -312,10 +320,12 @@ fewerTagsDomReady( () => { FTtaxonomyElement.addEventListener( 'change', ( event ) => { const targetTaxonomy = event.target.value; - fewerTagsGetAllTerms( targetTaxonomy ).then( ( terms ) => { + 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' ).innerHTML = 'Target ' + targetTaxonomy; + document.getElementById( 'fewer-tags-target-term-label' ).textContent = 'Target ' + targetTaxonomy; document.getElementById( 'fewer-tags-target-taxonomy-slug' ).value = targetTaxonomy; } ); } ); diff --git a/src/class-admin-ajax.php b/src/class-admin-ajax.php index 3f9d829..223c828 100644 --- a/src/class-admin-ajax.php +++ b/src/class-admin-ajax.php @@ -50,9 +50,11 @@ public function redirect_term_on_deletion( $term, $tt_id, $taxonomy, $deleted_te 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' => \get_term_link( $deleted_term, $taxonomy ), + 'permalink' => $permalink, ]; $this->options->set( 'terms_to_redirect', $terms_to_redirect ); } @@ -68,6 +70,7 @@ public function redirect_term_on_deletion( $term, $tt_id, $taxonomy, $deleted_te 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 ) ); } /** @@ -78,6 +81,10 @@ public function pre_delete_term( $term_id, $taxonomy ) { 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' ); $msg = Helper::redirect_term_notice( $just_deleted_term->slug, $just_deleted_term->name, $just_deleted_term->taxonomy ); @@ -92,6 +99,10 @@ public function get_just_deleted_term() { 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' ) ] ); } @@ -112,6 +123,10 @@ public function dismiss_notice() { 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( [ @@ -134,7 +149,7 @@ public function redirect_url_action() { [ 'slug' => $term->slug, // translators: %1$s is the just redirected term name. - 'msg' => sprintf( __( 'Redirect for %1$s created!', 'fewer-tags' ), '' . $term->name . '' ), + 'msg' => sprintf( __( 'Redirect for %1$s created!', 'fewer-tags' ), '' . \esc_html( $term->name ) . '' ), ] ); } @@ -147,6 +162,10 @@ public function redirect_url_action() { 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' ) ] ); } @@ -180,10 +199,10 @@ public function merge_terms() { $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' ), '' . $source_term->name . '', '' . $target_term->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' ), '' . $source_term->name . '', '' . $target_term->name . '', $source_taxonomy, $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( [ diff --git a/src/class-admin.php b/src/class-admin.php index 0901089..e30f4c0 100644 --- a/src/class-admin.php +++ b/src/class-admin.php @@ -134,6 +134,11 @@ public function remove_view_action( $actions, $tag ) { * @return void */ public function redirect_tool_notice() { + $screen = \get_current_screen(); + if ( ! \is_object( $screen ) || $screen->base !== 'edit-tags' ) { + return; + } + if ( Helper::determine_redirect_tool() !== false ) { return; } diff --git a/src/class-helper.php b/src/class-helper.php index a55e746..1854dbe 100644 --- a/src/class-helper.php +++ b/src/class-helper.php @@ -22,7 +22,9 @@ class Helper { * @return string The redirect notice HTML. */ public static function redirect_term_notice( $slug, $name, $taxonomy ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Slugs are never user input. + $slug = \esc_attr( $slug ); + $taxonomy = \esc_attr( $taxonomy ); + $notice = '
'; $notice .= '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in function. @@ -80,8 +82,8 @@ public static function get_redirect_message( $slug, $taxonomy ) { } $msg .= '

'; return $msg; diff --git a/src/class-plugin.php b/src/class-plugin.php index 21155aa..6bc7da5 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -66,6 +66,13 @@ public function init() { \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 ) { @@ -225,7 +232,7 @@ public function output_modal() {

labels->name ) ) ); ?>

-

+

diff --git a/src/class-redirects.php b/src/class-redirects.php index 404bec9..ef3b218 100644 --- a/src/class-redirects.php +++ b/src/class-redirects.php @@ -101,11 +101,12 @@ public function create_redirect( $source_url, $target_url ) { * @return bool Whether the redirect was created successfully. */ public function create_redirection_redirect( $source_url, $target_url ) { - $redirection_group = $this->options->get( 'redirect_group' ); - if ( ! empty( $redirection_group ) || ! is_int( $redirection_group ) ) { + $redirection_group = $this->options->get( 'redirection_group' ); + if ( empty( $redirection_group ) || ! is_int( $redirection_group ) ) { $response = $this->create_redirection_group(); if ( false !== $response ) { - $this->options->set( 'redirection_group', $response->get_id() ); + $redirection_group = $response->get_id(); + $this->options->set( 'redirection_group', $redirection_group ); } } From 987367c489f2e8ef66bb28f1e73b9799aee96af5 Mon Sep 17 00:00:00 2001 From: Filip Date: Thu, 19 Mar 2026 21:53:02 +0000 Subject: [PATCH 09/15] Fix PHPCS: multi-item array formatting in get_taxonomies call Co-Authored-By: Claude Opus 4.6 (1M context) --- src/class-plugin.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/class-plugin.php b/src/class-plugin.php index 6bc7da5..affa813 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -69,7 +69,13 @@ public function init() { \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' ); + $taxonomies = \get_taxonomies( + [ + 'public' => true, + '_builtin' => false, + ], + 'names' + ); foreach ( $taxonomies as $taxonomy ) { \add_filter( "{$taxonomy}_row_actions", [ $this, 'add_merge_action' ], 10, 2 ); } From a52ad70e5ef1e7328f0e4f5865f12faa1e7b44ef Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 20 Mar 2026 15:23:08 +0100 Subject: [PATCH 10/15] Merge fewer_tags and fewer_tags_pro into a single array option Consolidate the two separate wp_options entries (fewer_tags scalar and fewer_tags_pro array) into a single fewer_tags array option. The Option singleton is now the single source of truth. - Change Option class to use 'fewer_tags' instead of 'fewer_tags_pro' - Remove Plugin::$option_name, read min_posts_count from Option singleton - Expand migrate_option() to handle all upgrade scenarios idempotently - Add sanitize_callback to preserve other array keys when saving settings - Update settings field name/id for nested array structure - Remove fewer_tags_pro cleanup from uninstall.php Co-Authored-By: Claude Opus 4.6 (1M context) --- src/class-admin.php | 43 +++++++++++++++++++++++++----- src/class-option.php | 2 +- src/class-plugin.php | 43 ++++++++++++++++++++++-------- tests/phpunit/test-class-admin.php | 6 ++--- tests/phpunit/test-fewer-tags.php | 2 +- uninstall.php | 3 +-- 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/class-admin.php b/src/class-admin.php index e30f4c0..a48792f 100644 --- a/src/class-admin.php +++ b/src/class-admin.php @@ -7,8 +7,6 @@ namespace FewerTags; -use FewerTags\Plugin; - /** * FewerTags Admin Class */ @@ -41,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' ], + ] + ); } /** @@ -68,8 +73,8 @@ public function display_section() { public function display_setting() { ?> get( 'min_posts_count' ) ?? 10 ); if ( is_admin() ) { if ( \wp_doing_ajax() ) { @@ -99,11 +92,39 @@ 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' ); + } } /** 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(); } diff --git a/uninstall.php b/uninstall.php index 73b0b39..1586787 100644 --- a/uninstall.php +++ b/uninstall.php @@ -13,6 +13,5 @@ // Backwards-compatibility for older versions of the plugin. delete_option( 'joost_min_posts_count' ); -// Delete the options. +// Delete the option. delete_option( 'fewer_tags' ); -delete_option( 'fewer_tags_pro' ); From 9ef7c9423ba1cb1b1f9ba65f1bd4d1b1c8165dca Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Sat, 21 Mar 2026 15:32:48 +0100 Subject: [PATCH 11/15] link to plugins, target="_blank" rel="noopener noreferrer" Signed-off-by: Filip Ilic --- src/class-admin.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/class-admin.php b/src/class-admin.php index a48792f..42d65ff 100644 --- a/src/class-admin.php +++ b/src/class-admin.php @@ -177,7 +177,13 @@ public function redirect_tool_notice() {

- + Redirection' + ); + ?>

-

+

Date: Fri, 29 May 2026 12:52:25 +0200 Subject: [PATCH 12/15] minor fixes, mostly defensive code Signed-off-by: Filip Ilic --- js/fewer-tags.js | 2 +- src/class-admin-ajax.php | 13 ++++++++++--- src/class-helper.php | 8 ++++++-- src/class-plugin.php | 5 +++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/js/fewer-tags.js b/js/fewer-tags.js index 6340082..07d1058 100644 --- a/js/fewer-tags.js +++ b/js/fewer-tags.js @@ -386,7 +386,7 @@ fewerTagsDomReady( () => { // Otherwise the old value will show up in the dropdown placeholder, super annoying. document.querySelector( '.choices.terms .choices__item' ).innerText = ''; - if ( termID === '1' && element.getAttribute( 'data-term-taxonomy' ) === 'category' ) { + 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 { diff --git a/src/class-admin-ajax.php b/src/class-admin-ajax.php index 223c828..8d722f4 100644 --- a/src/class-admin-ajax.php +++ b/src/class-admin-ajax.php @@ -86,7 +86,11 @@ public function get_just_deleted_term() { } $just_deleted_term = $this->options->get( 'just_deleted_term' ); - $msg = Helper::redirect_term_notice( $just_deleted_term->slug, $just_deleted_term->name, $just_deleted_term->taxonomy ); + 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 ); } @@ -178,12 +182,15 @@ public function merge_terms() { $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 ( $source_taxonomy === 'category' && $source_term_id === 1 ) { - \wp_remove_object_terms( (int) $post_id, 1, 'category' ); + 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. diff --git a/src/class-helper.php b/src/class-helper.php index 1854dbe..83186e3 100644 --- a/src/class-helper.php +++ b/src/class-helper.php @@ -64,14 +64,18 @@ public static function get_redirect_message( $slug, $taxonomy ) { $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 = $terms_to_redirect[ $taxonomy ][ $slug ]['permalink']; + $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 ) ); + $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' ) { diff --git a/src/class-plugin.php b/src/class-plugin.php index 2b62d41..611a155 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -161,6 +161,7 @@ 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( @@ -236,6 +237,7 @@ public function enqueue_scripts() { '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' @@ -317,6 +319,9 @@ public function do_notices() { 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 ); } From dec91edda5b138a28a19057c4af7e10bcec97bab Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Fri, 29 May 2026 13:22:16 +0200 Subject: [PATCH 13/15] Update readme.txt header and fix README.md repo URLs - readme.txt: contributors, tags, tested-up-to 7.0, GPL-3.0-or-later - README.md: point CI badges at ProgressPlanner repo Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Filip Ilic --- README.md | 6 +++--- readme.txt | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dd4e48c..46bc771 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![CS](https://github.com/jdevalk/fewer-tags/actions/workflows/cs.yml/badge.svg)](https://github.com/jdevalk/fewer-tags/actions/workflows/cs.yml) -[![Lint](https://github.com/jdevalk/fewer-tags/actions/workflows/lint.yml/badge.svg)](https://github.com/jdevalk/fewer-tags/actions/workflows/lint.yml) -[![Security](https://github.com/Emilia-Capital/fewer-tags/actions/workflows/security.yml/badge.svg)](https://github.com/Emilia-Capital/fewer-tags/actions/workflows/security.yml) +[![CS](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/cs.yml/badge.svg)](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/cs.yml) +[![Lint](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/lint.yml/badge.svg)](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/lint.yml) +[![Security](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/security.yml/badge.svg)](https://github.com/ProgressPlanner/fewer-tags/actions/workflows/security.yml) ![Fewer Tags](/.wordpress-org/github_banner_fewer_tags_pp.png) diff --git a/readme.txt b/readme.txt index 10ce580..982b73a 100644 --- a/readme.txt +++ b/readme.txt @@ -1,12 +1,12 @@ === Fewer Tags === -Contributors: progressplanner,aristath,joostdevalk -Tags: tag, tags, seo +Contributors: joostdevalk, aristath, filipi, progressplanner +Tags: seo, tags, taxonomy, sitemap, archives Requires at least: 6.7 -Tested up to: 6.9 +Tested up to: 7.0 Requires PHP: 7.4 Stable tag: 2.0 -License: GPL3+ -License URI: https://www.gnu.org/licenses/gpl-3.0.en.html +License: GPL-3.0-or-later +License URI: https://www.gnu.org/licenses/gpl-3.0.html Manage your site's tags: set minimum post counts, merge terms across taxonomies, and create redirects when deleting or merging terms. From 2fe5b4a47b1c10f8c64bc15ce3d1c38cc906fcbf Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Mon, 1 Jun 2026 12:54:38 +0200 Subject: [PATCH 14/15] Point plugin home URLs to progressplanner.com/plugins/fewer-tags/ Addresses @ilicfilip's review note on #49: the plugin moved to progressplanner.com but still referenced the old fewertags.com home in the plugin header (Plugin URI + description) and the readme.txt footer. Left the /research/ deep link pointing at its live source, which has no progressplanner.com equivalent. Co-Authored-By: Claude Opus 4.8 (1M context) --- fewer-tags.php | 4 ++-- readme.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fewer-tags.php b/fewer-tags.php index 98f2482..5c4159b 100644 --- a/fewer-tags.php +++ b/fewer-tags.php @@ -6,8 +6,8 @@ * @version 2.0 * * Plugin Name: Fewer Tags - * Plugin URI: https://fewertags.com/ - * Description: Manage your site's tags: set minimum post counts, merge terms, and create redirects. Change settings under Settings → Reading. Learn more 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: 2.0 diff --git a/readme.txt b/readme.txt index 982b73a..ca00ec4 100644 --- a/readme.txt +++ b/readme.txt @@ -40,7 +40,7 @@ See [this video](https://www.youtube.com/watch?v=KItn1X1qMas) if you want to lea https://www.youtube.com/watch?v=KItn1X1qMas -You can learn more about Fewer Tags on [fewertags.com](https://fewertags.com/)! +You can learn more about Fewer Tags on [progressplanner.com](https://progressplanner.com/plugins/fewer-tags/)! == Frequently Asked Questions == From 809f8bcd0ad0c22420341896ef43653cbd3808cf Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Fri, 19 Jun 2026 10:54:26 +0200 Subject: [PATCH 15/15] Fix contradictory redirect notices when no redirect tool is active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses @tacoverdo's report on #49: on a site with only Yoast SEO free (no Redirection, no Yoast SEO Premium), deleting a tag warned that a redirect tool was required, yet still offered "Redirect to homepage / another URL" links — and clicking them reported success even though no redirect was ever created. - class-helper.php: get_redirect_message() now checks for a redirect tool first; with none it tells the user to install Redirection or Yoast SEO Premium and stops, instead of offering links that silently fail. Also fix an inaccurate translator comment (%2$s is the singular label, not the slug). - class-admin-ajax.php: redirect_url_action() now reports a real failure via wp_send_json_error when the redirect could not be created, instead of always claiming success. - fewer-tags.js: render a notice-error vs notice-success based on response.success (wp_send_json_error still returns HTTP 200). - class-admin.php: correct the warning wording — the tool is needed to create redirects when merging/deleting terms, not to merge them. Co-Authored-By: Claude Opus 4.8 (1M context) --- js/fewer-tags.js | 3 ++- src/class-admin-ajax.php | 9 ++++++++- src/class-admin.php | 2 +- src/class-helper.php | 16 ++++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/js/fewer-tags.js b/js/fewer-tags.js index 07d1058..0e9ef72 100644 --- a/js/fewer-tags.js +++ b/js/fewer-tags.js @@ -104,9 +104,10 @@ function fewerTagsRedirectToUrl( slug, taxonomy, target, nonce ) { // eslint-dis _ajax_nonce: nonce, }, successAction: ( response ) => { + const noticeClass = response.success ? 'notice-success' : 'notice-error'; document.getElementById( `fewer-tags-redirect-${ response.data.slug }` ) .outerHTML = ` -
+

${ response.data.msg }