| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — Recycle Bin: window registration. |
| 4 |
* |
| 5 |
* Native window with id `desktop-mode-recycle-bin`, pinned to the dock. |
| 6 |
* Like the code editor, the template body is a static skeleton that the |
| 7 |
* JS bundle enhances on first open — the table is populated from the |
| 8 |
* REST list endpoint at render time. |
| 9 |
* |
| 10 |
* The bin lands on the dock and nowhere else. It used to also register |
| 11 |
* a wallpaper icon, which put the same target on two surfaces at once |
| 12 |
* and made the desktop something the shell furnished rather than |
| 13 |
* something the user did. That is a default, not a rule: the tile is |
| 14 |
* `placeable`, so the wallpaper is one pick away in Apps & Plugins. |
| 15 |
* |
| 16 |
* The registration is filterable via `openstation_recycle_bin_window_args` |
| 17 |
* so a plugin can swap the icon, change the dimensions, or restrict who |
| 18 |
* sees the bin without touching this file. |
| 19 |
* |
| 20 |
* @package OpenStation |
| 21 |
*/ |
| 22 |
|
| 23 |
defined( 'ABSPATH' ) || exit; |
| 24 |
|
| 25 |
/** |
| 26 |
* The bin SVG, used by the window icon and its dock tile. |
| 27 |
* |
| 28 |
* The bin used to be `dashicons-trash`, which worked but wore the |
| 29 |
* wrong clothes. Dashicons are WP core's icon set: solid fills on a |
| 30 |
* 20-unit grid, tuned for admin-menu sizes. The shell's own icons are |
| 31 |
* outlined vessels on a 64-unit grid at stroke 3. Sitting next to |
| 32 |
* WP Explorer, Corkboard and Games in the dock, the Dashicon was |
| 33 |
* visibly a guest from another system: heavier, tighter, and drawn to |
| 34 |
* a different rhythm. |
| 35 |
* |
| 36 |
* So this is the same object, redrawn to the house rule the other |
| 37 |
* three follow: an outlined vessel with solid content, three elements |
| 38 |
* because it renders as small as 20px in the dock. The lid is the |
| 39 |
* solid one, which gives the mark a single dense horizontal to be |
| 40 |
* recognised by when the tapered body below it thins out. |
| 41 |
* |
| 42 |
* Drawn in `currentColor`, so `renderIcon()` paints it as a CSS mask |
| 43 |
* and it takes the surface's own text colour. Dashicons already |
| 44 |
* inherited colour, being font glyphs; the point of the change is the |
| 45 |
* drawing, not the theming. |
| 46 |
* |
| 47 |
* Note that the row actions inside the bin window, and the "Move to |
| 48 |
* trash" entries in context menus, stay on `dashicons-trash`. Those |
| 49 |
* are menu glyphs sitting among other menu glyphs, and they should |
| 50 |
* match their neighbours rather than this icon. |
| 51 |
* |
| 52 |
* The bin has two states. Empty is the vessel on its own; full adds |
| 53 |
* three crumpled balls inside it and knocks the lid askew. See |
| 54 |
* {@link openstation_recycle_bin_icon_svg()} for why the lid only |
| 55 |
* moves 8 degrees. |
| 56 |
* |
| 57 |
* @param bool $full Whether to draw the bin holding something. |
| 58 |
* @return string Raw `<svg>` markup. |
| 59 |
*/ |
| 60 |
function openstation_recycle_bin_icon_svg( $full = false ) { |
| 61 |
// The lid and the handle travel together. In the full state the |
| 62 |
// pair is knocked askew, which is the whole difference at the top |
| 63 |
// of the mark. |
| 64 |
$lid_transform = $full |
| 65 |
? ' transform="translate(0 -2.5) rotate(8 32 21.5)"' |
| 66 |
: ''; |
| 67 |
|
| 68 |
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">' |
| 69 |
. '<g' . $lid_transform . '>' |
| 70 |
// The handle, outlined so it reads as a loop rather than a tab. |
| 71 |
. '<path d="M25 19v-2.5a3.5 3.5 0 0 1 3.5-3.5h7a3.5 3.5 0 0 1 3.5 3.5V19" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>' |
| 72 |
// The lid: the one solid element, and the widest, so it anchors |
| 73 |
// the mark at small sizes. |
| 74 |
. '<rect x="10" y="19" width="44" height="5" rx="2.5" fill="currentColor"/>' |
| 75 |
. '</g>' |
| 76 |
// The body, tapered towards the base the way a real bin is, which |
| 77 |
// is also what separates it from a plain bucket. |
| 78 |
// |
| 79 |
// 27 units tall, narrowing to 71% of its top width. It was 24 |
| 80 |
// tall at 59%, which drew a shallower, more conical tub than a |
| 81 |
// bin actually is, and left an interior too cramped to hold |
| 82 |
// anything. Taken further, to a bottom much past 75%, the walls |
| 83 |
// go vertical and the mark reads as a bucket; this sits at the |
| 84 |
// edge of that. |
| 85 |
. '<path d="M15.5 28.5h33l-1.2 24a3.5 3.5 0 0 1-3.5 3H20.2a3.5 3.5 0 0 1-3.5-3z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>'; |
| 86 |
|
| 87 |
if ( $full ) { |
| 88 |
// Three crumpled balls, one path with three subpaths so the |
| 89 |
// mark stays at four elements rather than six. |
| 90 |
// |
| 91 |
// Each is a seven-point polygon on a radius jittered between |
| 92 |
// 0.81 and 1.0, with a same-colour round-joined stroke that |
| 93 |
// turns the corners into creases instead of points. Seven |
| 94 |
// points rather than nine because a ball this small needs |
| 95 |
// deeper, fewer facets to keep any texture at all. |
| 96 |
// |
| 97 |
// The layout is staggered deliberately. Two balls at the same |
| 98 |
// height with a third centred under them reads as a face, and |
| 99 |
// it cannot be unseen once noticed, so no two share a y (the |
| 100 |
// top pair are 4.2 units apart) and the third sits below-left |
| 101 |
// rather than on the centreline. Every gap in the mark clears |
| 102 |
// 2 units: 5.1 to the left wall, 4.4 to the right, 2.8 to the |
| 103 |
// rim, 2.5 to the base, and 2.5 to 4.2 between the balls. Those |
| 104 |
// last three decide how far down the size ladder they stay |
| 105 |
// three things instead of one. |
| 106 |
$svg .= '<path d="M29.7 38 27.4 39.5 24.7 39.6 23.6 37.1 24.1 34.4 26.8 34.1 29.2 35.1' |
| 107 |
. 'ZM39.4 44.1 36.7 43.5 34.8 41.6 35.8 39.1 38.1 37.6 40.2 39.3 41.1 41.8' |
| 108 |
. 'ZM28.1 50.4 27 47.9 27.4 45.2 30 44.6 32.6 45.6 32.4 48.3 31 50.5Z"' |
| 109 |
. ' fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>'; |
| 110 |
} |
| 111 |
|
| 112 |
return $svg . '</svg>'; |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* Both bin states as base64 data URIs, ready for `renderIcon()`. |
| 117 |
* |
| 118 |
* The client swaps between these as the count crosses zero, so both |
| 119 |
* have to reach the page on the first paint. Cheap: two string |
| 120 |
* builds and two base64 encodes, no queries. |
| 121 |
* |
| 122 |
* @return array{empty:string,full:string} |
| 123 |
*/ |
| 124 |
function openstation_recycle_bin_icon_uris() { |
| 125 |
return array( |
| 126 |
'empty' => 'data:image/svg+xml;base64,' . base64_encode( openstation_recycle_bin_icon_svg( false ) ), |
| 127 |
'full' => 'data:image/svg+xml;base64,' . base64_encode( openstation_recycle_bin_icon_svg( true ) ), |
| 128 |
); |
| 129 |
} |
| 130 |
|
| 131 |
/** |
| 132 |
* Echoes the recycle bin window's template body. |
| 133 |
* |
| 134 |
* The shell wraps this in `<template id="os-native-window-desktop-mode-recycle-bin">` |
| 135 |
* and clones it into the window body BEFORE the JS render callback runs. |
| 136 |
* The `data-os-recycle-bin-*` hooks below are the contract the JS |
| 137 |
* relies on — keep them intact (or rename via the filter) when |
| 138 |
* customizing the layout. |
| 139 |
*/ |
| 140 |
function openstation_recycle_bin_render_template() { |
| 141 |
ob_start(); |
| 142 |
?> |
| 143 |
<div class="desktop-mode-recycle-bin" data-os-recycle-bin-root> |
| 144 |
<header class="os-recycle-bin__toolbar" data-os-recycle-bin-toolbar> |
| 145 |
<div class="os-recycle-bin__toolbar-left"> |
| 146 |
<os-segmented data-os-recycle-bin-filter> |
| 147 |
<os-segment value="" selected><?php esc_html_e( 'All', 'desktop-mode' ); ?></os-segment> |
| 148 |
<os-segment value="post"><?php esc_html_e( 'Posts', 'desktop-mode' ); ?></os-segment> |
| 149 |
<os-segment value="page"><?php esc_html_e( 'Pages', 'desktop-mode' ); ?></os-segment> |
| 150 |
<?php |
| 151 |
// The Media segment is only useful when WP itself routes |
| 152 |
// attachment deletions through Trash. That gate is the |
| 153 |
// `MEDIA_TRASH` constant — defaults to false in core, can |
| 154 |
// be flipped to true from `wp-config.php`. Without it, |
| 155 |
// attachments permanent-delete on first click and the |
| 156 |
// Trash bin will never have anything in this bucket, so |
| 157 |
// the tab would always read "0" and confuse users. |
| 158 |
if ( defined( 'MEDIA_TRASH' ) && MEDIA_TRASH ) : |
| 159 |
?> |
| 160 |
<os-segment value="attachment"><?php esc_html_e( 'Media', 'desktop-mode' ); ?></os-segment> |
| 161 |
<?php |
| 162 |
endif; |
| 163 |
?> |
| 164 |
<os-segment value="comment"><?php esc_html_e( 'Comments', 'desktop-mode' ); ?></os-segment> |
| 165 |
<os-segment value="desktop"><?php esc_html_e( 'Desktop', 'desktop-mode' ); ?></os-segment> |
| 166 |
</os-segmented> |
| 167 |
<os-text-field |
| 168 |
data-os-recycle-bin-search |
| 169 |
placeholder="<?php esc_attr_e( 'Search trash…', 'desktop-mode' ); ?>" |
| 170 |
></os-text-field> |
| 171 |
</div> |
| 172 |
<div class="os-recycle-bin__toolbar-right" data-os-recycle-bin-bulk hidden> |
| 173 |
<span class="os-recycle-bin__count" data-os-recycle-bin-count></span> |
| 174 |
<os-button variant="secondary" data-os-recycle-bin-restore-selected> |
| 175 |
<span class="dashicons dashicons-image-rotate" aria-hidden="true"></span> |
| 176 |
<?php esc_html_e( 'Restore', 'desktop-mode' ); ?> |
| 177 |
</os-button> |
| 178 |
<os-button variant="secondary" data-os-recycle-bin-pin-to-desktop> |
| 179 |
<span class="dashicons dashicons-desktop" aria-hidden="true"></span> |
| 180 |
<?php esc_html_e( 'Pin to desktop', 'desktop-mode' ); ?> |
| 181 |
</os-button> |
| 182 |
<os-button variant="danger" data-os-recycle-bin-purge-selected> |
| 183 |
<span class="dashicons dashicons-trash" aria-hidden="true"></span> |
| 184 |
<?php esc_html_e( 'Delete forever', 'desktop-mode' ); ?> |
| 185 |
</os-button> |
| 186 |
</div> |
| 187 |
<div class="os-recycle-bin__toolbar-trailing"> |
| 188 |
<os-button variant="ghost" data-os-recycle-bin-refresh title="<?php esc_attr_e( 'Refresh', 'desktop-mode' ); ?>"> |
| 189 |
<span class="dashicons dashicons-update" aria-hidden="true"></span> |
| 190 |
</os-button> |
| 191 |
<os-button variant="danger" data-os-recycle-bin-empty> |
| 192 |
<span class="dashicons dashicons-trash" aria-hidden="true"></span> |
| 193 |
<?php esc_html_e( 'Empty Trash', 'desktop-mode' ); ?> |
| 194 |
</os-button> |
| 195 |
</div> |
| 196 |
</header> |
| 197 |
<div class="os-recycle-bin__body" data-os-recycle-bin-body> |
| 198 |
<os-table |
| 199 |
data-os-recycle-bin-table |
| 200 |
selectable="multi" |
| 201 |
sticky-header |
| 202 |
hover |
| 203 |
striped |
| 204 |
loading |
| 205 |
> |
| 206 |
<div slot="empty" class="os-recycle-bin__empty"> |
| 207 |
<span class="dashicons dashicons-trash" aria-hidden="true"></span> |
| 208 |
<p><?php esc_html_e( 'The Trash is empty.', 'desktop-mode' ); ?></p> |
| 209 |
<p class="os-recycle-bin__empty-hint"> |
| 210 |
<?php esc_html_e( 'Deleted posts, pages, and media show up here. Restoring puts them back where they were.', 'desktop-mode' ); ?> |
| 211 |
</p> |
| 212 |
</div> |
| 213 |
</os-table> |
| 214 |
</div> |
| 215 |
</div> |
| 216 |
<?php |
| 217 |
$html = (string) ob_get_clean(); |
| 218 |
|
| 219 |
/** |
| 220 |
* Filter the recycle bin window's template HTML. |
| 221 |
* |
| 222 |
* Keep the `data-os-recycle-bin-*` hooks intact so the JS render |
| 223 |
* callback can find its mount points, or rename them and update the |
| 224 |
* matching constants in `src/recycle-bin/index.ts`. |
| 225 |
* |
| 226 |
* @param string $html Default template HTML. |
| 227 |
*/ |
| 228 |
$filtered = (string) apply_filters( 'openstation_recycle_bin_template_html', $html ); |
| 229 |
echo wp_kses( $filtered, openstation_native_window_allowed_html() ); |
| 230 |
} |
| 231 |
|
| 232 |
/** |
| 233 |
* Whether the current user should see the recycle bin at all. |
| 234 |
* |
| 235 |
* Filterable so plugins can hide it from authors/contributors who |
| 236 |
* don't manage trash, or invert the gate to expose it to a custom |
| 237 |
* role. |
| 238 |
* |
| 239 |
* @return bool |
| 240 |
*/ |
| 241 |
function openstation_recycle_bin_user_can_use() { |
| 242 |
$can = current_user_can( 'edit_posts' ); |
| 243 |
|
| 244 |
/** |
| 245 |
* Filter whether the current user can see the recycle bin window. |
| 246 |
* |
| 247 |
* @param bool $can Default: edit_posts capability. |
| 248 |
*/ |
| 249 |
return (bool) apply_filters( 'openstation_recycle_bin_user_can_use', $can ); |
| 250 |
} |
| 251 |
|
| 252 |
/** |
| 253 |
* Register the recycle bin window on `init`. |
| 254 |
* |
| 255 |
* Hooked at priority 20, after `components.php` has bootstrapped the |
| 256 |
* native-window registry — same timing as the code editor. |
| 257 |
*/ |
| 258 |
function openstation_recycle_bin_register_window() { |
| 259 |
if ( ! openstation_recycle_bin_user_can_use() ) { |
| 260 |
return; |
| 261 |
} |
| 262 |
|
| 263 |
$icon_uris = openstation_recycle_bin_icon_uris(); |
| 264 |
$icon_uri = $icon_uris['empty']; |
| 265 |
|
| 266 |
$window_args = array( |
| 267 |
'title' => __( 'Trash', 'desktop-mode' ), |
| 268 |
'icon' => $icon_uri, |
| 269 |
'template' => 'openstation_recycle_bin_render_template', |
| 270 |
'script' => 'desktop-mode-recycle-bin', |
| 271 |
'width' => 880, |
| 272 |
'height' => 560, |
| 273 |
'min_width' => 520, |
| 274 |
'min_height' => 360, |
| 275 |
'placement' => 'dock', |
| 276 |
'nav_kind' => 'control', |
| 277 |
// Last on the rail, after the shell's own cluster (Mio 10, |
| 278 |
// Overview 20, System 30). Trash is where things END UP, and a |
| 279 |
// dock reads left to right: putting it anywhere but the end |
| 280 |
// makes it one more app rather than the bottom of the pile. |
| 281 |
'dock_order' => 40, |
| 282 |
// The bin is the one dock tile a user can reasonably not want, |
| 283 |
// so it gets a row in Apps & Plugins: dock (the default), |
| 284 |
// desktop, both, or hidden. It registers no desktop icon, so |
| 285 |
// that row is its only control. |
| 286 |
'placeable' => true, |
| 287 |
); |
| 288 |
|
| 289 |
/** |
| 290 |
* Filter the args used to register the recycle bin native window. |
| 291 |
* |
| 292 |
* @param array $window_args Args passed to `openstation_register_window()`. |
| 293 |
*/ |
| 294 |
$window_args = (array) apply_filters( 'openstation_recycle_bin_window_args', $window_args ); |
| 295 |
|
| 296 |
$registered = openstation_register_window( 'desktop-mode-recycle-bin', $window_args ); |
| 297 |
if ( is_wp_error( $registered ) ) { |
| 298 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 299 |
error_log( '[openstation] Recycle bin window registration failed: ' . $registered->get_error_message() ); |
| 300 |
} |
| 301 |
} |
| 302 |
add_action( 'init', 'openstation_recycle_bin_register_window', 20 ); |
| 303 |
|
| 304 |
/** |
| 305 |
* Localize REST endpoints for the JS bundle. |
| 306 |
* |
| 307 |
* Same pattern as the code editor: the bundle reads its config off |
| 308 |
* `window.openStationRecycleBinConfig` and never hardcodes URLs. |
| 309 |
*/ |
| 310 |
function openstation_recycle_bin_localize_config() { |
| 311 |
if ( ! openstation_recycle_bin_user_can_use() ) { |
| 312 |
return; |
| 313 |
} |
| 314 |
|
| 315 |
wp_localize_script( |
| 316 |
'desktop-mode-recycle-bin', |
| 317 |
'openStationRecycleBinConfig', |
| 318 |
array( |
| 319 |
'restNonce' => wp_create_nonce( 'wp_rest' ), |
| 320 |
'listUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin' ) ), |
| 321 |
'restoreUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin/restore' ) ), |
| 322 |
'purgeUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin/purge' ) ), |
| 323 |
'emptyUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin/empty' ) ), |
| 324 |
'countUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin/count' ) ), |
| 325 |
'postTypes' => openstation_recycle_bin_capture_post_types(), |
| 326 |
) |
| 327 |
); |
| 328 |
|
| 329 |
wp_enqueue_style( 'desktop-mode-recycle-bin' ); |
| 330 |
} |
| 331 |
// Priority 5, not an afterthought: `openstation_enqueue_assets()` (default 10) |
| 332 |
// harvests every lazy window's `wp_localize_script` data into the shell |
| 333 |
// payload, so config attached after 10 ships the bundle with no config — the |
| 334 |
// exact "openStationRecycleBinConfig is missing" failure the bundle warns |
| 335 |
// about. This ran at 30 and got away with it only while the bundle was |
| 336 |
// enqueued eagerly and WordPress printed the data itself at print time. |
| 337 |
add_action( 'admin_enqueue_scripts', 'openstation_recycle_bin_localize_config', 5 ); |
| 338 |
|
| 339 |
/** |
| 340 |
* Inject the initial trash count and both bin drawings into the |
| 341 |
* shell config, so the dock tile (and any icon a user or plugin has |
| 342 |
* placed against this window) shows the right one on the very first |
| 343 |
* paint, before the bin window has ever opened. |
| 344 |
* |
| 345 |
* Both drawings travel together rather than the server picking one: |
| 346 |
* the count changes without a reload, and shipping the pair makes |
| 347 |
* crossing zero a local swap instead of a round trip. |
| 348 |
* |
| 349 |
* @param array $config Shell config blob. |
| 350 |
* @return array |
| 351 |
*/ |
| 352 |
function openstation_recycle_bin_inject_shell_config( $config ) { |
| 353 |
if ( ! is_array( $config ) ) { |
| 354 |
return $config; |
| 355 |
} |
| 356 |
$icons = openstation_recycle_bin_icon_uris(); |
| 357 |
|
| 358 |
$config['recycleBinCount'] = openstation_recycle_bin_count(); |
| 359 |
$config['recycleBinCountUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/recycle-bin/count' ) ); |
| 360 |
$config['recycleBinPostTypes'] = openstation_recycle_bin_capture_post_types(); |
| 361 |
$config['recycleBinIconEmpty'] = $icons['empty']; |
| 362 |
$config['recycleBinIconFull'] = $icons['full']; |
| 363 |
return $config; |
| 364 |
} |
| 365 |
add_filter( 'openstation_shell_config', 'openstation_recycle_bin_inject_shell_config', 20 ); |
| 366 |
|