PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.3
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.1.3, at includes/recycle-bin/window.php

366 lines 15.2 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 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