├── LICENSE ├── composer.json ├── inc └── functions.php └── src ├── AdminUi.php ├── Archive.php ├── ArchiveType.php └── Bootstrap.php /LICENSE: -------------------------------------------------------------------------------- 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. 340 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inpsyde/cpt-archives", 3 | "description": "Allows a post-like editing experience for post type archives.", 4 | "type": "library", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Inpsyde GmbH", 9 | "email": "hello@inpsyde.com", 10 | "homepage": "http://inpsyde.com", 11 | "role": "Company" 12 | }, 13 | { 14 | "name": "Giuseppe Mazzapica", 15 | "email": "g.mazzapica@inpsyde.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "minimum-stability": "stable", 20 | "prefer-stable": true, 21 | "require": { 22 | "php": ">=7" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "CptArchives\\": "src/" 27 | } 28 | }, 29 | "config": { 30 | "optimize-autoloader": true 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "0.x-dev" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /inc/functions.php: -------------------------------------------------------------------------------- 1 | archive_title( $before, $after ); 30 | } 31 | 32 | /** 33 | * @param string|\WP_Post_Type|NULL $post_type 34 | * @param string|null $more_link_text 35 | * @param bool $strip_teaser 36 | * 37 | * @return string 38 | */ 39 | function archive_content( 40 | $post_type = null, 41 | string $more_link_text = null, 42 | bool $strip_teaser = false 43 | ): string { 44 | 45 | return Archive::for_type( $post_type ) 46 | ->archive_content( $more_link_text, $strip_teaser ); 47 | } 48 | 49 | /** 50 | * @param string|\WP_Post_Type|NULL $post_type 51 | * 52 | * @return string 53 | */ 54 | function archive_excerpt( $post_type = null ): string { 55 | 56 | return Archive::for_type( $post_type ) 57 | ->archive_excerpt(); 58 | } 59 | 60 | /** 61 | * @param string|\WP_Post_Type|NULL $post_type 62 | * 63 | * @return bool 64 | */ 65 | function archive_has_thumbnail( $post_type = null ): bool { 66 | 67 | return Archive::for_type( $post_type ) 68 | ->archive_has_thumbnail(); 69 | } 70 | 71 | /** 72 | * @param string|\WP_Post_Type|NULL $post_type 73 | * 74 | * @return int 75 | */ 76 | function archive_thumbnail_id( $post_type = null ): int { 77 | 78 | return Archive::for_type( $post_type ) 79 | ->archive_thumbnail_id(); 80 | } 81 | 82 | /** 83 | * @param string|\WP_Post_Type|NULL $post_type 84 | * @param string $size 85 | * @param array $attr 86 | * 87 | * @return string 88 | */ 89 | function archive_thumbnail( $post_type = null, string $size = 'post-thumbnail', array $attr = [] ): string { 90 | 91 | return Archive::for_type( $post_type ) 92 | ->archive_thumbnail( $size, $attr ); 93 | 94 | } 95 | 96 | /** 97 | * @param string|\WP_Post_Type|NULL $post_type 98 | * @param string $key 99 | * @param bool $single 100 | * 101 | * @return mixed 102 | */ 103 | function archive_meta( $post_type = null, string $key = '', bool $single = false ) { 104 | 105 | return Archive::for_type( $post_type ) 106 | ->archive_meta( $key, $single ); 107 | } -------------------------------------------------------------------------------- /src/AdminUi.php: -------------------------------------------------------------------------------- 1 | id, 2 ); 42 | $suffix = preg_quote( self::MENU_SUFFIX, '~' ); 43 | 44 | if ( ! preg_match( '~^page_(.+)?' . $suffix . '$~', end( $id_part ), $matches ) ) { 45 | return FALSE; 46 | } 47 | 48 | $type = $matches[ 1 ]; 49 | if ( ! post_type_exists( $type ) ) { 50 | return FALSE; 51 | } 52 | 53 | $type_obj = get_post_type_object( $type ); 54 | if ( ! $type_obj || ! $type_obj->has_archive ) { 55 | return FALSE; 56 | } 57 | 58 | $page = filter_input( INPUT_GET, 'page' ); 59 | if ( ! preg_match( '~^(.+)?' . $suffix . '$~', $page, $matches ) ) { 60 | return FALSE; 61 | } 62 | 63 | if ( $matches[ 1 ] === $type_obj->name ) { 64 | self::$target_post_type = $type_obj; 65 | 66 | return TRUE; 67 | } 68 | 69 | return FALSE; 70 | } 71 | 72 | /** 73 | * Run all the setup tasks on admin screen. 74 | * 75 | * @return bool 76 | */ 77 | public function setup(): bool { 78 | 79 | if ( ! is_admin() || is_network_admin() || is_user_admin() ) { 80 | return FALSE; 81 | } 82 | 83 | return 84 | $this->setup_menu_pages() 85 | && $this->remove_slug_metabox() 86 | && $this->fix_delete_text() 87 | && $this->fix_side_metaboxes() 88 | && $this->fix_post_messages(); 89 | } 90 | 91 | /** 92 | * Adds entry to admin menu. 93 | * 94 | * There's a parent entry, plus a submenu entry for each post type. 95 | * Parent entry will be linked to first submenu entry, like usual in wordPress. 96 | * However, when there's just one post type, the menu will show just the parent item. This is due to a limitation of 97 | * WordPress that otherwise would add a submenu entry to match parent entry, which would be broken. 98 | * 99 | * @return bool 100 | */ 101 | private function setup_menu_pages(): bool { 102 | 103 | $target_post_types = ArchiveType::target_post_types(); 104 | 105 | if ( ! $target_post_types ) { 106 | return FALSE; 107 | } 108 | 109 | $done_count = 0; 110 | $types_count = count( $target_post_types ); 111 | 112 | /* Translators: 1 is the CPT label, e.g. "Products" */ 113 | $type_label = esc_html__( '%1$s Archive', 'cpt-archives' ); 114 | 115 | $f_type = array_shift( $target_post_types ); 116 | $f_slug = $f_type->name . self::MENU_SUFFIX; 117 | $f_title = sprintf( $type_label, $f_type->labels->name ); 118 | $f_capability = apply_filters( self::FILTER_CAPABILITY, $f_type->cap->edit_others_posts, $f_type ); 119 | $parent_title = $types_count > 1 ? esc_html__( 'CPT Archives', 'cpt-archives' ) : $f_title; 120 | 121 | add_menu_page( $parent_title, $parent_title, 'edit_posts', $f_slug, '', 'dashicons-archive' ); 122 | 123 | $success = $this->setup_menu_page( $f_slug, $f_slug, $f_title, (string) $f_capability, $f_type ); 124 | if ( ! $success ) { 125 | 126 | remove_menu_page( $f_slug ); 127 | 128 | return FALSE; 129 | } 130 | 131 | $done_count ++; 132 | 133 | /** @var \WP_Post_Type $post_type */ 134 | foreach ( $target_post_types as $post_type ) { 135 | $type_title = sprintf( $type_label, $post_type->labels->name ); 136 | $capability = apply_filters( self::FILTER_CAPABILITY, $post_type->cap->edit_others_posts, $post_type ); 137 | $slug = $post_type->name . self::MENU_SUFFIX; 138 | $success = $this->setup_menu_page( $f_slug, $slug, $type_title, (string) $capability, $post_type ); 139 | $success and $done_count ++; 140 | } 141 | 142 | return $done_count === $types_count; 143 | 144 | } 145 | 146 | /** 147 | * Add submenu entry with given arguments. 148 | * 149 | * Use this `render_menu_page()` method as me page callback. 150 | * 151 | * @param string $parent 152 | * @param string $slug 153 | * @param string $title 154 | * @param string $capability 155 | * @param \WP_Post_Type $cpt 156 | * 157 | * @return bool 158 | * 159 | * @see AdminUI::render_menu_page() 160 | */ 161 | private function setup_menu_page( 162 | string $parent, 163 | string $slug, 164 | string $title, 165 | string $capability, 166 | \WP_Post_Type $cpt 167 | ): bool { 168 | 169 | return (bool) add_submenu_page( 170 | $parent, 171 | $title, 172 | $title, 173 | $capability, 174 | $slug, 175 | function () use ( $cpt, $capability ) { 176 | 177 | $this->render_menu_page( [ $cpt->name . self::MENU_SUFFIX => [ $capability, $cpt ] ] ); 178 | } 179 | ); 180 | } 181 | 182 | /** 183 | * Render callback for menu pages. 184 | * 185 | * After having sanity checks, setup global variables and require core `edit-form-advanced.php`. 186 | * 187 | * @param array $type_data 188 | */ 189 | private function render_menu_page( array $type_data ) { 190 | 191 | if ( ! self::is_archive_ui_screen() ) { 192 | return; 193 | } 194 | 195 | $key = self::$target_post_type->name . self::MENU_SUFFIX; 196 | 197 | if ( empty( $type_data[ $key ] ) ) { 198 | return; 199 | } 200 | 201 | /** 202 | * @var string $capability 203 | * @var \WP_Post_Type $target_type_object 204 | */ 205 | list( $capability, $target_type_object ) = $type_data[ $key ]; 206 | 207 | if ( ! current_user_can( $capability ) || $target_type_object->name !== self::$target_post_type->name ) { 208 | $this->bail_on_render(); 209 | 210 | return; 211 | } 212 | 213 | self::$target_post_type = $target_type_object; 214 | 215 | global $post_type, $post_type_object, $post, $post_ID, $action, $typenow, $is_IE, $title; 216 | 217 | $post_id = Archive::for_type( $target_type_object ) 218 | ->archive_post_id(); 219 | 220 | if ( ! $post_id || ! ( $post = get_post( $post_id ) ) ) { 221 | $this->bail_on_render(); 222 | 223 | return; 224 | } 225 | 226 | $post_type_object = get_post_type_object( ArchiveType::SLUG ); 227 | $post_type = $typenow = $post_type_object->name; 228 | $post_ID = $post_id; 229 | $action = 'edit'; 230 | 231 | $post_type_object->publicly_queryable = TRUE; 232 | $post_type_object->public = TRUE; 233 | 234 | // Change public from false only when needed to show archive link under title 235 | add_filter( 'post_updated_messages', function ( $messages ) use ( $post_type_object ) { 236 | $post_type_object->publicly_queryable = FALSE; 237 | $post_type_object->public = FALSE; 238 | add_action( 'edit_form_top', function () use ( $post_type_object ) { 239 | $post_type_object->publicly_queryable = TRUE; 240 | $post_type_object->public = TRUE; 241 | add_action( 'edit_form_after_title', function () use ( $post_type_object ) { 242 | $post_type_object->publicly_queryable = FALSE; 243 | $post_type_object->public = FALSE; 244 | } ); 245 | } ); 246 | 247 | return $messages; 248 | } ); 249 | 250 | require_once ABSPATH . 'wp-admin/edit-form-advanced.php'; 251 | } 252 | 253 | /** 254 | * Remove the metabox for post slug, because unnecessary. 255 | * 256 | * @return bool 257 | */ 258 | private function remove_slug_metabox(): bool { 259 | 260 | return (bool) add_action( 'add_meta_boxes', function ( $post_type ) { 261 | if ( $post_type === ArchiveType::SLUG ) { 262 | remove_meta_box( 'slugdiv', get_current_screen(), 'normal' ); 263 | } 264 | } ); 265 | } 266 | 267 | /** 268 | * WordPress triggers metaboxes via `do_meta_boxes()` but for "side" metaboxes it used the post type name 269 | * as first argument (used for key of `$wp_meta_boxes`). for other contexts it uses `null` that is then replaced 270 | * with current screen id. 271 | * For core this is not an issue, because screen id and post type matches, but for us screen id is very different 272 | * than post type name, so we need to normalize "side" metaboxes to use post type name and all the others to use 273 | * screen id. 274 | * 275 | * @return bool 276 | */ 277 | private function fix_side_metaboxes(): bool { 278 | 279 | return (bool) add_action( 'edit_form_after_editor', function () { 280 | 281 | if ( ! self::is_archive_ui_screen() ) { 282 | return; 283 | } 284 | 285 | $screen = get_current_screen(); 286 | global $wp_meta_boxes; 287 | 288 | $cpt_boxes = $wp_meta_boxes[ ArchiveType::SLUG ] ?? []; 289 | $cpt_boxes_side = $cpt_boxes[ 'side' ] ?? []; 290 | unset( $cpt_boxes[ 'side' ], $wp_meta_boxes[ ArchiveType::SLUG ][ 'side' ] ); 291 | 292 | $screen_boxes = $wp_meta_boxes[ $screen->id ]; 293 | $screen_boxes_side = $screen_boxes[ 'side' ] ?? []; 294 | unset( $screen_boxes[ 'side' ], $wp_meta_boxes[ $screen->id ][ 'side' ] ); 295 | 296 | $wp_meta_boxes[ ArchiveType::SLUG ][ 'side' ] = array_merge_recursive( 297 | $cpt_boxes_side, 298 | $screen_boxes_side 299 | ); 300 | 301 | foreach ( $cpt_boxes as $context => $context_cpt_boxes ) { 302 | 303 | $screen_boxes[ $context ] = array_key_exists( $context, $screen_boxes ) 304 | ? array_merge_recursive( $context_cpt_boxes, $screen_boxes[ $context ] ) 305 | : $context_cpt_boxes; 306 | 307 | unset( $wp_meta_boxes[ ArchiveType::SLUG ][ $context ] ); 308 | } 309 | 310 | $wp_meta_boxes[ $screen->id ] = $screen_boxes; 311 | } ); 312 | } 313 | 314 | /** 315 | * We don't support trash, and there's no way to change the wording in submit box other than acting on gettext. 316 | */ 317 | private function fix_delete_text(): bool { 318 | 319 | $fix = function ( $translation, $text, $domain ) { 320 | 321 | static $doing; 322 | if ( 323 | ! $doing 324 | && $domain === 'default' 325 | && ( $text === 'Move to Trash' || $text === 'Delete Permanently' ) 326 | ) { 327 | $doing = TRUE; 328 | $translation = esc_html__( 'Reset all the data.', 'cpt-archives' ); 329 | } 330 | 331 | return $translation; 332 | }; 333 | 334 | add_action( 'post_submitbox_start', function () use ( $fix ) { 335 | if ( self::is_archive_ui_screen() ) { 336 | add_filter( 'gettext', $fix, 10, 3 ); 337 | } 338 | } ); 339 | 340 | add_action( 'add_meta_boxes', function () use ( $fix ) { 341 | if ( self::is_archive_ui_screen() ) { 342 | remove_filter( 'gettext', $fix, 10 ); 343 | } 344 | }, 0 ); 345 | 346 | return TRUE; 347 | } 348 | 349 | /** 350 | * By default, WordPress prints in `edit-form-advanced.php` messages like "Post updated" or "View post" that can 351 | * be misleading, let's replace them using "Archive", e.g. "Archive updated" or "Archive post". 352 | * 353 | * @return bool 354 | */ 355 | private function fix_post_messages(): bool { 356 | 357 | return (bool) add_filter( 'post_updated_messages', function ( array $messages ) { 358 | 359 | if ( ! self::is_archive_ui_screen() ) { 360 | return $messages; 361 | } 362 | 363 | global $post_ID, $post; 364 | $permalink = esc_url( get_permalink( $post_ID ) ? : '' ); 365 | $preview_url = esc_url( get_preview_post_link( $post ) ); 366 | $scheduled_date = date_i18n( __( 'M j, Y @ H:i' ), strtotime( $post->post_date ) ); 367 | 368 | $link_format_blank = ' %2$s'; 369 | $link_format = ' %2$s'; 370 | 371 | $preview = esc_html__( 'Preview archive', 'cpt-archives' ); 372 | $view = esc_html__( 'View archive', 'cpt-archives' ); 373 | $archive_updated = esc_html__( 'Archive updated.', 'cpt-archives' ); 374 | $archive_restored = esc_html__( 'Archive restored to revision from %s.', 'cpt-archives' ); 375 | $archive_published = esc_html__( 'Archive published.', 'cpt-archives' ); 376 | $archive_saved = esc_html__( 'Archive saved.', 'cpt-archives' ); 377 | $archive_scheduled = esc_html__( 'Archive scheduled for: %s.', 'cpt-archives' ); 378 | $archive_submitted = esc_html__( 'Archive submitted.', 'cpt-archives' ); 379 | $cf_updated = __( 'Custom field updated.' ); 380 | 381 | $preview_link_html = sprintf( $link_format_blank, esc_url( $preview_url ), $preview ); 382 | $scheduled_link_html = sprintf( $link_format_blank, $permalink, $preview ); 383 | $view_link_html = sprintf( $link_format, $permalink, $view ); 384 | 385 | $messages[ 'post' ] = [ 386 | '', 387 | $archive_updated . $view_link_html, 388 | $cf_updated, 389 | $cf_updated, 390 | $archive_updated, 391 | isset( $_GET[ 'revision' ] ) 392 | ? sprintf( $archive_restored, wp_post_revision_title( (int) $_GET[ 'revision' ], FALSE ) ) 393 | : FALSE, 394 | $archive_published . $view_link_html, 395 | $archive_saved, 396 | $archive_submitted . $preview_link_html, 397 | sprintf( $archive_scheduled, "{$scheduled_date}" ) . $scheduled_link_html, 398 | esc_html__( 'Archive draft updated.', 'cpt-archives' ) . $preview_link_html, 399 | ]; 400 | 401 | return $messages; 402 | } ); 403 | } 404 | 405 | /** 406 | * Prints an error message if it is not possible to properly render the edit screen. 407 | * 408 | * @return bool 409 | */ 410 | private function bail_on_render(): bool { 411 | 412 | echo '

' . esc_html__( 'Something went wrong...', 'cpt-archives' ) . '

'; 413 | echo '

' . esc_html__( 'Sorry, it was not possible to find an archive to edit.', 'cpt-archives' ) . '

'; 414 | 415 | return FALSE; 416 | } 417 | } -------------------------------------------------------------------------------- /src/Archive.php: -------------------------------------------------------------------------------- 1 | name : $type; 46 | is_string( $type_name ) or $type_name = ''; 47 | 48 | if ( ! $type_name ) { 49 | return new static(); 50 | } 51 | 52 | if ( array_key_exists( $type_name, self::$instances ) ) { 53 | return self::$instances[ $type_name ]; 54 | } 55 | 56 | $post_type = get_post_type_object( $type_name ); 57 | 58 | if ( ! $post_type || ! $post_type->has_archive ) { 59 | return new static(); 60 | } 61 | 62 | $valid_types = ArchiveType::target_post_types(); 63 | 64 | if ( ! array_key_exists( $type_name, $valid_types ) ) { 65 | return new static(); 66 | } 67 | 68 | $posts = get_posts( 69 | [ 70 | 'post_type' => ArchiveType::SLUG, 71 | 'post_status' => [ 'draft', 'publish', 'future', 'pending', 'private' ], 72 | 'posts_per_page' => 1, 73 | 'meta_key' => self::TARGET_TYPE_KEY, 74 | 'meta_value' => $type_name, 75 | ] 76 | ); 77 | 78 | if ( $posts ) { 79 | self::$instances[ $type_name ] = new static( reset( $posts ) ); 80 | 81 | return self::$instances[ $type_name ]; 82 | } 83 | 84 | $insert = wp_insert_post( 85 | [ 86 | 'post_type' => ArchiveType::SLUG, 87 | 'post_name' => $type_name, 88 | 'post_status' => 'draft', 89 | 'post_title' => apply_filters( 'post_type_archive_title', $post_type->labels->name, $post_type ), 90 | 'meta_input' => [ 91 | self::TARGET_TYPE_KEY => $type_name, 92 | ], 93 | ] 94 | ); 95 | 96 | if ( $insert && ! is_wp_error( $insert ) && ( $post = get_post( $insert ) ) ) { 97 | self::$instances[ $type_name ] = new static( $post ); 98 | 99 | return self::$instances[ $type_name ]; 100 | } 101 | 102 | return new static(); 103 | } 104 | 105 | /** 106 | * Named constructor that build an instance of archive API object for current post type when in post type archive 107 | * pages. 108 | * 109 | * When no internal post exists yet for given type, it is created. 110 | * 111 | * @return Archive 112 | */ 113 | public static function for_current_type(): Archive { 114 | 115 | if ( ! is_post_type_archive() ) { 116 | return new static(); 117 | } 118 | 119 | global $wp_query; 120 | $post_type = $wp_query->get( 'post_type' ); 121 | is_array( $post_type ) and $post_type = reset( $post_type ); 122 | 123 | return self::for_type( (string) $post_type ); 124 | } 125 | 126 | /** 127 | * Disabled on purpose, use named constructors. 128 | * 129 | * @param \WP_Post $post 130 | */ 131 | public function __construct( \WP_Post $post = null ) { 132 | 133 | $this->post = $post; 134 | } 135 | 136 | /** 137 | * Return the internal WordPress post object. 138 | * 139 | * @return int 140 | */ 141 | public function archive_post_id(): int { 142 | 143 | return $this->post ? (int) $this->post->ID : 0; 144 | } 145 | 146 | /** 147 | * @return string 148 | */ 149 | public function target_type(): string { 150 | 151 | $type = get_post_meta( $this->post->ID, self::TARGET_TYPE_KEY, TRUE ); 152 | if ( ! $type || ! post_type_exists( $type ) ) { 153 | return ''; 154 | } 155 | 156 | return $type; 157 | } 158 | 159 | /** 160 | * Returns the title for the archive. 161 | * 162 | * @param string $before 163 | * @param string $after 164 | * 165 | * @return string 166 | */ 167 | public function archive_title( string $before = '', string $after = '' ): string { 168 | 169 | if ( ! $this->is_valid() ) { 170 | return ''; 171 | } 172 | 173 | $title = get_the_title( $this->post ); 174 | 175 | if ( strlen( $title ) == 0 ) { 176 | return ''; 177 | } 178 | 179 | return $before . $title . $after; 180 | } 181 | 182 | /** 183 | * Returns the content for the archive. 184 | * 185 | * @param string|null $more_link_text 186 | * @param bool $strip_teaser 187 | * 188 | * @return string 189 | */ 190 | public function archive_content( string $more_link_text = null, bool $strip_teaser = FALSE ): string { 191 | 192 | if ( ! $this->is_valid() ) { 193 | return ''; 194 | } 195 | 196 | setup_postdata( $this->post ); 197 | ob_start(); 198 | the_content( $more_link_text, $strip_teaser ); 199 | $content = ob_get_clean(); 200 | wp_reset_postdata(); 201 | 202 | return $content; 203 | } 204 | 205 | /** 206 | * Returns the excerpt for the archive. 207 | * 208 | * @return string 209 | */ 210 | public function archive_excerpt(): string { 211 | 212 | if ( ! $this->is_valid() ) { 213 | return ''; 214 | } 215 | 216 | setup_postdata( $this->post ); 217 | ob_start(); 218 | the_excerpt(); 219 | $excerpt = ob_get_clean(); 220 | wp_reset_postdata(); 221 | 222 | return $excerpt; 223 | } 224 | 225 | /** 226 | * Returns true when a thumbnail is available for the archive. 227 | * 228 | * @return bool 229 | */ 230 | public function archive_has_thumbnail(): bool { 231 | 232 | return $this->is_valid() 233 | ? has_post_thumbnail( $this->post ) 234 | : FALSE; 235 | } 236 | 237 | /** 238 | * Returns the archive thumbnail id or `0` when no thumbnail. 239 | * 240 | * @return int 241 | */ 242 | public function archive_thumbnail_id(): int { 243 | 244 | return $this->archive_has_thumbnail() ? (int) get_post_thumbnail_id( $this->post ) : 0; 245 | } 246 | 247 | /** 248 | * Returns the archive thumbnail `img` tag or empty string when no thumbnail. 249 | * 250 | * @param string $size 251 | * @param array $attr 252 | * 253 | * @return string 254 | */ 255 | public function archive_thumbnail( string $size = 'post-thumbnail', array $attr = [] ): string { 256 | 257 | $thumb = $this->archive_thumbnail_id(); 258 | 259 | if ( ! $thumb ) { 260 | return ''; 261 | } 262 | 263 | return get_the_post_thumbnail( $this->post, $size, $attr ); 264 | } 265 | 266 | /** 267 | * Returns a custom field value for the archive. 268 | * 269 | * @param string $key 270 | * @param bool $single 271 | * 272 | * @return mixed 273 | */ 274 | public function archive_meta( string $key = '', bool $single = FALSE ) { 275 | 276 | if ( ! $this->is_valid( [ 'draft', 'publish', 'future', 'pending', 'private' ] ) ) { 277 | return $single && $key ? FALSE : []; 278 | } 279 | 280 | if ( ! $key ) { 281 | return get_post_custom( $this->post->ID ); 282 | } 283 | 284 | return get_post_meta( $this->post->ID, $key, $single ); 285 | } 286 | 287 | /** 288 | * @param array $allowed_statuses 289 | * 290 | * @return bool 291 | */ 292 | private function is_valid( array $allowed_statuses = [ 'publish' ] ): bool { 293 | 294 | return $this->post && $this->post->ID && in_array( $this->post->post_status, $allowed_statuses ); 295 | } 296 | 297 | } -------------------------------------------------------------------------------- /src/ArchiveType.php: -------------------------------------------------------------------------------- 1 | TRUE, 45 | 'publicly_queryable' => TRUE, 46 | '_builtin' => FALSE 47 | ], 48 | 'objects' 49 | ); 50 | 51 | $allowed_types = (array) apply_filters( self::FILTER_ALLOWED_CPTS, $types ); 52 | $allowed_types and $allowed_types = array_filter( 53 | $allowed_types, 54 | function ( $type ) { 55 | 56 | return 57 | $type instanceof \WP_Post_Type 58 | && $type->has_archive 59 | && $type->publicly_queryable 60 | && $type->_builtin === FALSE 61 | && ! in_array( $type->name, self::CORE_TYPES, TRUE ); 62 | } 63 | ); 64 | 65 | return $allowed_types; 66 | } 67 | 68 | /** 69 | * Setup the post type and other necessary task to make it work. 70 | * 71 | * @return bool 72 | */ 73 | public function setup(): bool { 74 | 75 | if ( $this->register_post_type() ) { 76 | $this->filter_urls(); 77 | 78 | return TRUE; 79 | } 80 | 81 | return FALSE; 82 | } 83 | 84 | /** 85 | * Register archive post type. 86 | * 87 | * Many post type arguments are filterable, some other are enforced. 88 | * 89 | * @return bool 90 | */ 91 | private function register_post_type(): bool { 92 | 93 | $args = [ 94 | 'label' => __( 'CPT Archives', 'cpt-archives' ), 95 | 'labels' => [ 96 | 'name' => __( 'CPT Archives', 'cpt-archives' ), 97 | 'singular_name' => __( 'CPT Archive', 'cpt-archives' ), 98 | 'add_new_item' => __( 'Add new CPT Archive', 'cpt-archives' ), 99 | 'edit_item' => __( 'EditCPT Archive', 'cpt-archives' ), 100 | 'new_item' => __( 'New CPT Archive', 'cpt-archives' ), 101 | 'view_item' => __( 'View CPT Archive', 'cpt-archives' ), 102 | 'view_items' => __( 'View CPT Archives', 'cpt-archives' ), 103 | 'search_items' => __( 'Search CPT Archives', 'cpt-archives' ), 104 | 'not_found' => __( 'No CPT Archives found.', 'cpt-archives' ), 105 | 'not_found_in_trash' => __( 'No CPT Archives found in trash.', 'cpt-archives' ), 106 | 'parent_item_colon' => __( 'Parent CPT Archive:', 'cpt-archives' ), 107 | 'all_items' => __( 'All CPT Archives', 'cpt-archives' ), 108 | 'archives' => __( 'CPT Archive archives', 'cpt-archives' ), 109 | 'attributes' => __( 'CPT Archive attributes', 'cpt-archives' ), 110 | 'insert_into_item' => __( 'Insert into CPT Archive', 'cpt-archives' ), 111 | 'uploaded_to_this_item' => __( 'Uploaded to this CPT Archive', 'cpt-archives' ), 112 | ], 113 | 114 | 'capability_type' => 'post', 115 | 'map_meta_cap' => TRUE, 116 | 'capabilities' => [ 'create_posts' => 'do_not_allow' ], 117 | 'supports' => [ 118 | 'title', 119 | 'editor', 120 | 'thumbnail', 121 | 'excerpt', 122 | 'custom-fields', 123 | 'revisions', 124 | ], 125 | 'show_in_nav_menus' => TRUE, 126 | 'show_in_rest' => FALSE, 127 | ]; 128 | 129 | $args = (array) apply_filters( self::FILTER_ARGS, $args ); 130 | 131 | $forced = [ 132 | 'public' => FALSE, 133 | 'hierarchical' => FALSE, 134 | 'exclude_from_search' => TRUE, 135 | 'publicly_queryable' => FALSE, 136 | 'show_ui' => FALSE, 137 | 'show_in_menu' => FALSE, 138 | 'show_in_admin_bar' => FALSE, 139 | 'has_archive' => FALSE, 140 | 'rewrite' => FALSE, 141 | 'query_var' => FALSE, 142 | 'permalink_epmask' => EP_NONE, 143 | 'delete_with_user' => FALSE, 144 | ]; 145 | 146 | $registered = register_post_type( self::SLUG, array_merge( $args, $forced ) ); 147 | 148 | return $registered && ! is_wp_error( $registered ); 149 | } 150 | 151 | /** 152 | * Archive post type is not a _real_ post type, and its URL must be replaced according to context. 153 | */ 154 | private function filter_urls() { 155 | 156 | add_action( 157 | 'wp_insert_post', 158 | function ( $post_id, \WP_Post $post ) { 159 | 160 | if ( $post->post_type !== self::SLUG ) { 161 | return; 162 | } 163 | 164 | $target_type = get_post_meta( $post_id, Archive::TARGET_TYPE_KEY, TRUE ); 165 | if ( ! $target_type || ! post_type_exists( $target_type ) ) { 166 | return; 167 | } 168 | 169 | $post_type = get_post_type_object( $post->post_type ); 170 | $post_type->_edit_link = add_query_arg( 171 | [ 'page' => $target_type . AdminUi::MENU_SUFFIX ], 172 | 'admin.php' 173 | ); 174 | }, 175 | 0, 176 | 2 177 | ); 178 | 179 | add_filter( 180 | 'get_delete_post_link', 181 | function ( $link, $post_id ) { 182 | 183 | $post = get_post( $post_id ); 184 | 185 | if ( ! $post || $post->post_type !== self::SLUG ) { 186 | return $link; 187 | } 188 | 189 | $target_type = get_post_meta( $post_id, Archive::TARGET_TYPE_KEY, TRUE ); 190 | if ( ! $target_type || ! post_type_exists( $target_type ) ) { 191 | return $link; 192 | } 193 | 194 | $delete_link = add_query_arg( 195 | [ 'post' => $post_id, 'action' => 'delete', ], 196 | 'post.php' 197 | ); 198 | 199 | return wp_nonce_url( $delete_link, "delete-post_{$post_id}" ); 200 | }, 201 | 99, 202 | 2 203 | ); 204 | 205 | add_filter( 206 | 'post_type_link', 207 | function ( $post_link, \WP_Post $post ) { 208 | 209 | if ( $post->post_type !== ArchiveType::SLUG ) { 210 | return $post_link; 211 | } 212 | 213 | $target_type = get_post_meta( $post->ID, Archive::TARGET_TYPE_KEY, TRUE ); 214 | 215 | if ( ! $target_type ) { 216 | return $post_link; 217 | } 218 | 219 | return get_post_type_archive_link( $target_type ) ? : home_url(); 220 | }, 221 | 99, 222 | 2 223 | ); 224 | 225 | add_filter( 226 | 'preview_post_link', 227 | function ( $preview_link, \WP_Post $post ) { 228 | 229 | if ( $post->post_type !== ArchiveType::SLUG ) { 230 | return $preview_link; 231 | } 232 | 233 | $target_type = get_post_meta( $post->ID, Archive::TARGET_TYPE_KEY, TRUE ); 234 | 235 | if ( ! $target_type ) { 236 | return $preview_link; 237 | } 238 | 239 | return get_post_type_archive_link( $target_type ) ? : home_url(); 240 | }, 241 | 99, 242 | 2 243 | ); 244 | 245 | add_filter( 246 | 'get_sample_permalink', 247 | function ( $permalink, $post_id, $title, $name, \WP_Post $post ) { 248 | 249 | if ( $post->post_type !== ArchiveType::SLUG ) { 250 | return $permalink; 251 | } 252 | 253 | $target_type = get_post_meta( $post->ID, Archive::TARGET_TYPE_KEY, TRUE ); 254 | 255 | if ( ! $target_type ) { 256 | return $permalink; 257 | } 258 | 259 | return [ get_post_type_archive_link( $target_type ) ? : home_url(), $target_type ]; 260 | }, 261 | 99, 262 | 5 263 | ); 264 | 265 | } 266 | 267 | } -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | core(); 40 | is_admin() ? $instance->backend() : $instance->frontend(); 41 | 42 | return TRUE; 43 | } 44 | 45 | /** 46 | * Do core bootstrap tasks. 47 | */ 48 | private function core() { 49 | 50 | require_once dirname( __DIR__ ) . '/inc/functions.php'; 51 | 52 | $locale = apply_filters( 'plugin_locale', get_user_locale(), 'cpt-archives' ); 53 | $path = dirname( __DIR__ ) . '/languages'; 54 | if ( is_readable( "{$path}/cpt-archives-{$locale}.mo" ) ) { 55 | load_textdomain( 'cpt-archives', "{$path}/cpt-archives-{$locale}.mo" ); 56 | } 57 | 58 | add_action( 'init', [ new ArchiveType(), 'setup' ] ); 59 | } 60 | 61 | /** 62 | * Do bootstrap tasks for backend. 63 | */ 64 | private function backend() { 65 | 66 | add_action( 'admin_menu', [ new AdminUi(), 'setup' ] ); 67 | 68 | add_action( 'current_screen', function ( \WP_Screen $screen ) { 69 | if ( AdminUi::is_archive_ui_screen() ) { 70 | $screen->post_type = ArchiveType::SLUG; 71 | } 72 | } ); 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Do bootstrap tasks for frontend. 79 | */ 80 | private function frontend() { 81 | 82 | add_filter( 83 | 'post_type_archive_title', 84 | function ( $value ) { 85 | 86 | $title = archive_title(); 87 | if ( $title ) { 88 | add_filter( 'get_the_archive_title', function ( $archive_title ) use ( $title ) { 89 | if ( $archive_title === sprintf( __( 'Archives: %s' ), $title ) ) { 90 | $archive_title = $title; 91 | } 92 | 93 | return $archive_title; 94 | } ); 95 | } 96 | 97 | return $title ? : $value; 98 | } 99 | ); 100 | 101 | add_filter( 102 | 'get_the_archive_description', 103 | function ( $value ) { 104 | 105 | return archive_excerpt() ? : $value; 106 | } 107 | ); 108 | 109 | add_action( 'admin_bar_menu', function ( \WP_Admin_Bar $wp_admin_bar ) { 110 | if ( ! is_post_type_archive() ) { 111 | return; 112 | } 113 | 114 | $archive = Archive::for_current_type(); 115 | $post_id = $archive->archive_post_id(); 116 | if ( $post_id && current_user_can( 'edit_post', $post_id ) ) { 117 | $wp_admin_bar->add_menu( 118 | [ 119 | 'id' => 'edit', 120 | 'title' => sprintf( __( 'Edit Archive', 'cpt-archives' ) ), 121 | 'href' => add_query_arg( 122 | [ 'page' => $archive->target_type() . AdminUi::MENU_SUFFIX, ], 123 | admin_url( 'admin.php' ) 124 | ) 125 | ] 126 | ); 127 | } 128 | }, 80 ); 129 | } 130 | 131 | } --------------------------------------------------------------------------------