| @@ -42,11 +42,19 @@ | ||
| 42 | 42 | * @param array $columns Existing columns. |
| 43 | 43 | * @return array |
| 44 | 44 | */ |
| 45 | 45 | function wpatt_add_files_column( $columns ) { |
| 46 | - $label = esc_html_x( 'Files', 'list table column heading', 'wp-attachments' ); | |
| 47 | - $out = array(); | |
| 46 | + $label = esc_html_x( 'Files', 'list table column heading', 'wp-attachments' ); | |
| 47 | + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; | |
| 48 | + $out = array(); | |
| 48 | 49 | |
| 50 | + // List switched off for the whole post type: say so once, here. | |
| 51 | + if ( $screen && $screen->post_type && ! wpatt_is_frontend_enabled( $screen->post_type ) ) { | |
| 52 | + $off = __( 'The list of files is not shown on the site for this content type.', 'wp-attachments' ); | |
| 53 | + $label = '<span class="wpa-files-heading-off" title="' . esc_attr( $off ) . '">' . $label . wpatt_eye_off_svg() | |
| 54 | + . '<span class="screen-reader-text">' . esc_html( $off ) . '</span></span>'; | |
| 55 | + } | |
| 56 | + | |
| 49 | 57 | foreach ( $columns as $key => $value ) { |
| 50 | 58 | if ( 'date' === $key ) { |
| 51 | 59 | $out['wpa_files'] = $label; |
| 52 | 60 | } |
| @@ -157,11 +165,51 @@ | ||
| 157 | 165 | esc_attr( $label ), |
| 158 | 166 | wpatt_paperclip_svg(), |
| 159 | 167 | esc_html( number_format_i18n( $count ) ) |
| 160 | 168 | ); |
| 169 | + | |
| 170 | + // Whether this post shows its list on the site, as set in its metabox. | |
| 171 | + // When the whole post type is off, every row would carry the same mark, | |
| 172 | + // so the column heading explains it once instead. | |
| 173 | + if ( ! wpatt_is_frontend_enabled( get_post_type( $post_id ) ) ) { | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + | |
| 177 | + $shown = wpatt_is_display_enabled( $post_id ); | |
| 178 | + $state = $shown ? __( 'Shown on the site', 'wp-attachments' ) : __( 'Not shown on the site', 'wp-attachments' ); | |
| 179 | + | |
| 180 | + printf( | |
| 181 | + '<span class="wpa-files-state %1$s" title="%2$s">%3$s<span class="screen-reader-text">%2$s</span></span>', | |
| 182 | + $shown ? 'is-shown' : 'is-hidden', | |
| 183 | + esc_attr( $state ), | |
| 184 | + $shown ? wpatt_eye_svg() : wpatt_eye_off_svg() | |
| 185 | + ); | |
| 161 | 186 | } |
| 162 | 187 | |
| 163 | 188 | /** |
| 189 | + * Eye: the list is shown on the frontend for this post. | |
| 190 | + */ | |
| 191 | +function wpatt_eye_svg() { | |
| 192 | + return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"' | |
| 193 | + . ' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' | |
| 194 | + . '<path d="M2.5 12S6 5.75 12 5.75 21.5 12 21.5 12 18 18.25 12 18.25 2.5 12 2.5 12Z"/>' | |
| 195 | + . '<circle cx="12" cy="12" r="2.75"/>' | |
| 196 | + . '</svg>'; | |
| 197 | +} | |
| 198 | + | |
| 199 | +/** | |
| 200 | + * Crossed-out eye: the list is hidden on the frontend for this post. | |
| 201 | + */ | |
| 202 | +function wpatt_eye_off_svg() { | |
| 203 | + return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"' | |
| 204 | + . ' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' | |
| 205 | + . '<path d="M10.6 5.8A9.8 9.8 0 0 1 12 5.75c6 0 9.5 6.25 9.5 6.25a17 17 0 0 1-2.3 3.1M6.5 7.4C3.9 9.1 2.5 12 2.5 12s3.5 6.25 9.5 6.25a9.6 9.6 0 0 0 4.6-1.15"/>' | |
| 206 | + . '<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/>' | |
| 207 | + . '<path d="M3 3l18 18"/>' | |
| 208 | + . '</svg>'; | |
| 209 | +} | |
| 210 | + | |
| 211 | +/** | |
| 164 | 212 | * Paperclip mark. Without it the cell is a bare digit that could be a count of |
| 165 | 213 | * anything; with it the column reads at a glance. |
| 166 | 214 | */ |
| 167 | 215 | function wpatt_paperclip_svg() { |
| @@ -207,8 +255,133 @@ | ||
| 207 | 255 | } |
| 208 | 256 | ); |
| 209 | 257 | |
| 210 | 258 | /** |
| 259 | + * Files filter above the list table, next to the date and category ones. | |
| 260 | + * | |
| 261 | + * A native select (it combines with the other filters and keeps working | |
| 262 | + * with the core Filter button), followed by a legend with the same icons the | |
| 263 | + * column uses -- options of a select cannot hold icons. | |
| 264 | + */ | |
| 265 | +add_action( | |
| 266 | + 'restrict_manage_posts', | |
| 267 | + function ( $post_type, $which = 'top' ) { | |
| 268 | + if ( 'top' !== $which || ! in_array( $post_type, wpatt_column_post_types(), true ) ) { | |
| 269 | + return; | |
| 270 | + } | |
| 271 | + | |
| 272 | + $frontend = wpatt_is_frontend_enabled( $post_type ); | |
| 273 | + $current = wpatt_files_filter_mode(); | |
| 274 | + | |
| 275 | + $options = array( | |
| 276 | + '' => __( 'All files', 'wp-attachments' ), | |
| 277 | + 'with' => __( 'With files', 'wp-attachments' ), | |
| 278 | + 'without' => __( 'Without files', 'wp-attachments' ), | |
| 279 | + ); | |
| 280 | + if ( $frontend ) { | |
| 281 | + $options['shown'] = __( 'Shown on the site', 'wp-attachments' ); | |
| 282 | + $options['hidden'] = __( 'Not shown on the site', 'wp-attachments' ); | |
| 283 | + } | |
| 284 | + | |
| 285 | + echo '<label for="wpa-files-filter" class="screen-reader-text">' . esc_html__( 'Filter by attached files', 'wp-attachments' ) . '</label>'; | |
| 286 | + echo '<select name="wpa_files" id="wpa-files-filter">'; | |
| 287 | + foreach ( $options as $value => $label ) { | |
| 288 | + printf( '<option value="%1$s"%2$s>%3$s</option>', esc_attr( $value ), selected( $current, $value, false ), esc_html( $label ) ); | |
| 289 | + } | |
| 290 | + echo '</select>'; | |
| 291 | + | |
| 292 | + // Decorative: the cells carry the same information as text. | |
| 293 | + echo '<span class="wpa-files-legend" aria-hidden="true">'; | |
| 294 | + echo '<span>' . wpatt_paperclip_svg() . esc_html__( 'Files', 'wp-attachments' ) . '</span>'; | |
| 295 | + if ( $frontend ) { | |
| 296 | + echo '<span>' . wpatt_eye_svg() . esc_html__( 'Shown', 'wp-attachments' ) . '</span>'; | |
| 297 | + echo '<span>' . wpatt_eye_off_svg() . esc_html__( 'Not shown', 'wp-attachments' ) . '</span>'; | |
| 298 | + } | |
| 299 | + echo '</span>'; | |
| 300 | + }, | |
| 301 | + 10, | |
| 302 | + 2 | |
| 303 | +); | |
| 304 | + | |
| 305 | +/** | |
| 306 | + * Current value of the Files filter, or '' when it is not in use. | |
| 307 | + * | |
| 308 | + * @return string | |
| 309 | + */ | |
| 310 | +function wpatt_files_filter_mode() { | |
| 311 | + $mode = isset( $_GET['wpa_files'] ) ? sanitize_key( wp_unslash( $_GET['wpa_files'] ) ) : ''; | |
| 312 | + | |
| 313 | + return in_array( $mode, array( 'with', 'without', 'shown', 'hidden' ), true ) ? $mode : ''; | |
| 314 | +} | |
| 315 | + | |
| 316 | +/** | |
| 317 | + * Apply the Files filter to the main list table query. | |
| 318 | + */ | |
| 319 | +add_action( | |
| 320 | + 'pre_get_posts', | |
| 321 | + function ( $query ) { | |
| 322 | + global $pagenow, $typenow; | |
| 323 | + | |
| 324 | + if ( ! is_admin() || ! $query->is_main_query() || 'edit.php' !== $pagenow ) { | |
| 325 | + return; | |
| 326 | + } | |
| 327 | + | |
| 328 | + $post_type = $typenow ? $typenow : 'post'; | |
| 329 | + $mode = wpatt_files_filter_mode(); | |
| 330 | + | |
| 331 | + if ( ! $mode || ! in_array( $post_type, wpatt_column_post_types(), true ) ) { | |
| 332 | + return; | |
| 333 | + } | |
| 334 | + // Shown / not shown mean nothing while the whole post type is off. | |
| 335 | + if ( in_array( $mode, array( 'shown', 'hidden' ), true ) && ! wpatt_is_frontend_enabled( $post_type ) ) { | |
| 336 | + return; | |
| 337 | + } | |
| 338 | + | |
| 339 | + $query->set( 'wpa_files_filter', $mode ); | |
| 340 | + | |
| 341 | + // Same rule as wpatt_is_display_enabled(): only '1' hides the list. | |
| 342 | + if ( 'hidden' === $mode || 'shown' === $mode ) { | |
| 343 | + $meta_query = (array) $query->get( 'meta_query' ); | |
| 344 | + $meta_query[] = ( 'hidden' === $mode ) | |
| 345 | + ? array( 'key' => 'wpa_off', 'value' => '1' ) | |
| 346 | + : array( | |
| 347 | + 'relation' => 'OR', | |
| 348 | + array( 'key' => 'wpa_off', 'compare' => 'NOT EXISTS' ), | |
| 349 | + array( 'key' => 'wpa_off', 'value' => '1', 'compare' => '!=' ), | |
| 350 | + ); | |
| 351 | + $query->set( 'meta_query', $meta_query ); | |
| 352 | + } | |
| 353 | + } | |
| 354 | +); | |
| 355 | + | |
| 356 | +/** | |
| 357 | + * "Has attachments" condition for the Files filter. | |
| 358 | + * | |
| 359 | + * Same rule as the column count: attachments that are not in the trash. | |
| 360 | + * Shown / not shown only cover posts with files, matching the eye icons. | |
| 361 | + */ | |
| 362 | +add_filter( | |
| 363 | + 'posts_where', | |
| 364 | + function ( $where, $query ) { | |
| 365 | + $mode = $query->get( 'wpa_files_filter' ); | |
| 366 | + if ( ! $mode ) { | |
| 367 | + return $where; | |
| 368 | + } | |
| 369 | + | |
| 370 | + global $wpdb; | |
| 371 | + | |
| 372 | + $exists = "EXISTS (SELECT 1 FROM {$wpdb->posts} AS wpa_att" | |
| 373 | + . " WHERE wpa_att.post_parent = {$wpdb->posts}.ID" | |
| 374 | + . " AND wpa_att.post_type = 'attachment'" | |
| 375 | + . " AND wpa_att.post_status != 'trash')"; | |
| 376 | + | |
| 377 | + return $where . ' AND ' . ( 'without' === $mode ? 'NOT ' : '' ) . $exists; | |
| 378 | + }, | |
| 379 | + 10, | |
| 380 | + 2 | |
| 381 | +); | |
| 382 | + | |
| 383 | +/** | |
| 211 | 384 | * Column styling. Loaded only on the list tables that show it. |
| 212 | 385 | */ |
| 213 | 386 | add_action( |
| 214 | 387 | 'admin_enqueue_scripts', |
| @@ -227,15 +400,24 @@ | ||
| 227 | 400 | wp_add_inline_style( |
| 228 | 401 | 'wpa-post-columns', |
| 229 | 402 | /* .widefat th wins over a bare .column-* selector, so the header |
| 230 | 403 | needs the extra specificity or it stays left aligned. */ |
| 231 | - '.widefat th.column-wpa_files,.widefat td.column-wpa_files{width:5.5em;text-align:center;} | |
| 404 | + '.widefat th.column-wpa_files,.widefat td.column-wpa_files{width:6.5em;text-align:center;} | |
| 232 | 405 | .wpa-files-count{font-variant-numeric:tabular-nums;} |
| 233 | 406 | .wpa-files-count.is-zero{color:#a7aaad;} |
| 234 | 407 | .wpa-files-link{display:inline-flex;align-items:center;gap:4px;text-decoration:none;font-variant-numeric:tabular-nums;} |
| 235 | 408 | .wpa-files-link svg{width:13px;height:13px;flex-shrink:0;opacity:.65;} |
| 236 | 409 | .wpa-files-link:hover svg,.wpa-files-link:focus svg{opacity:1;} |
| 410 | +.wpa-files-state,.wpa-files-heading-off{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;} | |
| 411 | +.wpa-files-state{margin-left:6px;} | |
| 412 | +.wpa-files-state.is-shown{color:#a7aaad;} | |
| 413 | +.wpa-files-state.is-hidden{color:#646970;} | |
| 414 | +.wpa-files-state svg,.wpa-files-heading-off svg{width:14px;height:14px;flex-shrink:0;} | |
| 415 | +.tablenav .wpa-files-legend{display:inline-flex;align-items:center;gap:10px;height:30px;margin:0 10px 0 2px;color:#646970;font-size:12px;vertical-align:middle;} | |
| 416 | +.wpa-files-legend>span{display:inline-flex;align-items:center;gap:3px;} | |
| 417 | +.wpa-files-legend svg{width:13px;height:13px;flex-shrink:0;} | |
| 237 | 418 | @media screen and (max-width:782px){ |
| 419 | +.tablenav .wpa-files-legend{display:none;} | |
| 238 | 420 | .widefat th.column-wpa_files,.widefat td.column-wpa_files{width:auto;text-align:left;} |
| 239 | 421 | .wpa-files-link{display:inline-flex;} |
| 240 | 422 | }' |
| 241 | 423 | ); |