PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / recycle-bin / window.php

window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.0, at includes/recycle-bin/window.php

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