├── .gitignore ├── LICENSE.txt ├── README.md ├── ajax-filter-posts.php ├── assets ├── css │ ├── ajax-filter-posts.css │ └── ajax-filter-theme.css └── js │ └── ajax-filter-posts.js ├── class-ajax-filter-posts.php ├── index.php ├── languages ├── ajax-filter-posts-fr_FR.mo ├── ajax-filter-posts-fr_FR.po ├── ajax-filter-posts-nl_NL.mo ├── ajax-filter-posts-nl_NL.po └── ajax-filter-posts.pot ├── package.json └── templates ├── base.php └── partials ├── filters.php └── loop.php /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | *.DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | 7 | # Files that might appear in the root of a volume 8 | .DocumentRevisions-V100 9 | .fseventsd 10 | .Spotlight-V100 11 | .TemporaryItems 12 | .Trashes 13 | .VolumeIcon.icns 14 | .com.apple.timemachine.donotpresent 15 | 16 | # Directories potentially created on remote AFP share 17 | .AppleDB 18 | .AppleDesktop 19 | .apdisk -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Ajax Filter Posts 2 | 3 | ## Description 4 | 5 | A WordPress plugin to filter posts with taxonomies terms and load more posts via Ajax. 6 | You can add posts and filters via a **shortcode** on any page. 7 | 8 | ``` 9 | [ajax_filter_posts post_type="recipe" tax="meal_type, food_type, diet_type" posts_per_page="12"] 10 | ``` 11 | 12 | This plugins uses no dependencies, is translatable and WPML ready. 13 | 14 | ## Parameters 15 | 16 | - **tax** 17 | A comma seperated list of taxonomies to filter the post by. Default `post_term`. 18 | 19 | - **post_type** 20 | A comma seperated list of post types to show. Default `post`. 21 | 22 | - **post_status** 23 | A comma seperated list of post status to show. Default `publish`. 24 | 25 | - **post_per_page** 26 | Numbers of maximum posts to retreive at a time. Default 12. 27 | 28 | - **orderby** 29 | Value to order the posts by. Supports `ID`, `author`, `title`, `name`, `type`, `date`, `modified`, `parent`, `rand`, `comment_count`, `relevance`, and `menu_order`. 30 | Does **not** support `meta_value`, `meta_value_num`, `post_name__in`, `post_parent__in` `post_parent__in` because additionals arguments needs to be set with these orderby values. You can [add your own query arguments via a filter hook](#ajax_filter_posts_query_args) if you need that support. Defaults to `date`. 31 | 32 | Check the [WordPress documentation on Query arguments](https://developer.wordpress.org/reference/classes/wp_query/#order-orderby-parameters) for more information. 33 | 34 | - **order** 35 | Order the posts ascending or descendings. Support `ASC` (1, 2, 3; a, b, c) and `DESC` (3, 2, 1; c, b, a). Defaults to `DESC`. 36 | 37 | - **multiselect** 38 | Allow one or more active filters per taxonomy. Defaults to `true`: allow more active filters 39 | 40 | - **id** 41 | Usefull for custom styling or to target specific instances of the shortcode in the filter hooks. Default not set. 42 | 43 | ## Overwriting template files 44 | 45 | To easily overwrite template files you can copy one or more of the files in the templates folder to your own theme. Create a folder `ajax-filter-posts` in the root of your theme directory and copy the files in that newly created folder. Keep in mind that you have to keep the folder structure intact. For example: If you want a custom version of `loop.php`, you copy the file to `<>/wp-content/themes/<>/ajax-filter-posts/partials`. 46 | 47 | You can also [set a custom template path](#ajax_filter_posts_template_name). 48 | 49 | ## Motivation 50 | 51 | I build a lot of sites that needed a functionality like this and decided to create a plugin for it. Although there are a lot of plugins doing something like this, they usually add a lot of bloat and are not developer friendly. This plugin is for a developer easier to implement, easier to edit and keeps te codebase cleaner. 52 | 53 | ## Installation 54 | 55 | Clone this repo to your plugins or mu-plugins folder. When you load it in your mu-plugins folder, you have to call the plugin via a file that is directly in the `mu-plugins` folder. See [this article](https://www.sitepoint.com/wordpress-mu-plugins/) for more information. 56 | 57 | ## Requirements 58 | Wordpress 5.7.0 or higer 59 | 60 | ## Filters hooks 61 | As a developer you can overwrite functionality with WordPress hooks 62 | 63 | ### `ajax_filter_posts_query_args` 64 | 65 | #### Description 66 | With the filter `ajax_filter_posts_query_args` you can pass or alter query arguments to all post queries made by this plugin. 67 | 68 | #### Arguments 69 | `array $query_args` - query arguments set by the plugin Ajax Filter posts 70 | `array $shortcode_attributes` - all shortcode attributes 71 | 72 | #### Example 73 | For example you can add an extra taxonomy query. 74 | 75 | ```php 76 | /** 77 | * Add the diet term on all the queries made with the shortcode ajax_filter_posts 78 | * 79 | * @param array $query_args query arguments set by the plugin Ajax Filter posts 80 | * @param array $shortcode_attributes all shortcode attributes 81 | * 82 | * @return array a updated list of query arguments 83 | */ 84 | function my_site_set_additional_term_for_ajax_filter_posts($query_args, $shortcode_attributes) { 85 | 86 | // Only show posts with the term vegan in the diet taxonomy 87 | $diet_tax_query_args = [ 88 | [ 89 | 'taxonomy' => 'diet', 90 | 'field' => 'slug', 91 | 'terms' => 'vegan', 92 | ], 93 | ]; 94 | 95 | // If there are already tax queries args set, merge my query args with the set args 96 | if ( !empty( $query_args['tax_query'] ) ) { 97 | $prev_set_tax_args = $query_args['tax_query']; 98 | $query_args['tax_query'] = [ 99 | // Set the relationship to AND: we want only post with my term and the set terms by the user 100 | // Also see https://developer.wordpress.org/reference/classes/wp_query/#taxonomy-parameters 101 | 'relation' => 'AND', 102 | $diet_tax_query_args, 103 | $prev_set_tax_args 104 | ]; 105 | return $query_args; 106 | } 107 | 108 | // If there are no tax queries args already set, just add it 109 | $query_args['tax_query'] = $diet_tax_query_args; 110 | return $query_args; 111 | } 112 | add_filter('ajax_filter_posts_query_args', 'my_site_set_additional_term_for_ajax_filter_posts', 10, 2); 113 | ``` 114 | ### `ajax_filter_posts_is_post_type_viewable` 115 | 116 | By default only post types that are publicly queryable are allowed as shortcode parameters. 117 | This prevents that for example a custom private can be viewed when the wrong parameters are set or when a visitor manipulates the AJAX-request. 118 | 119 | For built-in post types such as posts and pages, the 'public' value will be evaluated. For all others, the 'publicly_queryable' value will be used. 120 | 121 | You can overwrite this check with this hook 122 | 123 | #### Arguments 124 | `boolean $is_publicly_queryable` - Default return value, esult of checking all set post types against Wordpress' *is_post_type_viewable* function 125 | 126 | `array $shortcode_attributes` - all shortcode attributes, including the *post_type* attribute 127 | 128 | ### `ajax_filter_posts_is_post_status_viewable` 129 | 130 | By default only post status that are publicly queryable are allowed as shortcode parameters. 131 | This prevents that for example private or trashed posts can be viewed when the wrong parameters are set or when a visitor manipulates the AJAX-request. 132 | 133 | For built-in post statuses such as publish and private, the ‘public’ value will be evaluted. For all others, the ‘publicly_queryable’ value will be used. 134 | 135 | You can overwrite this check with this hook 136 | 137 | #### Arguments 138 | `boolean $is_publicly_queryable` - Default return value, result of checking all set post status against Wordpress' *is_post_status_viewable* function 139 | 140 | `array $shortcode_attributes` - all shortcode attributes, including the *post_status* attribute 141 | 142 | ### `ajax_filter_posts_template_name` 143 | 144 | This package searches for the the template files [in the active theme folder and in this plugin folder](#overwriting-template-files). If that doesn't fit your needs, you can overwrite the template path. 145 | 146 | #### Arguments 147 | `string $template` - The current retrieved template path. Empty if no path could be found. 148 | 149 | `string $template_name` - The name of the current template to retrieve, with exentsion and subpath (e.g. base.php, partials/filters.php). See the template folder of this package for the used template files. 150 | 151 | #### Arguments 152 | `boolean $is_publicly_queryable` - Default return value, result of checking all set post status against Wordpress' *is_post_status_viewable* function 153 | 154 | `array $shortcode_attributes` - all shortcode attributes, including the *post_status* attribute 155 | 156 | 157 | ## License 158 | 159 | GNU GENERAL PUBLIC LICENSE 160 | -------------------------------------------------------------------------------- /ajax-filter-posts.php: -------------------------------------------------------------------------------- 1 | a { 228 | display: block; 229 | } 230 | 231 | .ajax-posts__post img { 232 | width: 100%; 233 | } 234 | 235 | 236 | @media screen and (min-width: 35rem) { 237 | .ajax-posts__post { 238 | /* IE11 doesn't support calc in flex-basis. 1.8rem padding, diveded by 2, the amount of items in a row */ 239 | width: calc(50% - 0.9rem); 240 | -webkit-flex: 0 0 auto; 241 | -ms-flex: 0 0 auto; 242 | flex: 0 0 auto; 243 | } 244 | } 245 | 246 | @media screen and (min-width: 35rem) and (max-width: 61rem) { 247 | .is-expanded-filters .ajax-posts__post:nth-child(n+6) { 248 | margin-right: 50%; /* fake a second element to break the flow */ 249 | } 250 | } 251 | 252 | @media screen and (min-width: 62rem) { 253 | .ajax-posts__post { 254 | /* IE11 doesn't support calc in flex-basis. Two times 1.8rem padding, diveded by 3, the amount of items in a row */ 255 | width: calc(33.33% - 1.2rem); 256 | -webkit-flex: 0 0 auto; 257 | -ms-flex: 0 0 auto; 258 | flex: 0 0 auto; 259 | } 260 | 261 | .is-expanded-filters .ajax-posts__post:nth-child(2n) { 262 | margin-right: 33.33%; /* fake a third element to break the flow */ 263 | } 264 | } 265 | 266 | /*------------------------------------*\ 267 | SPINNER 268 | \*------------------------------------*/ 269 | 270 | .ajax-posts__spinner { 271 | clear: both; 272 | display: none; 273 | position: absolute; 274 | left: 50%; 275 | top: 70px; 276 | -webkit-transform: translateX(-50%); 277 | -ms-transform: translateX(-50%); 278 | transform: translateX(-50%); 279 | width: 40px; 280 | height: 40px; 281 | border-radius: 50%; 282 | border: 10px solid red; 283 | -webkit-animation: ajax-posts__spinner 8s infinite linear; 284 | animation: ajax-posts__spinner 8s infinite linear; 285 | } 286 | 287 | .ajax-posts.is-waiting .ajax-posts__spinner { 288 | display: block; 289 | } 290 | 291 | @-webkit-keyframes ajax-posts__spinner { 292 | 0%, 100%{ border: solid 20px #68C3A3; } 293 | 6.25% { border: solid 2px #68C3A3; } 294 | 12.5% { border: solid 2px #52B3D9; } 295 | 18.75% { border: solid 20px #52B3D9; } 296 | 25% { border: solid 20px #52B3D9; } 297 | 31.25% { border: solid 2px #52B3D9; } 298 | 37.5% { border: solid 2px #F4D03F; } 299 | 43.75% { border: solid 20px #F4D03F; } 300 | 50% { border: solid 20px #F4D03F; } 301 | 56.25% { border: solid 2px #F4D03F; } 302 | 62.5% { border: solid 2px #D24D57; } 303 | 68.75% { border: solid 20px #D24D57; } 304 | 75% { border: solid 20px #D24D57; } 305 | 81.25% { border: solid 2px #D24D57; } 306 | 87.5% { border: solid 2px #68C3A3; } 307 | 93.75% { border: solid 20px #68C3A3; } 308 | } 309 | 310 | @keyframes ajax-posts__spinner { 311 | 0%, 100%{ border: solid 20px #68C3A3; } 312 | 6.25% { border: solid 2px #68C3A3; } 313 | 12.5% { border: solid 2px #52B3D9; } 314 | 18.75% { border: solid 20px #52B3D9; } 315 | 25% { border: solid 20px #52B3D9; } 316 | 31.25% { border: solid 2px #52B3D9; } 317 | 37.5% { border: solid 2px #F4D03F; } 318 | 43.75% { border: solid 20px #F4D03F; } 319 | 50% { border: solid 20px #F4D03F; } 320 | 56.25% { border: solid 2px #F4D03F; } 321 | 62.5% { border: solid 2px #D24D57; } 322 | 68.75% { border: solid 20px #D24D57; } 323 | 75% { border: solid 20px #D24D57; } 324 | 81.25% { border: solid 2px #D24D57; } 325 | 87.5% { border: solid 2px #68C3A3; } 326 | 93.75% { border: solid 20px #68C3A3; } 327 | } 328 | 329 | /*------------------------------------*\ 330 | CUSTOM STYLING 331 | 332 | When you copy the css and want to 333 | remove the styling of this plugin, 334 | you can skip this part. 335 | \*------------------------------------*/ 336 | 337 | .ajax-posts button { 338 | display: block; 339 | margin: 2rem auto; 340 | padding: 0.7em 1.5em 0.6em 1.5em; 341 | background: #a6a6a6; 342 | color: white; 343 | border: 1px solid #a1a1a1; 344 | border-radius: 4px; 345 | text-transform: uppercase; 346 | font-size: 0.8em; 347 | letter-spacing: 1.2px; 348 | font-weight: bold; 349 | transition: background 0.2s ease; 350 | outline: none; 351 | } 352 | 353 | .ajax-posts button:hover { 354 | background: #999; 355 | } 356 | 357 | .ajax-posts__status { 358 | margin: 1rem auto; 359 | background: rgba(255, 0, 0, 0.53); 360 | color: white; 361 | padding: 0.5rem 1rem; 362 | border-radius: 10px; 363 | width: 80%; 364 | max-width: 400px; 365 | text-align: center; 366 | font-weight: bold; 367 | } 368 | 369 | .ajax-posts .ajax-posts__toggle-filter { 370 | margin-left: 0; 371 | } 372 | 373 | .ajax-posts-message { 374 | text-align: center; 375 | } 376 | 377 | .ajax-posts-message--empty { 378 | width: 100%; 379 | margin-top: 2rem; 380 | color: #999; 381 | } 382 | 383 | .ajax-posts__filters h3 { 384 | margin: 0; 385 | font-size: 0.8rem; 386 | text-transform: uppercase; 387 | letter-spacing: 1.2px; 388 | font-weight: bold; 389 | } 390 | 391 | .ajax-posts__filter { 392 | color: #333; 393 | border-top: 1px dashed #B9B9B4; 394 | padding: 5px; 395 | } 396 | 397 | .ajax-posts_filters li:last-child a { 398 | border-bottom: 1px dashed #B9B9B4; 399 | } 400 | 401 | .ajax-posts__filter:hover, 402 | .ajax-posts__filter.is-active, { 403 | color: #27ae60; 404 | text-decoration: none; 405 | } 406 | 407 | .ajax-posts__post { 408 | padding-bottom: 1.8rem; 409 | } 410 | 411 | .ajax-posts__post h3 { 412 | font-size: 1rem; 413 | font-weight: bold; 414 | margin-top: 0.8rem; 415 | } 416 | 417 | .ajax-posts__post img { 418 | transition: 0.2s ease; 419 | width: 100%; 420 | } 421 | 422 | .ajax-posts__post:hover img { 423 | -webkit-transform: scale(1.05); 424 | -ms-transform: scale(1.05); 425 | transform: scale(1.05); 426 | } 427 | 428 | @media screen and (min-width: 35rem) { 429 | .ajax-posts__filters { 430 | padding-right: 1.8rem; 431 | } 432 | } -------------------------------------------------------------------------------- /assets/css/ajax-filter-theme.css: -------------------------------------------------------------------------------- 1 | .ajax-posts__filter:after{ 2 | background: #f0efea; 3 | color: #27ae60; 4 | width: 15px; 5 | height: 15px; 6 | content: ''; 7 | margin-right: 5px; 8 | margin-top: 4px; 9 | float: right; 10 | } 11 | 12 | .ajax-posts__filter:hover:after, 13 | .ajax-posts__filter.is-active:after { 14 | content: '✔'; 15 | background: none; 16 | } -------------------------------------------------------------------------------- /assets/js/ajax-filter-posts.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | 3 | var queryParams = null; 4 | var container = document.querySelector('.js-container-async'); 5 | if (container) { 6 | var filterTogglers = container.getElementsByClassName('js-toggle-filters'); 7 | var content = container.querySelector('.ajax-posts__posts'); 8 | var status = container.querySelector('.ajax-posts__status'); 9 | } 10 | 11 | function init() { 12 | if (container) { 13 | addEventListeners(); 14 | setDefaults(); 15 | } 16 | } 17 | 18 | /** 19 | * Add event listeners 20 | */ 21 | function addEventListeners() { 22 | // Add event listeners with event propagination 23 | on(container,'click', 'a[data-filter]', function(event) { 24 | handleFilterEvent(event.target); 25 | event.preventDefault(); 26 | return true; 27 | }); 28 | 29 | on(container, 'click', '.js-collapse-filterlist', function(event) { 30 | toggleFilterListCollapse(event); 31 | event.preventDefault(); 32 | return true; 33 | }); 34 | 35 | on(container, 'click', '.js-reset-filters', function(event) { 36 | resetFilters(); 37 | event.preventDefault(); 38 | return true; 39 | }); 40 | 41 | on(container, 'click', '.js-load-more', function(event) { 42 | handleLoadMoreEvent(event.target); 43 | event.preventDefault(); 44 | return true; 45 | }); 46 | 47 | // Add event listner on all filter toggle buttons 48 | [].slice.call(filterTogglers).forEach(function(button){ 49 | button.addEventListener('click', toggleFilters) 50 | }); 51 | } 52 | 53 | /** 54 | * Set default query variables 55 | */ 56 | function setDefaults() { 57 | queryParams = { 58 | 'id' : container.dataset.id || null, 59 | 'page' : null, 60 | 'tax' : {}, 61 | 'quantity': parseInt(container.dataset.quantity, 10) || 0, 62 | 'postType': container.dataset.postType || 'post', 63 | 'postStatus': container.dataset.postStatus || 'publish', 64 | 'orderby': container.dataset.orderby || 'date', 65 | 'order': container.dataset.order || 'DESC', 66 | 'multiselect': container.dataset.multiselect === 'true', 67 | }; 68 | } 69 | 70 | /** 71 | * Update query and get posts after filter change 72 | * 73 | * @param NodeElement filter Clicked filter 74 | */ 75 | function handleFilterEvent(filter) { 76 | if (!filter.classList.contains('is-active')) { 77 | // If we only allow one select per filter, deselect the other filter 78 | if (!queryParams.multiselect) { 79 | deSelectSiblingFilters(filter); 80 | } 81 | filter.classList.add('is-active'); 82 | updateQueryParams({ 83 | page: 1, 84 | filter: filter.dataset.filter, 85 | term: filter.dataset.term, 86 | }); 87 | } else { 88 | filter.classList.remove('is-active'); 89 | removeQueryParam(filter.dataset.filter, filter.dataset.term); 90 | } 91 | getAJAXPosts({reset: true}); 92 | } 93 | 94 | /** 95 | * Deselect siblings filters when only one term per taxoomy is allowed 96 | * 97 | * @param NodeElement filter Clicked filter 98 | */ 99 | function deSelectSiblingFilters(filter) { 100 | var selector = '.ajax-posts__filter.is-active[data-filter=' + filter.dataset.filter + ']'; 101 | var activeSiblingFilters = container.querySelectorAll(selector); 102 | activeSiblingFilters.forEach(function(siblingFilter) { 103 | siblingFilter.classList.remove('is-active'); 104 | removeQueryParam(siblingFilter.dataset.filter, siblingFilter.dataset.term); 105 | }); 106 | } 107 | 108 | /** 109 | * Get next page of posts 110 | * 111 | * @param NodeElement button Clicked load more button 112 | */ 113 | function handleLoadMoreEvent(button){ 114 | updateQueryParams({ page: parseInt(button.dataset.page, 10) }) 115 | getAJAXPosts({reset: false}); 116 | } 117 | 118 | /** 119 | * Reset filters to empty values 120 | */ 121 | function resetFilters() { 122 | // Convert nodeList to array to prevent fail on for each 123 | var activeFilters = Array.prototype.slice.call(container.querySelectorAll('a[data-filter].is-active')); 124 | 125 | // Remove active classes 126 | activeFilters.forEach(function(filter){ 127 | filter.classList.remove('is-active'); 128 | }); 129 | 130 | // Empty taxonomy from query 131 | queryParams.tax = {}; 132 | queryParams.page = 1; 133 | getAJAXPosts({reset: true}); 134 | } 135 | 136 | /** 137 | * Toggle the filter class. 138 | * Called via event listeners 139 | */ 140 | function toggleFilters() { 141 | container.classList.toggle('is-expanded-filters'); 142 | } 143 | 144 | function toggleFilterListCollapse(event) { 145 | event.target.parentNode.parentNode.classList.toggle('is-collapsed'); 146 | } 147 | 148 | /** 149 | * Update te query parameters based on the filter change 150 | * 151 | * @param Array params params that will be changed 152 | */ 153 | function updateQueryParams(params) { 154 | queryParams.page = params.page; 155 | 156 | // If we're also updating the taxonomy 157 | if (params.filter) { 158 | if (queryParams.tax.hasOwnProperty(params.filter)) { 159 | queryParams.tax[params.filter].push(params.term); 160 | } else { 161 | queryParams.tax[params.filter] = [params.term]; 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * Remove a term from the set of query params 168 | * 169 | * @param string tax taxonomy of the term to remove 170 | * @param {string term term to remove 171 | */ 172 | function removeQueryParam(tax, term) { 173 | if (queryParams.tax.hasOwnProperty(tax)) { 174 | if (queryParams.tax[tax].indexOf(term) > -1) { 175 | queryParams.tax[tax].splice( queryParams.tax[tax].indexOf(term) , 1 ); 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * Show the Error Reponse div 182 | */ 183 | function showResponseMessage() { 184 | status.style.display = 'block'; 185 | status.scrollIntoView({behavior: "smooth"}); 186 | } 187 | 188 | /** 189 | * Show the Error Reponse div 190 | */ 191 | function hideResponseMessage() { 192 | status.style.display = 'none'; 193 | } 194 | 195 | /** 196 | * Get new posts via Ajax 197 | * 198 | * Retrieve a new set of posts based on the created query 199 | * 200 | * @return string server side generated HTML 201 | */ 202 | function getAJAXPosts(args) { 203 | 204 | // Set status to querying 205 | container.classList.add('is-waiting'); 206 | 207 | var request = new XMLHttpRequest(); 208 | request.open('POST', filterPosts.ajaxUrl, true); 209 | request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 210 | request.timeout = 4000; // time in milliseconds 211 | 212 | request.onload = function() { 213 | 214 | //remove load more button 215 | var loadMoreButton = content.querySelector('.js-load-more'); 216 | if (loadMoreButton) { 217 | content.removeChild(loadMoreButton.parentNode); 218 | } 219 | var response = JSON.parse(this.response); 220 | if (this.status === 200) { 221 | // If we have a succesfull query 222 | if (response.success) { 223 | // Hide error status button 224 | hideResponseMessage(); 225 | // If we have to remove the show more button 226 | if (args.reset) { 227 | content.innerHTML = response.data.content; 228 | } else { 229 | content.innerHTML += response.data.content; 230 | } 231 | } else { 232 | status.innerHTML = response.data; 233 | showResponseMessage(); 234 | } 235 | } else { 236 | status.innerHTML = filterPosts.serverErrorMessage; 237 | showResponseMessage(); 238 | } 239 | // Resolve status 240 | container.classList.remove('is-waiting'); 241 | }; 242 | 243 | request.ontimeout = function() { 244 | status.innerHTML = filterPosts.timeoutMessage; 245 | showResponseMessage(); 246 | container.classList.remove('is-waiting'); 247 | } 248 | 249 | request.send(objectToQueryString({ 250 | action: 'process_filter_change', 251 | nonce: filterPosts.nonce, 252 | params: queryParams, 253 | })); 254 | } 255 | 256 | /** 257 | * Helper function for event delegation 258 | * 259 | * To add event listeners on dynamic content, you can add a listener 260 | * on thewrapping container, find the dom-node that triggered 261 | * the event and check if that node mach our 262 | * 263 | * @param NodeElement el wrapping element for the dynamic content 264 | * @param string eventName type of event, e.g. click, mouseenter, etc 265 | * @param string selector selector criteria of the element where the action should be on 266 | * @param Function fn callback funciton 267 | * @return Function The callback 268 | */ 269 | function on(el, eventName, selector, fn) { 270 | var element = el; 271 | 272 | element.addEventListener(eventName, function(event) { 273 | var possibleTargets = element.querySelectorAll(selector); 274 | 275 | var target = event.target; 276 | 277 | for (var i = 0, l = possibleTargets.length; i < l; i++) { 278 | var el = target; 279 | var p = possibleTargets[i]; 280 | 281 | while(el && el !== element) { 282 | if (el === p) { 283 | return fn.call(p, event); 284 | } 285 | 286 | el = el.parentNode; 287 | } 288 | } 289 | }); 290 | } 291 | 292 | /** 293 | * Convert an deep object to a url parameter list 294 | * 295 | * Boiled down from jQuery 296 | * 297 | * WordPress Ajax post request doesn't accept JSON, only form-urlencoded! 298 | * Took me a while to get... 299 | * 300 | * Although seems not to be totally true: 301 | * http://wordpress.stackexchange.com/questions/177554/allowing-admin-ajax-php-to-receive-application-json-instead-of-x-www-form-url 302 | * 303 | * But in this case we just convert the params object to a url encoded string, like our friend and foe jQuery does. 304 | * 305 | */ 306 | function objectToQueryString(a) { 307 | var prefix, s, add, name, r20, output; 308 | s = []; 309 | r20 = /%20/g; 310 | add = function (key, value) { 311 | // If value is a function, invoke it and return its value 312 | value = ( typeof value == 'function' ) ? value() : ( value == null ? "" : value ); 313 | s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); 314 | }; 315 | if (a instanceof Array) { 316 | for (name in a) { 317 | add(name, a[name]); 318 | } 319 | } else { 320 | for (prefix in a) { 321 | buildParams(prefix, a[ prefix ], add); 322 | } 323 | } 324 | output = s.join("&").replace(r20, "+"); 325 | return output; 326 | }; 327 | 328 | /** 329 | * Helper function to create URL parameters of deep object 330 | * 331 | * Boiled down from jQuery 332 | */ 333 | function buildParams(prefix, obj, add) { 334 | var name, i, l, rbracket; 335 | rbracket = /\[\]$/; 336 | if (obj instanceof Array) { 337 | for (i = 0, l = obj.length; i < l; i++) { 338 | if (rbracket.test(prefix)) { 339 | add(prefix, obj[i]); 340 | } else { 341 | buildParams(prefix + "[" + ( typeof obj[i] === "object" ? i : "" ) + "]", obj[i], add); 342 | } 343 | } 344 | } else if (typeof obj == "object") { 345 | // Serialize object item. 346 | for (name in obj) { 347 | buildParams(prefix + "[" + name + "]", obj[ name ], add); 348 | } 349 | } else { 350 | // Serialize scalar item. 351 | add(prefix, obj); 352 | } 353 | } 354 | 355 | init(); 356 | 357 | }()); 358 | -------------------------------------------------------------------------------- /class-ajax-filter-posts.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class Ajax_Filter_Posts { 28 | 29 | /** 30 | * The unique identifier of this plugin. 31 | * 32 | * @var string $plugin_name The string used to uniquely identify this plugin. 33 | */ 34 | protected $plugin_name; 35 | 36 | /** 37 | * The current version of the plugin. 38 | * 39 | * @var String $version The current version of the plugin. 40 | */ 41 | protected $version; 42 | 43 | /** 44 | * Defaults arguments that can be overwritten vua the shortcode attributes 45 | * 46 | * @var array $default_shortcode_attributes list of arguments 47 | */ 48 | protected $default_shortcode_attributes = array( 49 | 'id' => null, // a user can add a custom id 50 | 'post_type' => 'post,page', 51 | 'post_status' => 'publish', 52 | 'posts_per_page' => 12, 53 | 'order' => 'DESC', 54 | 'orderby' => 'date', 55 | 'tax' => 'post_tag', 56 | 'multiselect' => 'true', 57 | ); 58 | 59 | /** 60 | * Allowed orderby values a user can add as an value in the orderby shortcode attribute 61 | * 62 | * Does not support `meta_value`, `meta_value_num`, `post_name__in`, `post_parent__in` `post_parent__in` 63 | * because additionals arguments needs to be set with these orderby values. 64 | * 65 | * @var array $allowed_orderby_values flat list of orderby values 66 | */ 67 | protected $allowed_orderby_values = array( 68 | 'ID', 69 | 'author', 70 | 'title', 71 | 'name', 72 | 'type', 73 | 'date', 74 | 'modified', 75 | 'parent', 76 | 'rand', 77 | 'comment_count', 78 | 'relevance', 79 | 'menu_order', 80 | ); 81 | 82 | 83 | /** 84 | * Define the core functionality of the plugin. 85 | * 86 | * Set the plugin name and the plugin version that can be used throughout the plugin. 87 | * Load the dependencies, define the locale, and set the hooks. 88 | * 89 | */ 90 | public function __construct() { 91 | 92 | $this->plugin_name = 'ajax-filter-posts'; 93 | $this->version = '0.5.2'; 94 | 95 | add_action( 'plugins_loaded', [$this, 'load_textdomain'] ); 96 | add_action( 'wp_enqueue_scripts', [$this,'add_scripts'] ); 97 | add_action( 'wp_ajax_process_filter_change', [$this, 'process_filter_change'] ); 98 | add_action( 'wp_ajax_nopriv_process_filter_change', [$this, 'process_filter_change'] ); 99 | add_shortcode( 'ajax_filter_posts', [$this, 'create_shortcode'] ); 100 | } 101 | 102 | /** 103 | * Set the plugins language domain 104 | */ 105 | public function load_textdomain() { 106 | if (strpos( __FILE__, basename( WPMU_PLUGIN_DIR ))) { 107 | load_muplugin_textdomain( 'ajax-filter-posts', basename( dirname( __FILE__ )) . '/languages' ); 108 | } else { 109 | load_plugin_textdomain( 'ajax-filter-posts', false, basename(dirname( __FILE__ )) . '/languages' ); 110 | } 111 | } 112 | 113 | /** 114 | * Load the required assets for this plugin. 115 | * 116 | */ 117 | public function add_scripts() { 118 | 119 | $script_variables = [ 120 | 'nonce' => wp_create_nonce( 'filter-posts-nonce' ), 121 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 122 | 'timeoutMessage' => __('It took to long the get the posts. Please reload the page and try again.', 'ajax-filter-posts'), 123 | 'serverErrorMessage' => __('Got no response. Please reload the page and try again.', 'ajax-filter-posts'), 124 | ]; 125 | 126 | // IF WPML is installed add language variable to set variable later during the query 127 | // WPML can't figure out which language to query, when posts are loaded via AJAX. 128 | if (function_exists('icl_object_id')) { 129 | $script_variables['language'] = ICL_LANGUAGE_CODE; 130 | } 131 | 132 | wp_enqueue_script( 'ajax-filter', plugins_url('/assets/js/ajax-filter-posts.js', __FILE__), [], '', true ); 133 | wp_enqueue_style( 'ajax-filter', plugins_url('/assets/css/ajax-filter-posts.css', __FILE__), []); 134 | wp_localize_script( 'ajax-filter', 'filterPosts', $script_variables); 135 | } 136 | 137 | /** 138 | * Create shortcode 139 | * 140 | * @param Array $atts Array of given attributes 141 | * @return String HTML initial rendered by shortcode 142 | */ 143 | public function create_shortcode($given_attributes) { 144 | 145 | $attributes = shortcode_atts( $this->default_shortcode_attributes, $given_attributes, $this->plugin_name ); 146 | 147 | // Check if the set attributes are allowed 148 | $attributes = $this->validate_attributes($attributes); 149 | 150 | if ( is_wp_error($attributes) ) { 151 | return $attributes->get_error_message(); 152 | } 153 | 154 | $filterlists = $this->get_filterlist($attributes['tax']); 155 | 156 | $query = $this->query_posts([ 157 | 'post_type' => $attributes['post_type'], 158 | 'post_status' => $attributes['post_status'], 159 | 'posts_per_page' => $attributes['posts_per_page'], 160 | 'order' => $attributes['order'], 161 | 'orderby' => $attributes['orderby'], 162 | ], $attributes); 163 | 164 | $plural_post_name = $this->get_post_type_plural_name($query->query['post_type']); 165 | 166 | ob_start(); 167 | 168 | $this->load_template('base.php', [ 169 | 'attributes' => $attributes, 170 | 'query' => $query, 171 | 'filterlists' => $filterlists, 172 | 'plural_post_name' => $plural_post_name, 173 | ]); 174 | 175 | return ob_get_clean(); 176 | } 177 | 178 | /** 179 | * Validate shrotode attributes 180 | * 181 | * @param Array $atts Array of given attributes 182 | * @return Array|WP_Error given attributes or an erorr when attributes are not valid 183 | */ 184 | protected function validate_attributes($attributes) { 185 | 186 | // Always convert post type and status to an array so we can always be sure we deal with an array 187 | // makes multipe is_array/is_string checks redundant 188 | $attributes['post_type'] = $this->delimited_to_array($attributes['post_type']); 189 | $attributes['post_status'] = $this->delimited_to_array($attributes['post_status']); 190 | 191 | // only allow publicly post types and status to be queried, if not overwritten by developer 192 | $is_post_type_viewable = apply_filters('ajax_filter_posts_is_post_type_viewable', $this->is_every_post_type_viewable($attributes['post_type']), $attributes); 193 | $is_post_status_viewable = apply_filters('ajax_filter_posts_is_post_status_viewable', $this->is_every_post_status_viewable($attributes['post_status']), $attributes); 194 | 195 | if ( !$is_post_type_viewable || !$is_post_status_viewable ) { 196 | return new WP_Error('Posts not viewable', __("Something went wrong. The posts you've requested does not exist or is not viewable.", 'ajax-filter-posts')); 197 | } 198 | 199 | // don't allow orderby values that are not supported 200 | if ( !in_array($attributes['orderby'], $this->allowed_orderby_values) ) { 201 | return new WP_Error('Invalid orderby attribute', __("Something went wrong. The posts could not be sorted with the given orderby method.", 'ajax-filter-posts')); 202 | } 203 | 204 | return $attributes; 205 | } 206 | 207 | /** 208 | * Convert comma delimited attributes to php arrays 209 | * 210 | * @param array $attribute 211 | * 212 | * @return array comma delimited to array 213 | */ 214 | protected function delimited_to_array($attribute) { 215 | if ( !is_string($attribute) ) { 216 | return $attribute; 217 | } 218 | $attribute = explode(',', $attribute); 219 | $attribute = array_map('trim', $attribute); 220 | return $attribute; 221 | } 222 | 223 | /** 224 | * Check if all post types are viewable 225 | * 226 | * @param array $post_type 227 | * 228 | * @return boolean 229 | */ 230 | protected function is_every_post_type_viewable($post_type) { 231 | foreach ($post_type as $type) { 232 | if ( !is_post_type_viewable($type) ) { 233 | return false; 234 | } 235 | } 236 | return true; 237 | } 238 | 239 | /** 240 | * Check if all post status are viewable 241 | * 242 | * @param array $post_status 243 | * 244 | * @return boolean 245 | */ 246 | protected function is_every_post_status_viewable($post_status) { 247 | foreach ($post_status as $status) { 248 | if ( !is_post_status_viewable($status) ) { 249 | return false; 250 | } 251 | } 252 | return true; 253 | } 254 | 255 | /** 256 | * Query the posts with the given arguments 257 | * This function is called for the original query and for the queries when filters are applied 258 | * 259 | * @param array $args a list of query arguments 260 | * @param array $shortcode_attributes a list of all shortcode attributes 261 | * 262 | * @return WP_Query a new instance of WP Query 263 | */ 264 | protected function query_posts($args, $shortcode_attributes) { 265 | $query_args = apply_filters('ajax_filter_posts_query_args', $args, $shortcode_attributes); 266 | return new WP_Query($query_args); 267 | } 268 | 269 | /** 270 | * Get a list of filters and terms, based on the taxonomies set in the shortcode 271 | * 272 | * @param String $taxonomies Comma seperated list of taxonomies 273 | * @return Array List of taxonomies with terms 274 | */ 275 | protected function get_filterlist($taxonomies) { 276 | $filterlists = $this->delimited_to_array($taxonomies); 277 | $filterlists = array_filter($filterlists, 'taxonomy_exists'); 278 | $filterlists = $this->get_termlist($filterlists); 279 | return $filterlists; 280 | } 281 | 282 | /** 283 | * Get a list of filters and terms 284 | * 285 | * @param array $taxonomies A single taxonomy 286 | * @return Array Taxonomy name and list of terms associated with the taxonomy 287 | */ 288 | protected function get_termlist($taxonomies) { 289 | $list = []; 290 | 291 | foreach ($taxonomies as $taxonomy) { 292 | $terms = get_terms($taxonomy); 293 | $taxonomy_data = get_taxonomy($taxonomy); 294 | if (!empty($terms)) { 295 | $list[] = [ 296 | 'name' => $taxonomy_data->labels->singular_name, 297 | 'id' => 'taxonomy-' . str_replace('_', '-', $taxonomy_data->name), 298 | 'filters' => $terms, 299 | ]; 300 | } 301 | } 302 | 303 | return $list; 304 | } 305 | 306 | /** 307 | * Send new posts query via AJAX after filters are changed in the frontend 308 | * 309 | * @return String HTML string with parsed posts or an error message 310 | */ 311 | public function process_filter_change() { 312 | 313 | check_ajax_referer( 'filter-posts-nonce', 'nonce' ); 314 | 315 | $attributes = array( 316 | // when the id is null, the id is not transfered 317 | 'id' => !empty($_POST['params']['id']) ? sanitize_text_field($_POST['params']['id']) : null, 318 | 'post_type' => sanitize_text_field($_POST['params']['postType']), 319 | 'post_status' => sanitize_text_field($_POST['params']['postStatus']), 320 | 'tax' => $this->get_tax_query_vars($_POST['params']['tax']), 321 | 'page' => intval($_POST['params']['page']), 322 | 'orderby' => $_POST['params']['orderby'], 323 | 'order' => $_POST['params']['order'], 324 | 'quantity' => intval($_POST['params']['quantity']), 325 | 'language' => sanitize_text_field($_POST['params']['language']), 326 | ); 327 | 328 | // Abort on false attributes 329 | // Because we get these attributes via AJAX the user could have changed the attributes 330 | $attributes = $this->validate_attributes($attributes); 331 | 332 | if ( is_wp_error($attributes) ) { 333 | wp_send_json_error( $attributes->get_error_message() ); 334 | die(); 335 | } 336 | 337 | $query_args = array( 338 | 'paged' => $attributes['page'], 339 | 'post_type' => $attributes['post_type'], 340 | 'post_status' => $attributes['post_status'], 341 | 'posts_per_page' => $attributes['quantity'], 342 | 'tax_query' => $attributes['tax'], 343 | 'orderby' => $attributes['orderby'], 344 | 'order' => $attributes['order'], 345 | ); 346 | 347 | $response = $this->get_filter_posts($query_args, $attributes); 348 | 349 | if ($response) { 350 | wp_send_json_success($response); 351 | } else { 352 | wp_send_json_error(__('Oops, something went wrong. Please reload the page and try again.', 'ajax-filter-posts')); 353 | } 354 | die(); 355 | } 356 | 357 | /** 358 | * Converts the queried page number to a real page number 359 | * 360 | * @param Object $query WP Query 361 | * @return Integer Current page 362 | */ 363 | private function get_page_number($query){ 364 | $query_page = $query->get( 'paged' ); 365 | return $query_page == 0 ? 1 : $query_page; 366 | } 367 | 368 | /** 369 | * Check if the queried page is the last page of the query 370 | * 371 | * @param Object $query WP Query 372 | * @return Boolean true if is last page 373 | */ 374 | private function is_last_page($query) { 375 | return $this->get_page_number($query) >= $query->max_num_pages; 376 | } 377 | 378 | /** 379 | * Get the query paramaters based on set filters 380 | * 381 | * @param array $taxonomies list of taxanomies with terms 382 | * @return array taxonomies prepared for the WordPress Query 383 | */ 384 | protected function get_tax_query_vars($taxonomies) { 385 | $tax_query = []; 386 | 387 | foreach ($taxonomies as $taxonomy => $terms) { 388 | $taxonomy = sanitize_text_field($taxonomy); 389 | if (taxonomy_exists($taxonomy)) { 390 | $valid_terms = $this->get_valid_terms($terms, $taxonomy); 391 | if ($valid_terms) { 392 | $term_query = [ 393 | 'taxonomy' => $taxonomy, 394 | 'field' => 'slug', 395 | 'terms' => $valid_terms, 396 | ]; 397 | 398 | $tax_query[] = $term_query; 399 | 400 | } 401 | } 402 | } 403 | 404 | if( count($tax_query) > 1 ) { 405 | $tax_query[] = ['relation' => 'OR']; 406 | } 407 | return $tax_query; 408 | } 409 | 410 | /** 411 | * Check of the given terms are valid terms 412 | * 413 | * @param array $terms List of terms set by the filters 414 | * @param string $tax Taxomy associated with the terms 415 | * @return array List of valid terms 416 | */ 417 | protected function get_valid_terms($terms, $tax) { 418 | $valid_terms = []; 419 | 420 | foreach ($terms as $term) { 421 | $term = sanitize_text_field($term); 422 | if (term_exists($term,$tax)) { 423 | $valid_terms[] = $term; 424 | } 425 | } 426 | return $valid_terms; 427 | } 428 | 429 | /** 430 | * Set up a filters query and parse the template 431 | * 432 | * @param array $args Arguments for the WordPress Query 433 | * @param array $attributes All shortcodes attributes passed from the frontend 434 | * @return string HTMl to be sent via Ajax 435 | */ 436 | public function get_filter_posts($args, $attributes) { 437 | if (function_exists('icl_object_id') && !empty($attributes['language'])) { 438 | global $sitepress; 439 | $sitepress->switch_lang( $attributes['language'] ); 440 | } 441 | 442 | $query = $this->query_posts($args, $attributes); 443 | $plural_post_name = $this->get_post_type_plural_name($query->query['post_type']); 444 | $response = []; 445 | 446 | ob_start(); 447 | $this->load_template('partials/loop.php', [ 448 | 'query' => $query, 449 | 'plural_post_name' => $plural_post_name, 450 | 'attributes' => $attributes, 451 | ]); 452 | 453 | $response['content'] = ob_get_clean(); 454 | $response['found'] = $query->found_posts; 455 | return $response; 456 | } 457 | 458 | /** 459 | * Load the template 460 | * 461 | * @param string $template_name 462 | * 463 | * @return void 464 | */ 465 | private function load_template($template_name, $args) { 466 | extract($args); 467 | $template = $this->get_local_template($template_name); 468 | if ( !$template ) { 469 | echo 'No template found'; 470 | return; 471 | } 472 | include $template; 473 | } 474 | 475 | /* Get the post type plural name. defaults to post 476 | * 477 | * @param string|array $post_type 478 | * 479 | * @return string 480 | */ 481 | protected function get_post_type_plural_name($post_type) { 482 | $post_type = is_array($post_type) ? $post_type[0] : $post_type; 483 | $post_type_object = get_post_type_object($post_type); 484 | return $post_type_object ? strtolower($post_type_object->labels->name) : __('posts', 'ajax-filter-posts'); 485 | } 486 | 487 | /** 488 | * Locate template. 489 | * 490 | * Locate the called template. 491 | * Search Order: 492 | * 1. /themes/theme/ajax-posts-filters/$template_name 493 | * 2. /plugins/ajax-filter-posts/templates/$template_name. 494 | * 495 | * @since 0.3.0 496 | * 497 | * @param string $template_name Template to load. 498 | * @return string Path to the template file. 499 | */ 500 | public function get_local_template($template_name) { 501 | 502 | if (empty($template_name)) return false; 503 | 504 | $template = locate_template('ajax-filter-posts/' . $template_name); 505 | $template = apply_filters( 'ajax_filter_posts_template_name', $template, $template_name ); 506 | 507 | // If template not in theme, get plugins template file. 508 | if ( !$template ) { 509 | $template = plugin_dir_path( __FILE__ ) . 'templates/' . $template_name; 510 | } 511 | 512 | if ( !file_exists( $template ) ) { 513 | _doing_it_wrong( __FUNCTION__, sprintf( '%s does not exist.', $template ), '4.6.0' ); 514 | return false; 515 | } 516 | 517 | return $template; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 13 | "X-Textdomain-Support: yesX-Generator: Poedit 1.6.4\n" 14 | "X-Poedit-SourceCharset: UTF-8\n" 15 | "X-Poedit-KeywordsList: __;_e;esc_html_e;esc_html_x:1,2c;esc_html__;esc_attr_e;esc_attr_x:1,2c;esc_attr__;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_x:1,2c;_n:1,2;_n_noop:1,2;__ngettext:1,2;__ngettext_noop:1,2;_c,_nc:4c,1,2\n" 16 | "X-Poedit-Basepath: ..\n" 17 | "Language-Team: \n" 18 | "X-Generator: Poedit 2.4.2\n" 19 | "Last-Translator: \n" 20 | "Language: fr_FR\n" 21 | "X-Poedit-SearchPath-0: .\n" 22 | 23 | #: class-ajax-filter-posts.php:78 24 | msgid "It took to long the get the posts. Please reload the page and try again." 25 | msgstr "La récupération des posts a pris trop de temps. Merci de recharger la page et de réessayer." 26 | 27 | #: class-ajax-filter-posts.php:79 28 | msgid "Got no response. Please reload the page and try again." 29 | msgstr "Aucune réponse du serveur. Merci de recharger la page et de réessayer." 30 | 31 | #: class-ajax-filter-posts.php:172 32 | msgid "Oops, something went wrong. Please reload the page and try again." 33 | msgstr "Oups, une erreur est survenue. Merci de recharger la page et de réessayer." 34 | 35 | #: templates/base.php:4 36 | #, php-format 37 | msgid "Filter %s" 38 | msgstr "Filtrer les %s" 39 | 40 | #: templates/base.php:5 41 | #, php-format 42 | msgid "Show %s" 43 | msgstr "Afficher les %s" 44 | 45 | #: templates/base.php:6 46 | msgid "Hide filters" 47 | msgstr "Masquer les filtres" 48 | 49 | #: templates/base.php:17 50 | msgid "Loading" 51 | msgstr "Chargement" 52 | 53 | #: templates/partials/loop.php:14 54 | msgid "Load more" 55 | msgstr "Charger plus" 56 | 57 | #: templates/partials/loop.php:21 58 | #, php-format 59 | msgid "Oh, we couldn't find any %s" 60 | msgstr "Oh, nous n’avons trouvé aucun %s" 61 | 62 | #: templates/partials/loop.php:22 63 | #, php-format 64 | msgid "Try different filters or reset them all." 65 | msgstr "Essayez différents filtres ou réinitialisez-les tous." 66 | -------------------------------------------------------------------------------- /languages/ajax-filter-posts-nl_NL.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Robbertdk/wordpress-ajax-filter-posts/67c93606a42e6dd36548ded7649bcf3942c34ce1/languages/ajax-filter-posts-nl_NL.mo -------------------------------------------------------------------------------- /languages/ajax-filter-posts-nl_NL.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 ... 2 | # This file is distributed under the GNU General Public License v2 or later. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: ajax-filter-posts\n" 6 | "Report-Msgid-Bugs-To: \n" 7 | "POT-Creation-Date: 2021-11-25 12:26+0100\n" 8 | "PO-Revision-Date: \n" 9 | "Last-Translator: \n" 10 | "Language-Team: \n" 11 | "Language: nl\n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | "X-Textdomain-Support: yesX-Generator: Poedit 1.6.4\n" 17 | "X-Poedit-SourceCharset: UTF-8\n" 18 | "X-Poedit-KeywordsList: __;_e;esc_html_e;esc_html_x:1,2c;esc_html__;" 19 | "esc_attr_e;esc_attr_x:1,2c;esc_attr__;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;" 20 | "_x:1,2c;_n:1,2;_n_noop:1,2;__ngettext:1,2;__ngettext_noop:1,2;_c," 21 | "_nc:4c,1,2\n" 22 | "X-Poedit-Basepath: ..\n" 23 | "X-Generator: Poedit 3.0\n" 24 | "X-Poedit-SearchPath-0: .\n" 25 | 26 | #: class-ajax-filter-posts.php:122 27 | msgid "" 28 | "It took to long the get the posts. Please reload the page and try again." 29 | msgstr "" 30 | "Het laden van de berichten duurde te lang. Herlaad de pagina en probeer het " 31 | "nog een keer." 32 | 33 | #: class-ajax-filter-posts.php:123 34 | msgid "Got no response. Please reload the page and try again." 35 | msgstr "" 36 | "Geen reactie van de server ontvangen. Herlaad de pagina en probeer het nog " 37 | "een keer." 38 | 39 | #: class-ajax-filter-posts.php:189 40 | msgid "" 41 | "Something went wrong. The posts you've requested does not exist or is not " 42 | "viewable." 43 | msgstr "" 44 | "Er ging iets verkeerd. De berichten die wilde ophalen bestaan niet of zijn " 45 | "niet publiek toegankelijk." 46 | 47 | #: class-ajax-filter-posts.php:194 48 | msgid "" 49 | "Something went wrong. The posts could not be sorted with the given orderby " 50 | "method." 51 | msgstr "" 52 | "Er ging iets verkeerd. De berichten kunnen niet gesorteerd worden volgende " 53 | "de gegeven sorteermethode." 54 | 55 | #: class-ajax-filter-posts.php:345 56 | msgid "Oops, something went wrong. Please reload the page and try again." 57 | msgstr "" 58 | "Oeps, er ging iets verkeerd. Herlaad de pagina en probeer het nog een keer." 59 | 60 | #: class-ajax-filter-posts.php:456 61 | msgid "posts" 62 | msgstr "berichten" 63 | 64 | #: templates/base.php:17 65 | #, php-format 66 | msgid "Filter %s" 67 | msgstr "Filter %s" 68 | 69 | #: templates/base.php:18 70 | #, php-format 71 | msgid "Show %s" 72 | msgstr "Toon %s" 73 | 74 | #: templates/base.php:19 75 | msgid "Hide filters" 76 | msgstr "Verberg filters" 77 | 78 | #: templates/base.php:33 79 | msgid "Loading" 80 | msgstr "Laden" 81 | 82 | #: templates/partials/filters.php:15 83 | msgid "Show more" 84 | msgstr "Toon meer" 85 | 86 | #: templates/partials/filters.php:16 87 | msgid "Show less" 88 | msgstr "Toon minder" 89 | 90 | #: templates/partials/loop.php:15 91 | msgid "Load more" 92 | msgstr "Meer laden" 93 | 94 | #: templates/partials/loop.php:23 95 | #, php-format 96 | msgid "Oh, we couldn't find any %s" 97 | msgstr "Oh, we kunnen geen %s vinden" 98 | 99 | #: templates/partials/loop.php:24 100 | #, php-format 101 | msgid "Try different filters or reset them all." 102 | msgstr "Probeer andere filters uit of herstel ze allemaal." 103 | -------------------------------------------------------------------------------- /languages/ajax-filter-posts.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-25 12:26+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: class-ajax-filter-posts.php:122 21 | msgid "" 22 | "It took to long the get the posts. Please reload the page and try again." 23 | msgstr "" 24 | 25 | #: class-ajax-filter-posts.php:123 26 | msgid "Got no response. Please reload the page and try again." 27 | msgstr "" 28 | 29 | #: class-ajax-filter-posts.php:189 30 | msgid "" 31 | "Something went wrong. The posts you've requested does not exist or is not " 32 | "viewable." 33 | msgstr "" 34 | 35 | #: class-ajax-filter-posts.php:194 36 | msgid "" 37 | "Something went wrong. The posts could not be sorted with the given orderby " 38 | "method." 39 | msgstr "" 40 | 41 | #: class-ajax-filter-posts.php:345 42 | msgid "Oops, something went wrong. Please reload the page and try again." 43 | msgstr "" 44 | 45 | #: class-ajax-filter-posts.php:456 46 | msgid "posts" 47 | msgstr "" 48 | 49 | #: templates/base.php:17 50 | #, php-format 51 | msgid "Filter %s" 52 | msgstr "" 53 | 54 | #: templates/base.php:18 55 | #, php-format 56 | msgid "Show %s" 57 | msgstr "" 58 | 59 | #: templates/base.php:19 60 | msgid "Hide filters" 61 | msgstr "" 62 | 63 | #: templates/base.php:33 64 | msgid "Loading" 65 | msgstr "" 66 | 67 | #: templates/partials/filters.php:15 68 | msgid "Show more" 69 | msgstr "" 70 | 71 | #: templates/partials/filters.php:16 72 | msgid "Show less" 73 | msgstr "" 74 | 75 | #: templates/partials/loop.php:15 76 | msgid "Load more" 77 | msgstr "" 78 | 79 | #: templates/partials/loop.php:23 80 | #, php-format 81 | msgid "Oh, we couldn't find any %s" 82 | msgstr "" 83 | 84 | #: templates/partials/loop.php:24 85 | #, php-format 86 | msgid "Try different filters or reset them all." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-ajax-filter-posts", 3 | "version": "0.4.0", 4 | "description": "Filter and load more WordPress posts via AJAX", 5 | "main": "index.js", 6 | "scripts": { 7 | "create-pot": "mkdir -p ./languages && find . ./templates -iname '*.php' | xargs xgettext --add-comments=TRANSLATORS --force-po --from-code=UTF-8 --default-domain=ajax-filter-posts -k__ -k_e -k_n:1,2 -k_x:1,2c -k_ex:1,2c -k_nx:4c,12 -kesc_attr__ -kesc_attr_e -kesc_attr_x:1,2c -kesc_html__ -kesc_html_e -kesc_html_x:1,2c -k_n_noop:1,2 -k_nx_noop:3c,1,2, -k__ngettext_noop:1,2 -o languages/ajax-filter-posts.pot", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Robbertdk/wordpress-ajax-filter-posts.git" 13 | }, 14 | "keywords": [ 15 | "Wordpress", 16 | "AJAX", 17 | "posts", 18 | "filter", 19 | "load", 20 | "more", 21 | "pagination" 22 | ], 23 | "author": "Robbert de Kuiper", 24 | "license": " GPL-2.0-or-later", 25 | "bugs": { 26 | "url": "https://github.com/Robbertdk/wordpress-ajax-filter-posts/issues" 27 | }, 28 | "homepage": "https://github.com/Robbertdk/wordpress-ajax-filter-posts#readme" 29 | } 30 | -------------------------------------------------------------------------------- /templates/base.php: -------------------------------------------------------------------------------- 1 |
4 | id="ajax-posts-" 5 | data-id="" 6 | 7 | data-post-type="" 8 | data-post-status="" 9 | data-quantity="" 10 | data-multiselect="" 11 | data-orderby="" 12 | data-order="" 13 | > 14 | 15 | have_posts() && $query->post_count > 1) : ?> 16 | 21 | 22 |
23 | 28 |
29 | get_local_template('partials/loop.php') ); ?> 30 |
31 |
32 |
33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /templates/partials/filters.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 13 | = 5): ?> 14 |
15 | 16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /templates/partials/loop.php: -------------------------------------------------------------------------------- 1 | have_posts() ) : ?> 2 | have_posts() ): $query->the_post();?> 3 |
4 | 10 |
11 | 12 | is_last_page($query)) : ?> 13 |
14 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |

24 |

reset them all.', 'ajax-filter-posts'), 'href="#" class="js-reset-filters"'); ?>

25 |
26 | 27 | --------------------------------------------------------------------------------