PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.5
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.5
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 / assets / js / recycle-bin.js

recycle-bin.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.5, at assets/js/recycle-bin.js

3,381 lines 113.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 var desktopModeRecycleBin = function(exports) {
2 "use strict";
3 const TEXT_DOMAIN = "desktop-mode";
4 function i18n() {
5 return window.wp?.i18n;
6 }
7 function __(text, domain = TEXT_DOMAIN) {
8 return i18n()?.__(text, domain) ?? text;
9 }
10 function sprintf(format, ...args) {
11 const impl = i18n()?.sprintf;
12 if (impl) {
13 return impl(format, ...args);
14 }
15 let i = 0;
16 return format.replace(/%(?:(\d+)\$)?[sd]/g, (_match, pos) => {
17 const idx = pos ? Number.parseInt(pos, 10) - 1 : i++;
18 return String(args[idx] ?? "");
19 });
20 }
21 function html(strings, ...values) {
22 return { __wpdHtml: true, strings, values };
23 }
24 function isTemplateResult$1(v) {
25 return !!v && v.__wpdHtml === true;
26 }
27 const MARKER_PREFIX = "$$wpd$$";
28 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
29 function joinWithMarkers(strings) {
30 let out = strings[0];
31 for (let i = 1; i < strings.length; i++) {
32 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
33 }
34 return out;
35 }
36 const compiledCache = /* @__PURE__ */ new WeakMap();
37 function compile(strings) {
38 const cached = compiledCache.get(strings);
39 if (cached) {
40 return cached;
41 }
42 const template = document.createElement("template");
43 template.innerHTML = joinWithMarkers(strings);
44 const recipes = [];
45 const walk = (node, path) => {
46 if (node.nodeType === Node.ELEMENT_NODE) {
47 const el = node;
48 for (const attr of Array.from(el.attributes)) {
49 const rawName = attr.name;
50 const rawValue = attr.value;
51 const prefix = rawName[0];
52 if (MARKER_RE.test(rawValue)) {
53 MARKER_RE.lastIndex = 0;
54 if (prefix === "@") {
55 const match = MARKER_RE.exec(rawValue);
56 MARKER_RE.lastIndex = 0;
57 recipes.push({
58 path,
59 kind: "event",
60 name: rawName.slice(1),
61 valueIndex: match ? Number(match[1]) : 0
62 });
63 el.removeAttribute(rawName);
64 } else if (prefix === ".") {
65 const match = MARKER_RE.exec(rawValue);
66 MARKER_RE.lastIndex = 0;
67 recipes.push({
68 path,
69 kind: "prop",
70 name: rawName.slice(1),
71 valueIndex: match ? Number(match[1]) : 0
72 });
73 el.removeAttribute(rawName);
74 } else if (prefix === "?") {
75 const match = MARKER_RE.exec(rawValue);
76 MARKER_RE.lastIndex = 0;
77 recipes.push({
78 path,
79 kind: "bool",
80 name: rawName.slice(1),
81 valueIndex: match ? Number(match[1]) : 0
82 });
83 el.removeAttribute(rawName);
84 } else {
85 const fragments = [];
86 const indices = [];
87 let lastEnd = 0;
88 let m;
89 MARKER_RE.lastIndex = 0;
90 while ((m = MARKER_RE.exec(rawValue)) !== null) {
91 fragments.push(rawValue.slice(lastEnd, m.index));
92 indices.push(Number(m[1]));
93 lastEnd = m.index + m[0].length;
94 }
95 fragments.push(rawValue.slice(lastEnd));
96 recipes.push({
97 path,
98 kind: "attr",
99 name: rawName,
100 template: fragments,
101 valueIndices: indices
102 });
103 el.setAttribute(rawName, "");
104 }
105 }
106 }
107 }
108 const children = Array.from(node.childNodes);
109 let shift = 0;
110 for (let i = 0; i < children.length; i++) {
111 const child = children[i];
112 const liveIndex = i + shift;
113 if (child.nodeType === Node.TEXT_NODE) {
114 const text = child.textContent || "";
115 if (!MARKER_RE.test(text)) {
116 MARKER_RE.lastIndex = 0;
117 continue;
118 }
119 MARKER_RE.lastIndex = 0;
120 const parent = child.parentNode;
121 let lastEnd = 0;
122 let m;
123 const newNodes = [];
124 const newRecipes = [];
125 MARKER_RE.lastIndex = 0;
126 while ((m = MARKER_RE.exec(text)) !== null) {
127 if (m.index > lastEnd) {
128 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
129 }
130 const placeholder = document.createTextNode("");
131 newNodes.push(placeholder);
132 newRecipes.push({
133 path: [...path, liveIndex + newNodes.length - 1],
134 kind: "node",
135 valueIndex: Number(m[1])
136 });
137 lastEnd = m.index + m[0].length;
138 }
139 if (lastEnd < text.length) {
140 newNodes.push(document.createTextNode(text.slice(lastEnd)));
141 }
142 for (const nn of newNodes) {
143 parent.insertBefore(nn, child);
144 }
145 parent.removeChild(child);
146 shift += newNodes.length - 1;
147 recipes.push(...newRecipes);
148 } else {
149 walk(child, [...path, liveIndex]);
150 }
151 }
152 };
153 walk(template.content, []);
154 const buildParts = (fragment) => {
155 const out = [];
156 for (const r of recipes) {
157 let node = fragment;
158 for (const idx of r.path) {
159 node = node.childNodes[idx];
160 }
161 if (r.kind === "node") {
162 out.push({
163 kind: "node",
164 valueIndex: r.valueIndex,
165 child: {
166 anchor: node,
167 state: null
168 }
169 });
170 } else if (r.kind === "attr") {
171 out.push({
172 kind: "attr",
173 element: node,
174 name: r.name,
175 template: r.template,
176 valueIndices: r.valueIndices
177 });
178 } else if (r.kind === "event") {
179 out.push({
180 kind: "event",
181 valueIndex: r.valueIndex,
182 element: node,
183 name: r.name
184 });
185 } else if (r.kind === "prop") {
186 out.push({
187 kind: "prop",
188 valueIndex: r.valueIndex,
189 element: node,
190 name: r.name
191 });
192 } else if (r.kind === "bool") {
193 out.push({
194 kind: "bool",
195 valueIndex: r.valueIndex,
196 element: node,
197 name: r.name
198 });
199 }
200 }
201 return out;
202 };
203 const entry = { template, buildParts };
204 compiledCache.set(strings, entry);
205 return entry;
206 }
207 const mountState = /* @__PURE__ */ new WeakMap();
208 function render(result, container) {
209 const existing = mountState.get(container);
210 if (existing && existing.strings === result.strings) {
211 applyValues(existing.parts, result.values);
212 return;
213 }
214 const compiled = compile(result.strings);
215 const fragment = compiled.template.content.cloneNode(true);
216 const parts = compiled.buildParts(fragment);
217 while (container.firstChild) {
218 container.removeChild(container.firstChild);
219 }
220 container.appendChild(fragment);
221 applyValues(parts, result.values);
222 mountState.set(container, { strings: result.strings, parts });
223 }
224 function applyValues(parts, values) {
225 for (const part of parts) {
226 if (part.kind === "node") {
227 updateChildPart(part.child, values[part.valueIndex]);
228 } else if (part.kind === "attr") {
229 let composed = part.template[0];
230 for (let i = 0; i < part.valueIndices.length; i++) {
231 composed += formatText(values[part.valueIndices[i]]);
232 composed += part.template[i + 1];
233 }
234 if (composed !== part.last) {
235 part.last = composed;
236 if (composed === "") {
237 part.element.removeAttribute(part.name);
238 } else {
239 part.element.setAttribute(part.name, composed);
240 }
241 }
242 } else if (part.kind === "event") {
243 const next = values[part.valueIndex];
244 if (next !== part.current) {
245 if (part.current) {
246 part.element.removeEventListener(part.name, part.current);
247 }
248 if (next) {
249 part.element.addEventListener(part.name, next);
250 }
251 part.current = next;
252 }
253 } else if (part.kind === "prop") {
254 const next = values[part.valueIndex];
255 if (next !== part.last) {
256 part.last = next;
257 part.element[part.name] = next;
258 }
259 } else if (part.kind === "bool") {
260 const next = !!values[part.valueIndex];
261 if (next !== part.last) {
262 part.last = next;
263 if (next) {
264 part.element.setAttribute(part.name, "");
265 } else {
266 part.element.removeAttribute(part.name);
267 }
268 }
269 }
270 }
271 }
272 function updateChildPart(child, value) {
273 if (value === null || value === void 0 || value === false) {
274 if (child.state) {
275 disposeChildState(child.state);
276 child.state = null;
277 }
278 return;
279 }
280 if (Array.isArray(value)) {
281 updateArrayChild(child, value);
282 return;
283 }
284 if (isTemplateResult$1(value)) {
285 updateTemplateChild(child, value);
286 return;
287 }
288 if (value instanceof Node) {
289 updateNodeChild(child, value);
290 return;
291 }
292 updateTextChild(child, formatText(value));
293 }
294 function updateNodeChild(child, node) {
295 const old = child.state;
296 if (old?.shape === "node" && old.node === node) {
297 return;
298 }
299 if (old) {
300 disposeChildState(old);
301 }
302 insertBeforeAnchor(child, [node]);
303 child.state = { shape: "node", node };
304 }
305 function updateTextChild(child, text) {
306 const old = child.state;
307 if (old?.shape === "text") {
308 if (old.text !== text) {
309 old.node.textContent = text;
310 old.text = text;
311 }
312 return;
313 }
314 if (old) {
315 disposeChildState(old);
316 }
317 const node = document.createTextNode(text);
318 insertBeforeAnchor(child, [node]);
319 child.state = { shape: "text", node, text };
320 }
321 function updateTemplateChild(child, result) {
322 const old = child.state;
323 if (old?.shape === "template" && old.strings === result.strings) {
324 applyValues(old.parts, result.values);
325 return;
326 }
327 if (old) {
328 disposeChildState(old);
329 }
330 const compiled = compile(result.strings);
331 const fragment = compiled.template.content.cloneNode(true);
332 const parts = compiled.buildParts(fragment);
333 const topNodes = Array.from(fragment.childNodes);
334 insertBeforeAnchor(child, [fragment]);
335 applyValues(parts, result.values);
336 child.state = {
337 shape: "template",
338 strings: result.strings,
339 parts,
340 nodes: topNodes
341 };
342 }
343 function updateArrayChild(child, arr) {
344 const old = child.state;
345 if (old?.shape === "array" && old.entries.length === arr.length) {
346 for (let i = 0; i < arr.length; i++) {
347 updateChildPart(old.entries[i], arr[i]);
348 }
349 return;
350 }
351 if (old) {
352 disposeChildState(old);
353 }
354 const entries = [];
355 for (const v of arr) {
356 const entryAnchor = document.createTextNode("");
357 insertBeforeAnchor(child, [entryAnchor]);
358 const entry = { anchor: entryAnchor, state: null };
359 updateChildPart(entry, v);
360 entries.push(entry);
361 }
362 child.state = { shape: "array", entries };
363 }
364 function insertBeforeAnchor(child, nodes) {
365 const parent = child.anchor.parentNode;
366 if (!parent) {
367 return;
368 }
369 for (const node of nodes) {
370 parent.insertBefore(node, child.anchor);
371 }
372 }
373 function disposeChildState(state2) {
374 if (state2.shape === "text") {
375 state2.node.remove();
376 return;
377 }
378 if (state2.shape === "template") {
379 for (const node of state2.nodes) {
380 if (node.parentNode) {
381 node.parentNode.removeChild(node);
382 }
383 }
384 return;
385 }
386 if (state2.shape === "node") {
387 if (state2.node.parentNode) {
388 state2.node.parentNode.removeChild(state2.node);
389 }
390 return;
391 }
392 for (const entry of state2.entries) {
393 if (entry.state) {
394 disposeChildState(entry.state);
395 }
396 entry.anchor.remove();
397 }
398 }
399 function formatText(v) {
400 if (v === null || v === void 0 || v === false) {
401 return "";
402 }
403 return String(v);
404 }
405 const _Component = class _Component extends HTMLElement {
406 constructor() {
407 super();
408 this._renderScheduled = false;
409 this._propValues = {};
410 const ctor = this.constructor;
411 if (ctor.shadow) {
412 this.attachShadow({ mode: "open" });
413 this._renderRoot = this.shadowRoot;
414 } else {
415 this._renderRoot = this;
416 }
417 this._installPropAccessors();
418 }
419 static get observedAttributes() {
420 return this.props.map(kebab);
421 }
422 connectedCallback() {
423 this._adoptStyles();
424 this.requestUpdate();
425 }
426 attributeChangedCallback(name, oldValue, newValue) {
427 if (oldValue === newValue) {
428 return;
429 }
430 const prop = camel(name);
431 this._propValues[prop] = newValue;
432 this.requestUpdate();
433 }
434 /**
435 * Declarative class-name setter. Assign an array (or a
436 * space-separated string) and the host's `class` attribute is
437 * rewritten to match. Intended for programmatic styling — when
438 * a plugin has enqueued its own stylesheet and wants to apply
439 * one of those classes to a shell component:
440 *
441 * ```js
442 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
443 * // → <wpd-select class="my-plugin-brand is-active">
444 * ```
445 *
446 * The plain HTML `class="…"` attribute works just the same and
447 * is always preferred when writing markup by hand — this setter
448 * exists for the JS-API case where the caller has an array of
449 * conditional classes in hand.
450 *
451 * Getter returns the current `classList` as a plain array for
452 * symmetric read/write.
453 *
454 * @since 0.5.0
455 */
456 get classNames() {
457 return Array.from(this.classList);
458 }
459 set classNames(next) {
460 if (next === null || next === void 0) {
461 this.removeAttribute("class");
462 return;
463 }
464 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
465 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
466 this.className = cleaned.join(" ");
467 }
468 /**
469 * Request a re-render explicitly. Components rarely need this —
470 * declare state via props + attribute observers and the render
471 * loop picks up changes automatically.
472 */
473 requestUpdate() {
474 this._scheduleRender();
475 }
476 /**
477 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
478 * by default (matches typical WC UX — events cross shadow
479 * boundaries, parents can listen without knowing about internal
480 * structure).
481 */
482 emit(name, detail) {
483 return this.dispatchEvent(
484 new CustomEvent(name, {
485 detail,
486 bubbles: true,
487 composed: true
488 })
489 );
490 }
491 // ------------------------------------------------------------------
492 // Internals
493 // ------------------------------------------------------------------
494 /**
495 * Wire every `static props` entry to a matched property getter +
496 * setter on the element. Setting the property reflects into the
497 * attribute (so downstream observers + CSS selectors see it);
498 * reading the property falls back to the attribute.
499 */
500 _installPropAccessors() {
501 const ctor = this.constructor;
502 for (const prop of ctor.props) {
503 if (Object.getOwnPropertyDescriptor(this, prop)) {
504 continue;
505 }
506 const attr = kebab(prop);
507 Object.defineProperty(this, prop, {
508 get: () => {
509 if (prop in this._propValues) {
510 return this._propValues[prop];
511 }
512 return this.getAttribute(attr);
513 },
514 set: (value) => {
515 let str;
516 if (value === null || value === void 0 || value === false) {
517 str = null;
518 } else if (value === true) {
519 str = "";
520 } else {
521 str = String(value);
522 }
523 this._propValues[prop] = str;
524 if (str === null) {
525 this.removeAttribute(attr);
526 } else {
527 this.setAttribute(attr, str);
528 }
529 this.requestUpdate();
530 },
531 enumerable: true,
532 configurable: true
533 });
534 }
535 }
536 /**
537 * Schedule a render on the next microtask. Multiple property
538 * assignments in the same tick collapse into a single render.
539 */
540 _scheduleRender() {
541 if (this._renderScheduled || !this.isConnected) {
542 return;
543 }
544 this._renderScheduled = true;
545 queueMicrotask(() => {
546 this._renderScheduled = false;
547 if (!this.isConnected) {
548 return;
549 }
550 render(this.render(), this._renderRoot);
551 });
552 }
553 /**
554 * Mount adoptable stylesheets onto the shadow root (via
555 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
556 * tag per def). No-op if `static styles` is empty.
557 */
558 _adoptStyles() {
559 const ctor = this.constructor;
560 if (ctor.styles.length === 0) {
561 return;
562 }
563 if (ctor.shadow && this.shadowRoot) {
564 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
565 this.shadowRoot.adoptedStyleSheets = sheets;
566 if (sheets.length !== ctor.styles.length) {
567 for (const s of ctor.styles) {
568 if (!s.sheet) {
569 const tag = document.createElement("style");
570 tag.textContent = s.cssText;
571 this.shadowRoot.appendChild(tag);
572 }
573 }
574 }
575 } else {
576 this._adoptLightStyles(ctor);
577 }
578 }
579 _adoptLightStyles(ctor) {
580 if (_Component._lightStylesAdopted.has(ctor)) {
581 return;
582 }
583 _Component._lightStylesAdopted.add(ctor);
584 for (const s of ctor.styles) {
585 const tag = document.createElement("style");
586 tag.dataset.wpdUi = this.tagName.toLowerCase();
587 tag.textContent = s.cssText;
588 document.head.appendChild(tag);
589 }
590 }
591 };
592 _Component.props = [];
593 _Component.styles = [];
594 _Component.shadow = true;
595 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
596 let Component = _Component;
597 function defineComponent(tag, ctor) {
598 if (customElements.get(tag)) {
599 return;
600 }
601 customElements.define(tag, ctor);
602 }
603 function kebab(s) {
604 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
605 }
606 function camel(s) {
607 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
608 }
609 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
610 try {
611 const s = new CSSStyleSheet();
612 return typeof s.replaceSync === "function";
613 } catch {
614 return false;
615 }
616 })();
617 function css(strings, ...values) {
618 let text = strings[0];
619 for (let i = 1; i < strings.length; i++) {
620 const v = values[i - 1];
621 if (typeof v === "string" || typeof v === "number") {
622 text += String(v);
623 } else if (v && v.__wpdCss) {
624 text += v.cssText;
625 } else {
626 throw new TypeError(
627 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
628 );
629 }
630 text += strings[i];
631 }
632 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
633 const sheet = new CSSStyleSheet();
634 sheet.replaceSync(text);
635 return { __wpdCss: true, sheet, cssText: text };
636 }
637 return { __wpdCss: true, sheet: null, cssText: text };
638 }
639 const styles$1 = css`:host{display:block;--wpd-table-bg:var( --wpd-surface,#fff );--wpd-table-border:var( --wpd-border,rgba( 0,0,0,0.08 ) );--wpd-table-column-border:var( --wpd-border-strong,rgba( 0,0,0,0.14 ) );--wpd-table-header-bg:var( --wpd-surface-elevated,#f6f7f7 );--wpd-table-row-hover:rgba( 0,0,0,0.04 );--wpd-table-stripe:rgba( 0,0,0,0.03 );--wpd-table-cell-padding:8px 12px;--wpd-table-font-size:13px;--wpd-table-max-height:none;font-size:var( --wpd-table-font-size );color:inherit}:host( [ hidden ] ){display:none}.scroll{position:relative;overflow:auto;max-height:var( --wpd-table-max-height );border:1px solid var( --wpd-table-border );border-radius:4px;background:var( --wpd-table-bg )}table{width:100%;border-collapse:separate;border-spacing:0;background:var( --wpd-table-bg )}thead th{text-align:start;font-weight:600;background-color:var( --wpd-table-header-bg );padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );white-space:nowrap}tbody td{padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );background-color:var( --wpd-table-bg );vertical-align:middle}tbody tr:last-child td{border-bottom:0}:host( [ striped ] ) tbody tr:nth-child( odd ) td{background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ hover ] ) tbody tr:hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}:host( [ hover ] [ striped ] ) tbody tr:nth-child( odd ):hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) ),linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ compact ] ){--wpd-table-cell-padding:4px 8px;--wpd-table-font-size:12px}:host( [ bordered ] ) thead th,:host( [ bordered ] ) tbody td{border-inline-end:1px solid var( --wpd-table-column-border )}:host( [ bordered ] ) thead th:last-child,:host( [ bordered ] ) tbody td:last-child{border-inline-end:0}th.is-sticky,td.is-sticky{position:sticky;z-index:10}tbody td.is-sticky{background-color:var( --wpd-table-bg )}thead th.is-sticky{background-color:var( --wpd-table-header-bg );z-index:30}:host( [ sticky-header ] ) thead th{position:sticky;top:0;z-index:20}:host( [ sticky-header ] ) thead tr.filter-row th{top:var( --wpd-table-header-height,33px );z-index:20}:host( [ sticky-header ] ) thead th.is-sticky{z-index:40}:host( [ sticky-header ] ) thead tr.filter-row th.is-sticky{z-index:40}th.is-sticky-edge,td.is-sticky-edge{border-inline-end:var( --wpd-table-sticky-edge,2px solid var( --wpd-table-border ) )}.align-center{text-align:center}.align-end{text-align:end}.filter-row th{padding:4px 8px;background-color:var( --wpd-table-header-bg );border-bottom:1px solid var( --wpd-table-border );font-weight:400}.filter-input,.filter-select{width:100%;min-width:60px;box-sizing:border-box;padding:4px 6px;font:inherit;color:inherit;background-color:var( --wpd-table-bg );border:1px solid var( --wpd-table-border );border-radius:3px}.filter-input:focus,.filter-select:focus{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-1px}.expander{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;border-radius:3px;font-size:11px;line-height:1}.expander:hover{background:rgba( 0,0,0,0.06 )}td.col-expander,th.col-expander{width:36px;min-width:36px;padding-left:0;padding-right:0;text-align:center}tr.subtable td{padding:0;background-color:var( --wpd-table-bg );background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) );border-bottom:1px solid var( --wpd-table-border )}tr.subtable .subtable-inner{padding:8px 12px 8px 32px}tr.empty td{padding:24px;text-align:center;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );font-style:italic}thead th.is-sortable{cursor:pointer;user-select:none}thead th.is-sortable:hover{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}thead th.is-sortable:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px}.sort-indicator{font-size:10px;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );margin-inline-start:2px}thead th.sort-asc .sort-indicator,thead th.sort-desc .sort-indicator{color:var( --wp-admin-theme-color,#2271b1 )}td.col-select,th.col-select{width:40px;min-width:40px;padding-left:0;padding-right:0;text-align:center}.select-all-checkbox,.select-row-checkbox{cursor:pointer;margin:0}tbody tr.is-selected td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 10%,var( --wpd-table-bg ) );background-image:none}tbody tr.is-selected:hover td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 16%,var( --wpd-table-bg ) )}tbody tr.skeleton td{padding:var( --wpd-table-cell-padding )}.skeleton-bar{display:block;height:12px;border-radius:3px;background:linear-gradient( 90deg,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 0%,var( --wpd-table-skeleton-highlight,rgba( 0,0,0,0.14 ) ) 50%,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 100% );background-size:200% 100%;animation:wpd-table-skeleton-pulse 1.4s ease-in-out infinite}@keyframes wpd-table-skeleton-pulse{0%{background-position:200% 50%}100%{background-position:-200% 50%}}@media ( prefers-reduced-motion:reduce ){.skeleton-bar{animation:none}}`;
640 const EXPANDER_KEY = "__wpd_expander__";
641 const SELECT_KEY = "__wpd_select__";
642 const _WpdTable = class _WpdTable extends Component {
643 constructor() {
644 super(...arguments);
645 this._data = [];
646 this._columns = [];
647 this._filters = {};
648 this._expanded = /* @__PURE__ */ new Set();
649 this._subTable = null;
650 this._sort = null;
651 this._selection = /* @__PURE__ */ new Set();
652 this._getRowId = (_row, index) => index;
653 this._filterCache = /* @__PURE__ */ new Map();
654 this._paintScheduled = false;
655 this._stickyHeaderWarned = false;
656 this._stickyRaceWarned = false;
657 this._resizeObserver = null;
658 this._stickyMicroScheduled = false;
659 this._stickyRafHandle = null;
660 this._loadingDesyncWarned = false;
661 this._lastStickyIndex = -1;
662 }
663 // ------------------------------------------------------------------
664 // Public properties — set from JS (use `.data=${...}` in templates).
665 // ------------------------------------------------------------------
666 /** The row buffer. Reassigning replaces (and clears expansion state). */
667 get data() {
668 return this._data;
669 }
670 set data(next) {
671 this._data = Array.isArray(next) ? next.slice() : [];
672 this._expanded.clear();
673 this._schedulePaint();
674 }
675 /** Column descriptors. See {@link WpdTableColumn}. */
676 get columns() {
677 return this._columns;
678 }
679 set columns(next) {
680 this._columns = Array.isArray(next) ? next.slice() : [];
681 const keys = new Set(this._columns.map((c) => c.key));
682 for (const k of Object.keys(this._filters)) {
683 if (!keys.has(k)) {
684 delete this._filters[k];
685 }
686 }
687 for (const k of Array.from(this._filterCache.keys())) {
688 if (!keys.has(k)) {
689 this._filterCache.delete(k);
690 }
691 }
692 if (this._sort && !keys.has(this._sort.key)) {
693 this._sort = null;
694 }
695 this._schedulePaint();
696 }
697 /** Read or replace the current filter map. */
698 get filters() {
699 return { ...this._filters };
700 }
701 set filters(next) {
702 this._filters = next ? { ...next } : {};
703 this._schedulePaint();
704 }
705 /** Read or set the active sort. `null` clears it. */
706 get sort() {
707 return this._sort ? { ...this._sort } : null;
708 }
709 set sort(next) {
710 this._sort = next ? { ...next } : null;
711 this._schedulePaint();
712 }
713 /** Read or replace the selection (set of row ids). */
714 get selection() {
715 return new Set(this._selection);
716 }
717 set selection(next) {
718 this._selection = new Set(next ?? []);
719 this._schedulePaint();
720 }
721 /** The currently-selected rows (resolved from `selection` + `data`). */
722 get selectedRows() {
723 const out = [];
724 this._data.forEach((row, i) => {
725 if (this._selection.has(this._getRowId(row, i))) {
726 out.push(row);
727 }
728 });
729 return out;
730 }
731 /**
732 * The rows currently visible — i.e. passing the active client-side
733 * filters, in data order. This is the row set `selectAll()` and
734 * the header select-all tri-state operate on.
735 *
736 * Destructive bulk consumers should resolve `selection` against
737 * THIS list rather than `data`: selection deliberately survives
738 * `data` reassignment, and a data-driven change (a realtime
739 * refresh editing a row so it no longer matches an active filter)
740 * can hide a selected row without any filter event firing. Rows
741 * the user cannot see must never be swept into a destructive
742 * action. See `collectSelectedItems()` in src/recycle-bin/index.ts
743 * for the canonical consumer.
744 *
745 * @since 0.9.4
746 */
747 get visibleRows() {
748 return this._filteredRows().map((entry) => entry.row);
749 }
750 /** Stable row-id extractor. Default is row index. */
751 get getRowId() {
752 return this._getRowId;
753 }
754 set getRowId(fn) {
755 this._getRowId = typeof fn === "function" ? fn : (_r, i) => i;
756 this._schedulePaint();
757 }
758 /**
759 * Sub-table accessor. Return `null` (or omit) for rows with no
760 * children. Return `{ columns, data }` to render a nested
761 * `<wpd-table>` inline; or return any `Node` / `html\`\`` template
762 * for fully custom expanded content.
763 */
764 get subTable() {
765 return this._subTable;
766 }
767 set subTable(fn) {
768 this._subTable = typeof fn === "function" ? fn : null;
769 this._expanded.clear();
770 this._schedulePaint();
771 }
772 /** Read or replace the expansion set (row indices that are open). */
773 get expanded() {
774 return new Set(this._expanded);
775 }
776 set expanded(next) {
777 this._expanded = new Set(next ?? []);
778 this._schedulePaint();
779 }
780 // ------------------------------------------------------------------
781 // Programmatic methods
782 // ------------------------------------------------------------------
783 /** Open a row's sub-table by index. No-op if the index is out of range. */
784 expand(index) {
785 if (index < 0 || index >= this._data.length) {
786 return;
787 }
788 if (this._expanded.has(index)) {
789 return;
790 }
791 this._expanded.add(index);
792 this.emit("wpd-table-expand-change", {
793 row: this._data[index],
794 index,
795 expanded: true
796 });
797 this._schedulePaint();
798 }
799 /** Close a row's sub-table by index. No-op if it wasn't open. */
800 collapse(index) {
801 if (!this._expanded.has(index)) {
802 return;
803 }
804 this._expanded.delete(index);
805 this.emit("wpd-table-expand-change", {
806 row: this._data[index],
807 index,
808 expanded: false
809 });
810 this._schedulePaint();
811 }
812 /** Open every row that has children. */
813 expandAll() {
814 if (!this._subTable) {
815 return;
816 }
817 let changed = false;
818 for (let i = 0; i < this._data.length; i++) {
819 if (!this._subTable(this._data[i], i)) {
820 continue;
821 }
822 if (!this._expanded.has(i)) {
823 this._expanded.add(i);
824 changed = true;
825 }
826 }
827 if (changed) {
828 this._schedulePaint();
829 }
830 }
831 /** Close every open row. */
832 collapseAll() {
833 if (this._expanded.size === 0) {
834 return;
835 }
836 this._expanded.clear();
837 this._schedulePaint();
838 }
839 isExpanded(index) {
840 return this._expanded.has(index);
841 }
842 /** Drop every active filter and emit `wpd-table-filter-change`. */
843 clearFilters() {
844 if (Object.keys(this._filters).length === 0) {
845 return;
846 }
847 this._filters = {};
848 this.emit("wpd-table-filter-change", { filters: {} });
849 this._schedulePaint();
850 }
851 /** Drop the active sort and emit `wpd-table-sort-change`. */
852 clearSort() {
853 if (this._sort === null) {
854 return;
855 }
856 this._sort = null;
857 this.emit("wpd-table-sort-change", { sort: null });
858 this._schedulePaint();
859 }
860 /**
861 * Add a row id to the selection. Emits `wpd-table-selection-change`.
862 *
863 * Selection mutators (`select` / `deselect` / `selectAll` /
864 * `clearSelection`) update the affected row in place via
865 * {@link _syncSelectionDom} rather than re-rendering the whole
866 * tbody — a rebuild would tear down the focused checkbox and
867 * (because scroll-anchoring abandons a momentarily empty container)
868 * could snap scroll back to the top.
869 */
870 select(id) {
871 if (this._selection.has(id)) {
872 return;
873 }
874 const mode = this._readSelectable();
875 const previouslySelected = mode === "single" ? Array.from(this._selection) : [];
876 if (mode === "single") {
877 this._selection.clear();
878 }
879 this._selection.add(id);
880 this._emitSelectionChange();
881 this._syncSelectionDom([id, ...previouslySelected]);
882 }
883 /** Remove a row id from the selection. */
884 deselect(id) {
885 if (!this._selection.delete(id)) {
886 return;
887 }
888 this._emitSelectionChange();
889 this._syncSelectionDom([id]);
890 }
891 /** Select every visible row — the rows passing the active client-side filters (multi-mode only). */
892 selectAll() {
893 if (this._readSelectable() !== "multi") {
894 return;
895 }
896 for (const { row, index } of this._filteredRows()) {
897 this._selection.add(this._getRowId(row, index));
898 }
899 this._emitSelectionChange();
900 this._syncSelectionDom("all");
901 }
902 /** Empty the selection. */
903 clearSelection() {
904 if (this._selection.size === 0) {
905 return;
906 }
907 this._selection.clear();
908 this._emitSelectionChange();
909 this._syncSelectionDom("all");
910 }
911 /**
912 * Apply a selection change to the existing tbody DOM without
913 * rebuilding it. Updates each affected row's `is-selected` class
914 * and `select-row-checkbox` `checked` state, then re-syncs the
915 * header select-all checkbox (checked / indeterminate / empty).
916 *
917 * @param ids `'all'` to walk every row, or an iterable of row ids
918 * whose rows need updating. Unknown ids are silently
919 * skipped (row may not be in the current filter/page).
920 */
921 _syncSelectionDom(ids) {
922 const root = this.shadowRoot;
923 if (!root) {
924 return;
925 }
926 const tbody = root.querySelector("tbody");
927 if (!tbody) {
928 return;
929 }
930 let needle = null;
931 if (ids !== "all") {
932 needle = /* @__PURE__ */ new Set();
933 for (const id of ids) {
934 needle.add(String(id));
935 }
936 }
937 const rows = tbody.querySelectorAll(
938 "tr[data-row-id]"
939 );
940 for (const tr of rows) {
941 const rowIdStr = tr.dataset.rowId;
942 if (rowIdStr === void 0) {
943 continue;
944 }
945 if (needle && !needle.has(rowIdStr)) {
946 continue;
947 }
948 const idx = Number(tr.dataset.rowIndex);
949 if (!Number.isFinite(idx)) {
950 continue;
951 }
952 const row = this._data[idx];
953 if (row === void 0) {
954 continue;
955 }
956 const id = this._getRowId(row, idx);
957 const isSelected = this._selection.has(id);
958 tr.classList.toggle("is-selected", isSelected);
959 const cb = tr.querySelector(
960 "input.select-row-checkbox"
961 );
962 if (cb && cb.checked !== isSelected) {
963 cb.checked = isSelected;
964 }
965 }
966 const headerCb = root.querySelector(
967 "thead .select-all-checkbox"
968 );
969 if (headerCb) {
970 const { total, selected } = this._visibleSelectionStats();
971 headerCb.checked = total > 0 && selected === total;
972 headerCb.indeterminate = selected > 0 && selected < total;
973 }
974 }
975 /** Scroll the (filtered) row at `index` into view inside the table's scroll container. */
976 scrollToRow(index) {
977 const root = this.shadowRoot;
978 if (!root) {
979 return;
980 }
981 const rows = root.querySelectorAll(
982 "tbody tr:not(.subtable):not(.empty):not(.skeleton)"
983 );
984 const row = rows[index];
985 if (row) {
986 row.scrollIntoView({ block: "nearest", inline: "nearest" });
987 }
988 }
989 connectedCallback() {
990 super.connectedCallback();
991 this._schedulePaint();
992 }
993 disconnectedCallback() {
994 this._resizeObserver?.disconnect();
995 this._resizeObserver = null;
996 if (this._stickyRafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
997 cancelAnimationFrame(this._stickyRafHandle);
998 this._stickyRafHandle = null;
999 }
1000 }
1001 /**
1002 * Force a sticky-offsets recompute. Public escape hatch for the
1003 * rare case where layout settles after every internal hook has
1004 * fired — e.g. an out-of-band font swap or a JS-driven width
1005 * change on an ancestor that doesn't bubble through ResizeObserver.
1006 *
1007 * Usually you don't need this: the component schedules recomputes
1008 * on a microtask + animation frame after every paint, and a
1009 * ResizeObserver on the inner scroll element catches geometry
1010 * changes thereafter. Reach for `recomputeLayout()` only if you've
1011 * confirmed that all of those pathways missed your case.
1012 */
1013 recomputeLayout() {
1014 this._applyStickyOffsets();
1015 this._measureHeaderHeight();
1016 }
1017 // ------------------------------------------------------------------
1018 // Skeleton + paint pipeline
1019 // ------------------------------------------------------------------
1020 render() {
1021 return html`
1022 <div class="scroll" part="scroll">
1023 <table part="table">
1024 <colgroup></colgroup>
1025 <thead></thead>
1026 <tbody></tbody>
1027 </table>
1028 </div>
1029 `;
1030 }
1031 requestUpdate() {
1032 super.requestUpdate();
1033 this._schedulePaint();
1034 }
1035 _schedulePaint() {
1036 if (this._paintScheduled || !this.isConnected) {
1037 return;
1038 }
1039 this._paintScheduled = true;
1040 queueMicrotask(() => {
1041 this._paintScheduled = false;
1042 if (!this.isConnected) {
1043 return;
1044 }
1045 this._paint();
1046 });
1047 }
1048 _paint() {
1049 const root = this.shadowRoot;
1050 if (!root) {
1051 return;
1052 }
1053 if (!root.querySelector("tbody")) {
1054 render(this.render(), root);
1055 }
1056 const colgroup = root.querySelector("colgroup");
1057 const thead = root.querySelector("thead");
1058 const tbody = root.querySelector("tbody");
1059 if (!colgroup || !thead || !tbody) {
1060 return;
1061 }
1062 const cols = this._effectiveColumns();
1063 const stickyN = this._readStickyColumns();
1064 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1065 this._paintColgroup(colgroup, cols);
1066 this._paintHead(thead, cols, stickyN);
1067 this._paintBody(tbody, cols, stickyN);
1068 this._applyStickyOffsets();
1069 this._measureHeaderHeight();
1070 this._scheduleStickyOffsets();
1071 this._maybeWarnStickyHeader();
1072 this._maybeWarnLoadingDesync(tbody);
1073 this._ensureResizeObserver();
1074 }
1075 /**
1076 * Diagnostic for the "I set `loading` but the skeleton never
1077 * appeared" footgun. If we get here with the attribute on but no
1078 * `.skeleton` rows in `tbody`, something between attribute set and
1079 * paint went off the rails — historically this happened when the
1080 * base `Component.attributeChangedCallback` called `_scheduleRender`
1081 * directly, bypassing our `requestUpdate` override. Same pattern as
1082 * the sticky-columns 0px tripwire: should never fire, but if it
1083 * does, names the bug instead of leaving the dev guessing.
1084 */
1085 _maybeWarnLoadingDesync(tbody) {
1086 if (this._loadingDesyncWarned) {
1087 return;
1088 }
1089 if (!this.hasAttribute("loading")) {
1090 return;
1091 }
1092 if (tbody.querySelector("tr.skeleton")) {
1093 return;
1094 }
1095 this._loadingDesyncWarned = true;
1096 console.warn(
1097 "[wpd-table] `loading` attribute is set but no skeleton rows rendered. Either attributeChangedCallback didn't route through requestUpdate (framework regression), or `loading` was set after the most recent paint and no follow-up trigger ran. Toggling `data` will force a paint as a workaround."
1098 );
1099 }
1100 /**
1101 * Belt-and-braces sticky-offset scheduling.
1102 *
1103 * - Microtask: cheap, fires after the current task drains. Fixes
1104 * mounts where the synchronous read in `_paint` happened before
1105 * a sibling style applied.
1106 * - rAF: fires before the next paint. Catches "layout settles
1107 * after a queued style mutation" races — the most common cause
1108 * of "col 1 ended up at inset-inline-start: 0px".
1109 *
1110 * Both reduce to a no-op when nothing changed. The cost is two
1111 * extra DOM reads per paint; the win is the bug class disappears.
1112 */
1113 _scheduleStickyOffsets() {
1114 if (!this._stickyMicroScheduled) {
1115 this._stickyMicroScheduled = true;
1116 queueMicrotask(() => {
1117 this._stickyMicroScheduled = false;
1118 if (this.isConnected) {
1119 this._applyStickyOffsets();
1120 }
1121 });
1122 }
1123 if (this._stickyRafHandle === null && typeof requestAnimationFrame !== "undefined") {
1124 this._stickyRafHandle = requestAnimationFrame(() => {
1125 this._stickyRafHandle = null;
1126 if (this.isConnected) {
1127 this._applyStickyOffsets();
1128 this._measureHeaderHeight();
1129 }
1130 });
1131 }
1132 }
1133 /**
1134 * Wire a `ResizeObserver` on the inner `.scroll` element (NOT the
1135 * host). Why: the host's outer width is often pinned by its parent
1136 * panel — a vertical scrollbar appearing inside the table changes
1137 * the inner scroll-area width by ~15px without changing the host
1138 * size. Observing the host would miss that reflow and leave sticky
1139 * offsets stale.
1140 *
1141 * Idempotent — runs once after the first paint produces a real
1142 * `.scroll` element. Disconnect happens in `disconnectedCallback`.
1143 */
1144 _ensureResizeObserver() {
1145 if (this._resizeObserver) {
1146 return;
1147 }
1148 if (typeof ResizeObserver === "undefined") {
1149 return;
1150 }
1151 const scroll = this.shadowRoot?.querySelector(
1152 ".scroll"
1153 );
1154 if (!scroll) {
1155 return;
1156 }
1157 this._resizeObserver = new ResizeObserver(() => {
1158 if (!this.isConnected) {
1159 return;
1160 }
1161 this._applyStickyOffsets();
1162 this._measureHeaderHeight();
1163 this._stickyHeaderWarned = false;
1164 this._maybeWarnStickyHeader();
1165 });
1166 this._resizeObserver.observe(scroll);
1167 this._resizeObserver.observe(this);
1168 }
1169 _paintColgroup(colgroup, cols) {
1170 const out = [];
1171 for (const c of cols) {
1172 const col = document.createElement("col");
1173 if (c.width) {
1174 col.style.width = c.width;
1175 }
1176 out.push(col);
1177 }
1178 colgroup.replaceChildren(...out);
1179 }
1180 _paintHead(thead, cols, stickyN) {
1181 const newHeaderRow = document.createElement("tr");
1182 newHeaderRow.setAttribute("part", "header-row");
1183 for (let i = 0; i < cols.length; i++) {
1184 newHeaderRow.appendChild(this._buildHeaderCell(cols[i], i, stickyN));
1185 }
1186 const existingHeader = thead.querySelector(
1187 ':scope > tr[part="header-row"]'
1188 );
1189 if (existingHeader) {
1190 thead.replaceChild(newHeaderRow, existingHeader);
1191 } else {
1192 thead.insertBefore(newHeaderRow, thead.firstChild);
1193 }
1194 const hasFilter = cols.some(
1195 (c) => c.filter || Array.isArray(c.filterOptions) || typeof c.filterRender === "function"
1196 );
1197 let existingFilter = thead.querySelector(
1198 ":scope > tr.filter-row"
1199 );
1200 if (hasFilter) {
1201 const cells = [];
1202 for (let i = 0; i < cols.length; i++) {
1203 cells.push(this._buildFilterCell(cols[i], i, stickyN));
1204 }
1205 if (!existingFilter) {
1206 existingFilter = document.createElement("tr");
1207 existingFilter.classList.add("filter-row");
1208 existingFilter.setAttribute("part", "filter-row");
1209 thead.appendChild(existingFilter);
1210 }
1211 const current = Array.from(existingFilter.children);
1212 let same = current.length === cells.length;
1213 if (same) {
1214 for (let i = 0; i < cells.length; i++) {
1215 if (current[i] !== cells[i]) {
1216 same = false;
1217 break;
1218 }
1219 }
1220 }
1221 if (!same) {
1222 const wanted = new Set(cells);
1223 for (const cell of cells) {
1224 existingFilter.appendChild(cell);
1225 }
1226 for (const child of Array.from(existingFilter.children)) {
1227 if (!wanted.has(child)) {
1228 existingFilter.removeChild(child);
1229 }
1230 }
1231 }
1232 } else if (existingFilter) {
1233 existingFilter.remove();
1234 }
1235 }
1236 _buildHeaderCell(col, index, stickyN) {
1237 const th = document.createElement("th");
1238 th.setAttribute("scope", "col");
1239 th.dataset.key = col.key;
1240 this._applyCellClasses(th, col, index, stickyN);
1241 if (col.minWidth) {
1242 th.style.minWidth = col.minWidth;
1243 }
1244 if (col.key === SELECT_KEY) {
1245 const mode = this._readSelectable();
1246 if (mode === "multi") {
1247 const cb = document.createElement("input");
1248 cb.type = "checkbox";
1249 cb.className = "select-all-checkbox";
1250 cb.setAttribute("data-noclick", "");
1251 cb.setAttribute("aria-label", "Select all rows");
1252 const { total, selected } = this._visibleSelectionStats();
1253 cb.checked = total > 0 && selected === total;
1254 cb.indeterminate = selected > 0 && selected < total;
1255 cb.addEventListener("change", () => {
1256 if (cb.checked) {
1257 this.selectAll();
1258 } else {
1259 this.clearSelection();
1260 }
1261 });
1262 th.appendChild(cb);
1263 }
1264 return th;
1265 }
1266 th.textContent = col.label ?? (col.key === EXPANDER_KEY ? "" : col.key);
1267 if (col.sortable) {
1268 th.classList.add("is-sortable");
1269 const isActive = this._sort?.key === col.key;
1270 const indicator = document.createElement("span");
1271 indicator.className = "sort-indicator";
1272 let arrow = "";
1273 if (isActive) {
1274 arrow = this._sort.direction === "asc" ? " â–²" : " â–¼";
1275 }
1276 indicator.textContent = arrow;
1277 th.appendChild(indicator);
1278 if (isActive) {
1279 th.classList.add(
1280 this._sort.direction === "asc" ? "sort-asc" : "sort-desc"
1281 );
1282 }
1283 th.addEventListener("click", () => this._cycleSort(col.key));
1284 }
1285 return th;
1286 }
1287 _buildFilterCell(col, index, stickyN) {
1288 const cached = this._filterCache.get(col.key);
1289 const hasExplicitOptions = Array.isArray(col.filterOptions);
1290 const hasCustomRender = typeof col.filterRender === "function";
1291 let desiredKind;
1292 if (!col.filter && !hasExplicitOptions && !hasCustomRender || col.key === EXPANDER_KEY || col.key === SELECT_KEY) {
1293 desiredKind = "none";
1294 } else if (hasCustomRender) {
1295 desiredKind = "custom";
1296 } else if (col.filter === "select" || hasExplicitOptions) {
1297 desiredKind = "select";
1298 } else {
1299 desiredKind = "text";
1300 }
1301 if (cached && cached.kind === desiredKind) {
1302 cached.th.className = "";
1303 this._applyCellClasses(cached.th, col, index, stickyN);
1304 if (desiredKind === "select") {
1305 const select = cached.control;
1306 const opts = this._resolveFilterOptions(col);
1307 const optsKey = opts.map((o) => o.value).join("|");
1308 if (optsKey !== cached.optionsKey) {
1309 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1310 cached.optionsKey = optsKey;
1311 } else {
1312 select.value = this._filters[col.key] ?? "";
1313 }
1314 } else if (desiredKind === "text") {
1315 const input = cached.control;
1316 const want = this._filters[col.key] ?? "";
1317 if (input.value !== want && input.ownerDocument.activeElement !== input) {
1318 input.value = want;
1319 }
1320 } else if (desiredKind === "custom" && col.filterRender) {
1321 col.filterRender(cached.th, {
1322 value: this._filters[col.key] ?? "",
1323 setValue: (next) => this._onFilterChange(col.key, next),
1324 col
1325 });
1326 }
1327 return cached.th;
1328 }
1329 const th = document.createElement("th");
1330 this._applyCellClasses(th, col, index, stickyN);
1331 if (desiredKind === "none") {
1332 this._filterCache.set(col.key, {
1333 th,
1334 control: null,
1335 optionsKey: "",
1336 kind: "none"
1337 });
1338 return th;
1339 }
1340 if (desiredKind === "custom" && col.filterRender) {
1341 col.filterRender(th, {
1342 value: this._filters[col.key] ?? "",
1343 setValue: (next) => this._onFilterChange(col.key, next),
1344 col
1345 });
1346 this._filterCache.set(col.key, {
1347 th,
1348 control: null,
1349 optionsKey: "",
1350 kind: "custom"
1351 });
1352 return th;
1353 }
1354 let control;
1355 let optionsKey = "";
1356 if (desiredKind === "select") {
1357 const select = document.createElement("select");
1358 select.classList.add("filter-select");
1359 select.setAttribute("data-noclick", "");
1360 select.setAttribute(
1361 "aria-label",
1362 `Filter ${col.label ?? col.key}`
1363 );
1364 const opts = this._resolveFilterOptions(col);
1365 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1366 optionsKey = opts.map((o) => o.value).join("|");
1367 select.addEventListener("change", () => {
1368 this._onFilterChange(col.key, select.value);
1369 });
1370 control = select;
1371 } else {
1372 const input = document.createElement("input");
1373 input.type = "search";
1374 input.classList.add("filter-input");
1375 input.setAttribute("data-noclick", "");
1376 input.setAttribute("placeholder", "Filter…");
1377 input.setAttribute("aria-label", `Filter ${col.label ?? col.key}`);
1378 input.value = this._filters[col.key] ?? "";
1379 input.addEventListener("input", () => {
1380 this._onFilterChange(col.key, input.value);
1381 });
1382 control = input;
1383 }
1384 th.appendChild(control);
1385 this._filterCache.set(col.key, {
1386 th,
1387 control,
1388 optionsKey,
1389 kind: desiredKind
1390 });
1391 return th;
1392 }
1393 _populateSelect(select, options, current) {
1394 select.replaceChildren();
1395 const all = document.createElement("option");
1396 all.value = "";
1397 all.textContent = "All";
1398 select.appendChild(all);
1399 for (const opt of options) {
1400 const el = document.createElement("option");
1401 el.value = opt.value;
1402 el.textContent = opt.label;
1403 if (opt.value === current) {
1404 el.selected = true;
1405 }
1406 select.appendChild(el);
1407 }
1408 select.value = current;
1409 }
1410 /**
1411 * Resolve the option list for a select-filter column. Explicit
1412 * `filterOptions` win — that's the contract for server-driven
1413 * tables that need the dropdown to list values not present on
1414 * the current page. Without `filterOptions`, fall back to the
1415 * unique row values in the column (legacy behaviour for
1416 * client-side tables).
1417 */
1418 _resolveFilterOptions(col) {
1419 if (Array.isArray(col.filterOptions)) {
1420 return col.filterOptions;
1421 }
1422 return this._uniqueValues(col.key).map((v) => ({
1423 value: v,
1424 label: v
1425 }));
1426 }
1427 // ------------------------------------------------------------------
1428 // Body
1429 // ------------------------------------------------------------------
1430 _paintBody(tbody, cols, stickyN) {
1431 tbody.replaceChildren();
1432 if (this.hasAttribute("loading")) {
1433 const count = this._readLoadingRows();
1434 for (let i = 0; i < count; i++) {
1435 tbody.appendChild(this._buildSkeletonRow(cols, i));
1436 }
1437 return;
1438 }
1439 const filtered = this._sortedRows(this._filteredRows());
1440 if (filtered.length === 0) {
1441 tbody.appendChild(this._buildEmptyRow(cols.length));
1442 return;
1443 }
1444 for (const { row, index } of filtered) {
1445 tbody.appendChild(this._buildBodyRow(row, index, cols, stickyN));
1446 if (this._expanded.has(index) && this._subTable) {
1447 const sub = this._subTable(row, index);
1448 if (sub) {
1449 tbody.appendChild(this._buildSubTableRow(sub, cols.length));
1450 }
1451 }
1452 }
1453 }
1454 _buildEmptyRow(colspan) {
1455 const tr = document.createElement("tr");
1456 tr.classList.add("empty");
1457 const td = document.createElement("td");
1458 td.colSpan = colspan;
1459 const slot = document.createElement("slot");
1460 slot.name = "empty";
1461 slot.textContent = this.getAttribute("empty") || "No data";
1462 td.appendChild(slot);
1463 tr.appendChild(td);
1464 return tr;
1465 }
1466 _buildSkeletonRow(cols, seed) {
1467 const tr = document.createElement("tr");
1468 tr.classList.add("skeleton");
1469 tr.setAttribute("aria-hidden", "true");
1470 for (const _c of cols) {
1471 const td = document.createElement("td");
1472 const bar = document.createElement("span");
1473 bar.className = "skeleton-bar";
1474 const widthPct = 50 + (seed * 7 + tr.children.length * 13) % 40;
1475 bar.style.width = `${widthPct}%`;
1476 td.appendChild(bar);
1477 tr.appendChild(td);
1478 }
1479 return tr;
1480 }
1481 _buildBodyRow(row, rowIndex, cols, stickyN) {
1482 const tr = document.createElement("tr");
1483 tr.setAttribute("part", "row");
1484 tr.dataset.rowIndex = String(rowIndex);
1485 const id = this._getRowId(row, rowIndex);
1486 tr.dataset.rowId = String(id);
1487 if (this._selection.has(id)) {
1488 tr.classList.add("is-selected");
1489 }
1490 tr.addEventListener("click", (e) => {
1491 this._onRowClick(row, rowIndex, e);
1492 });
1493 for (let i = 0; i < cols.length; i++) {
1494 tr.appendChild(
1495 this._buildBodyCell(cols[i], i, row, rowIndex, stickyN)
1496 );
1497 }
1498 return tr;
1499 }
1500 _buildBodyCell(col, colIndex, row, rowIndex, stickyN) {
1501 const td = document.createElement("td");
1502 this._applyCellClasses(td, col, colIndex, stickyN);
1503 if (col.minWidth) {
1504 td.style.minWidth = col.minWidth;
1505 }
1506 if (col.key === SELECT_KEY) {
1507 const id = this._getRowId(row, rowIndex);
1508 const cb = document.createElement("input");
1509 cb.type = "checkbox";
1510 cb.className = "select-row-checkbox";
1511 cb.setAttribute("data-noclick", "");
1512 cb.setAttribute("aria-label", "Select row");
1513 cb.checked = this._selection.has(id);
1514 cb.addEventListener("change", () => {
1515 if (cb.checked) {
1516 this.select(id);
1517 } else {
1518 this.deselect(id);
1519 }
1520 });
1521 td.appendChild(cb);
1522 return td;
1523 }
1524 if (col.key === EXPANDER_KEY) {
1525 const hasChildren = this._subTable ? !!this._subTable(row, rowIndex) : false;
1526 if (!hasChildren) {
1527 return td;
1528 }
1529 const isOpen = this._expanded.has(rowIndex);
1530 const btn = document.createElement("button");
1531 btn.type = "button";
1532 btn.className = "expander";
1533 btn.setAttribute("data-noclick", "");
1534 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
1535 btn.setAttribute(
1536 "aria-label",
1537 isOpen ? "Collapse row" : "Expand row"
1538 );
1539 btn.textContent = isOpen ? "â–¾" : "â–¸";
1540 btn.addEventListener("click", (e) => {
1541 this._toggleRow(rowIndex, row, e);
1542 });
1543 td.appendChild(btn);
1544 return td;
1545 }
1546 const value = row[col.key];
1547 if (col.render) {
1548 const out = col.render(value, row, rowIndex);
1549 this._mountCellContent(td, out);
1550 } else if (value !== null && value !== void 0) {
1551 td.textContent = String(value);
1552 }
1553 return td;
1554 }
1555 _buildSubTableRow(sub, colspan) {
1556 const tr = document.createElement("tr");
1557 tr.classList.add("subtable");
1558 tr.setAttribute("part", "subtable-row");
1559 const td = document.createElement("td");
1560 td.colSpan = colspan;
1561 const inner = document.createElement("div");
1562 inner.classList.add("subtable-inner");
1563 if (sub instanceof Node) {
1564 inner.appendChild(sub);
1565 } else if (isTemplateResult(sub)) {
1566 render(sub, inner);
1567 } else {
1568 const nested = document.createElement("wpd-table");
1569 nested.columns = sub.columns;
1570 nested.data = sub.data;
1571 if (sub.subTable) {
1572 nested.subTable = sub.subTable;
1573 }
1574 inner.appendChild(nested);
1575 }
1576 td.appendChild(inner);
1577 tr.appendChild(td);
1578 return tr;
1579 }
1580 _mountCellContent(td, out) {
1581 if (typeof out === "string") {
1582 td.textContent = out;
1583 return;
1584 }
1585 if (out instanceof Node) {
1586 td.appendChild(out);
1587 return;
1588 }
1589 if (isTemplateResult(out)) {
1590 render(out, td);
1591 }
1592 }
1593 // ------------------------------------------------------------------
1594 // Behavior
1595 // ------------------------------------------------------------------
1596 _onFilterChange(key, value) {
1597 if (value === "") {
1598 delete this._filters[key];
1599 } else {
1600 this._filters[key] = value;
1601 }
1602 this.emit("wpd-table-filter-change", { filters: { ...this._filters } });
1603 const root = this.shadowRoot;
1604 const tbody = root?.querySelector("tbody");
1605 if (tbody) {
1606 const cols = this._effectiveColumns();
1607 const stickyN = this._readStickyColumns();
1608 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1609 this._paintBody(tbody, cols, stickyN);
1610 this._applyStickyOffsets();
1611 }
1612 }
1613 _onRowClick(row, index, e) {
1614 const path = e.composedPath?.() ?? [];
1615 for (const node of path) {
1616 if (node instanceof Element && node.hasAttribute("data-noclick")) {
1617 return;
1618 }
1619 if (node === this) {
1620 break;
1621 }
1622 }
1623 this.emit("wpd-table-row-click", { row, index, originalEvent: e });
1624 }
1625 _toggleRow(index, row, e) {
1626 e.stopPropagation();
1627 const isOpen = this._expanded.has(index);
1628 if (isOpen) {
1629 this._expanded.delete(index);
1630 } else {
1631 this._expanded.add(index);
1632 }
1633 this.emit("wpd-table-expand-change", {
1634 row,
1635 index,
1636 expanded: !isOpen
1637 });
1638 this._schedulePaint();
1639 }
1640 _cycleSort(key) {
1641 if (!this._sort || this._sort.key !== key) {
1642 this._sort = { key, direction: "asc" };
1643 } else if (this._sort.direction === "asc") {
1644 this._sort = { key, direction: "desc" };
1645 } else {
1646 this._sort = null;
1647 }
1648 this.emit("wpd-table-sort-change", {
1649 sort: this._sort ? { ...this._sort } : null
1650 });
1651 this._schedulePaint();
1652 }
1653 _emitSelectionChange() {
1654 this.emit("wpd-table-selection-change", {
1655 selection: Array.from(this._selection),
1656 rows: this.selectedRows
1657 });
1658 }
1659 // ------------------------------------------------------------------
1660 // Filtering + sorting
1661 // ------------------------------------------------------------------
1662 _filteredRows() {
1663 const out = [];
1664 const active = Object.keys(this._filters).filter(
1665 (k) => this._filters[k] !== ""
1666 );
1667 for (let i = 0; i < this._data.length; i++) {
1668 const row = this._data[i];
1669 let pass = true;
1670 for (const key of active) {
1671 const col = this._columns.find((c) => c.key === key);
1672 if (col && typeof col.filterRender === "function") {
1673 continue;
1674 }
1675 const filter = this._filters[key] ?? "";
1676 const cell = row[key];
1677 const cellStr = cell === null || cell === void 0 ? "" : String(cell);
1678 if (col?.filter === "select") {
1679 if (cellStr !== filter) {
1680 pass = false;
1681 break;
1682 }
1683 } else if (!cellStr.toLowerCase().includes(filter.toLowerCase())) {
1684 pass = false;
1685 break;
1686 }
1687 }
1688 if (pass) {
1689 out.push({ row, index: i });
1690 }
1691 }
1692 return out;
1693 }
1694 _sortedRows(rows) {
1695 if (!this._sort) {
1696 return rows;
1697 }
1698 const col = this._columns.find((c) => c.key === this._sort.key);
1699 if (!col) {
1700 return rows;
1701 }
1702 const dir = this._sort.direction === "desc" ? -1 : 1;
1703 const out = rows.slice();
1704 out.sort((a, b) => {
1705 const av = col.sortValue ? col.sortValue(a.row, a.row[col.key]) : a.row[col.key];
1706 const bv = col.sortValue ? col.sortValue(b.row, b.row[col.key]) : b.row[col.key];
1707 return compareValues(av, bv) * dir;
1708 });
1709 return out;
1710 }
1711 _uniqueValues(key) {
1712 const seen = /* @__PURE__ */ new Set();
1713 for (const row of this._data) {
1714 const v = row[key];
1715 if (v === null || v === void 0) {
1716 continue;
1717 }
1718 seen.add(String(v));
1719 }
1720 return Array.from(seen).sort();
1721 }
1722 /**
1723 * Selection stats over the VISIBLE (client-side-filtered) rows —
1724 * the same set `selectAll()` operates on. The header select-all
1725 * tri-state derives from these so "checked" always means "every
1726 * row the user can see is selected", even while ids of currently
1727 * hidden rows linger in the selection set.
1728 */
1729 _visibleSelectionStats() {
1730 let total = 0;
1731 let selected = 0;
1732 for (const { row, index } of this._filteredRows()) {
1733 total++;
1734 if (this._selection.has(this._getRowId(row, index))) {
1735 selected++;
1736 }
1737 }
1738 return { total, selected };
1739 }
1740 // ------------------------------------------------------------------
1741 // Sticky columns + attribute reads
1742 // ------------------------------------------------------------------
1743 _readStickyColumns() {
1744 const raw = parseInt(this.getAttribute("sticky-columns") || "0", 10);
1745 return Number.isFinite(raw) && raw > 0 ? raw : 0;
1746 }
1747 _readLoadingRows() {
1748 const raw = parseInt(this.getAttribute("loading-rows") || "5", 10);
1749 return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 100) : 5;
1750 }
1751 _readSelectable() {
1752 const v = this.getAttribute("selectable");
1753 if (v === "single") {
1754 return "single";
1755 }
1756 if (v === "multi" || v === "") {
1757 return "multi";
1758 }
1759 return null;
1760 }
1761 /**
1762 * Sticky-band membership. The first N columns get pinned, with two
1763 * per-column overrides: `column.sticky = true` opts in even outside
1764 * the band; `column.sticky = false` opts out within it.
1765 */
1766 _isStickyIndex(index, stickyN, col) {
1767 if (col.sticky === false) {
1768 return false;
1769 }
1770 if (col.sticky === true) {
1771 return true;
1772 }
1773 return index < stickyN;
1774 }
1775 _computeLastStickyIndex(cols, stickyN) {
1776 let last = -1;
1777 for (let i = 0; i < cols.length; i++) {
1778 if (this._isStickyIndex(i, stickyN, cols[i])) {
1779 last = i;
1780 }
1781 }
1782 return last;
1783 }
1784 _applyCellClasses(cell, col, index, stickyN) {
1785 if (col.key === EXPANDER_KEY) {
1786 cell.classList.add("col-expander");
1787 }
1788 if (col.key === SELECT_KEY) {
1789 cell.classList.add("col-select");
1790 }
1791 if (col.align === "center") {
1792 cell.classList.add("align-center");
1793 }
1794 if (col.align === "end") {
1795 cell.classList.add("align-end");
1796 }
1797 const sticky = this._isStickyIndex(index, stickyN, col);
1798 if (sticky) {
1799 cell.classList.add("is-sticky");
1800 if (index === this._lastStickyIndex) {
1801 cell.classList.add("is-sticky-edge");
1802 }
1803 }
1804 }
1805 _effectiveColumns() {
1806 const out = [];
1807 if (this._readSelectable()) {
1808 out.push({
1809 key: SELECT_KEY,
1810 label: "",
1811 // The descriptor width is painted onto a `<col>`
1812 // element and is the authoritative column-width
1813 // source in table-layout: auto — CSS `td { width }`
1814 // is ignored once `<col>` has a value. Pair with
1815 // the matching `td.col-select` rule (zero
1816 // `padding-inline`, `text-align: center`) so the
1817 // checkbox sits with breathing room on both sides.
1818 width: "40px",
1819 align: "center"
1820 });
1821 }
1822 if (this._subTable) {
1823 out.push({
1824 key: EXPANDER_KEY,
1825 label: "",
1826 // Same contract as col-select. 36px column +
1827 // 20px button + zero padding centers the chevron
1828 // with ~8px on each side.
1829 width: "36px",
1830 align: "center"
1831 });
1832 }
1833 out.push(...this._columns);
1834 return out;
1835 }
1836 /**
1837 * Walk the header row, sum the natural widths of the sticky cells,
1838 * then write cumulative `inset-inline-start` offsets onto every
1839 * row's matching cells.
1840 */
1841 _applyStickyOffsets() {
1842 const root = this.shadowRoot;
1843 if (!root) {
1844 return;
1845 }
1846 const headRow = root.querySelector("thead tr");
1847 if (!headRow) {
1848 return;
1849 }
1850 const ths = Array.from(headRow.children);
1851 const offsets = [];
1852 let acc = 0;
1853 for (let i = 0; i < ths.length; i++) {
1854 offsets[i] = acc;
1855 if (ths[i].classList.contains("is-sticky")) {
1856 acc += ths[i].offsetWidth;
1857 }
1858 }
1859 const rows = root.querySelectorAll(
1860 "thead tr, tbody tr:not(.subtable):not(.empty):not(.skeleton)"
1861 );
1862 rows.forEach((r) => {
1863 const cells = Array.from(r.children);
1864 for (let i = 0; i < cells.length; i++) {
1865 if (cells[i].classList.contains("is-sticky")) {
1866 cells[i].style.insetInlineStart = `${offsets[i]}px`;
1867 }
1868 }
1869 });
1870 this._maybeWarnStickyOffsetRace(ths, offsets);
1871 }
1872 _maybeWarnStickyOffsetRace(ths, offsets) {
1873 if (this._stickyRaceWarned) {
1874 return;
1875 }
1876 const stickyN = this._readStickyColumns();
1877 if (stickyN < 2) {
1878 return;
1879 }
1880 const lastIdx = Math.min(stickyN - 1, ths.length - 1);
1881 if (lastIdx <= 0) {
1882 return;
1883 }
1884 if (offsets[lastIdx] !== 0) {
1885 return;
1886 }
1887 if (this.offsetWidth === 0) {
1888 return;
1889 }
1890 this._stickyRaceWarned = true;
1891 const w0 = ths[0]?.offsetWidth ?? 0;
1892 console.warn(
1893 `[wpd-table] sticky-columns: column ${lastIdx} resolved to inset-inline-start: 0px while the host is visible. ths[0].offsetWidth was ${w0}px at measurement time. Likely a layout race — call recomputeLayout() after the panel finishes its mount/transition, or wrap the assignment of \`data\` in a requestAnimationFrame.`
1894 );
1895 }
1896 _measureHeaderHeight() {
1897 const root = this.shadowRoot;
1898 if (!root) {
1899 return;
1900 }
1901 const headRow = root.querySelector("thead tr");
1902 if (!headRow) {
1903 return;
1904 }
1905 const h = headRow.offsetHeight;
1906 if (h > 0) {
1907 this.style.setProperty("--wpd-table-header-height", `${h}px`);
1908 }
1909 }
1910 /**
1911 * Once-per-element warning for the most common sticky-header
1912 * mistake: forgetting to give the table a scroll container. Without
1913 * a max-height (or a scrolling ancestor), `position: sticky`
1914 * silently does nothing because there's no scrollport for it to
1915 * stick within.
1916 */
1917 _maybeWarnStickyHeader() {
1918 if (this._stickyHeaderWarned) {
1919 return;
1920 }
1921 if (!this.hasAttribute("sticky-header")) {
1922 return;
1923 }
1924 if (this.hasAttribute("loading") || this._data.length < 8) {
1925 return;
1926 }
1927 const scroll = this.shadowRoot?.querySelector(
1928 ".scroll"
1929 );
1930 if (!scroll) {
1931 return;
1932 }
1933 if (scroll.offsetWidth === 0) {
1934 return;
1935 }
1936 if (scroll.scrollHeight <= scroll.clientHeight + 1) {
1937 this._stickyHeaderWarned = true;
1938 console.warn(
1939 "[wpd-table] sticky-header is set but the table has no scroll container. Set --wpd-table-max-height on the host (or wrap it in a scrolling parent) so the header has something to stick to."
1940 );
1941 }
1942 }
1943 };
1944 _WpdTable.props = [
1945 "stickyColumns",
1946 "stickyHeader",
1947 "striped",
1948 "hover",
1949 "compact",
1950 "bordered",
1951 "empty",
1952 "loading",
1953 "loadingRows",
1954 "selectable"
1955 ];
1956 _WpdTable.styles = [styles$1];
1957 _WpdTable.help = {
1958 title: "Table",
1959 summary: "Data-driven table. Assign `columns` + `data` and you get a styled table with optional per-column filters, click-to-sort, multi-row selection, sticky columns/header, sub-tables, custom cell renderers, loading skeleton, and a slottable empty state.",
1960 status: "experimental",
1961 since: "0.6.0",
1962 props: [
1963 {
1964 name: "sticky-columns",
1965 type: "integer",
1966 description: "Pin the first N columns to the inline-start edge. Widths are measured after layout, so variable-width columns work. The auto-injected expander (subTable) and select (selectable) columns count toward N."
1967 },
1968 {
1969 name: "sticky-header",
1970 type: "boolean",
1971 description: "Pin the header (and filter row) to the top. Requires a scrolling parent or `--wpd-table-max-height` — the component warns once if it detects sticky-header on a non-scrolling container."
1972 },
1973 { name: "striped", type: "boolean", description: "Zebra rows." },
1974 { name: "hover", type: "boolean", description: "Highlight rows on hover." },
1975 { name: "compact", type: "boolean", description: "Tighter padding + smaller font." },
1976 { name: "bordered", type: "boolean", description: "Vertical cell borders." },
1977 {
1978 name: "empty",
1979 type: "string",
1980 description: "Fallback text shown when there are no rows. For richer empty states, project light-DOM content into the `empty` slot."
1981 },
1982 {
1983 name: "loading",
1984 type: "boolean",
1985 description: "Paint shimmering skeleton rows in place of body content. Filters / sort headers stay live."
1986 },
1987 {
1988 name: "loading-rows",
1989 type: "integer",
1990 description: "Number of skeleton rows when loading. Default 5."
1991 },
1992 {
1993 name: "selectable",
1994 type: '"single" | "multi"',
1995 description: "Auto-prepend a checkbox column. `multi` puts a select-all checkbox in the header; `single` enforces at-most-one selected."
1996 }
1997 ],
1998 events: [
1999 { name: "wpd-table-filter-change", description: "Filter input changed." },
2000 { name: "wpd-table-sort-change", description: "Header click cycled the sort." },
2001 { name: "wpd-table-selection-change", description: "Selection set changed." },
2002 { name: "wpd-table-row-click", description: "Body row clicked (skips data-noclick descendants)." },
2003 { name: "wpd-table-expand-change", description: "Sub-table toggled." }
2004 ],
2005 slots: [
2006 { name: "empty", description: "Custom empty-state content (CTA, illustration, etc.)." }
2007 ],
2008 cssProps: [
2009 { name: "--wpd-table-bg" },
2010 { name: "--wpd-table-border" },
2011 { name: "--wpd-table-column-border" },
2012 { name: "--wpd-table-header-bg" },
2013 { name: "--wpd-table-row-hover" },
2014 { name: "--wpd-table-stripe" },
2015 { name: "--wpd-table-cell-padding" },
2016 { name: "--wpd-table-font-size" },
2017 { name: "--wpd-table-max-height" },
2018 { name: "--wpd-table-skeleton-color" }
2019 ],
2020 example: html`
2021 <wpd-table id="sample-table" sticky-header striped hover></wpd-table>
2022 `
2023 };
2024 let WpdTable = _WpdTable;
2025 function isTemplateResult(v) {
2026 return !!v && v.__wpdHtml === true;
2027 }
2028 function compareValues(a, b) {
2029 if (a === b) {
2030 return 0;
2031 }
2032 if (a === null || a === void 0) {
2033 return -1;
2034 }
2035 if (b === null || b === void 0) {
2036 return 1;
2037 }
2038 if (typeof a === "number" && typeof b === "number") {
2039 return a - b;
2040 }
2041 if (a instanceof Date && b instanceof Date) {
2042 return a.getTime() - b.getTime();
2043 }
2044 const an = Number(a);
2045 const bn = Number(b);
2046 if (Number.isFinite(an) && Number.isFinite(bn)) {
2047 return an - bn;
2048 }
2049 return String(a).localeCompare(String(b));
2050 }
2051 defineComponent("wpd-table", WpdTable);
2052 const styles = css`:host{display:inline;color:inherit;font:inherit}`;
2053 const _instances = /* @__PURE__ */ new Set();
2054 let _ticker = null;
2055 const TICK_INTERVAL_MS = 3e4;
2056 function startTicker() {
2057 if (_ticker !== null) {
2058 return;
2059 }
2060 _ticker = window.setInterval(() => {
2061 for (const i of _instances) {
2062 i.tick();
2063 }
2064 }, TICK_INTERVAL_MS);
2065 }
2066 function stopTickerIfIdle() {
2067 if (_ticker !== null && _instances.size === 0) {
2068 window.clearInterval(_ticker);
2069 _ticker = null;
2070 }
2071 }
2072 function parseDatetime(raw) {
2073 if (!raw) {
2074 return null;
2075 }
2076 const tryDate = (v) => {
2077 const d = new Date(v);
2078 return Number.isNaN(d.getTime()) ? null : d;
2079 };
2080 if (raw.includes("T") || raw.endsWith("Z")) {
2081 return tryDate(raw);
2082 }
2083 return tryDate(raw.replace(" ", "T") + "Z");
2084 }
2085 let _rtfCache = null;
2086 function getRtf() {
2087 if (!_rtfCache) {
2088 const lang = typeof navigator !== "undefined" && navigator.language || "en";
2089 _rtfCache = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
2090 }
2091 return _rtfCache;
2092 }
2093 function relativeText(date, now) {
2094 const rtf = getRtf();
2095 const diffMs = date.getTime() - now;
2096 const diffSec = Math.round(diffMs / 1e3);
2097 const abs = Math.abs;
2098 if (abs(diffSec) < 45) {
2099 return rtf.format(0, "second");
2100 }
2101 const diffMin = Math.round(diffSec / 60);
2102 if (abs(diffMin) < 45) {
2103 return rtf.format(diffMin, "minute");
2104 }
2105 const diffHour = Math.round(diffMin / 60);
2106 if (abs(diffHour) < 22) {
2107 return rtf.format(diffHour, "hour");
2108 }
2109 const diffDay = Math.round(diffHour / 24);
2110 if (abs(diffDay) < 26) {
2111 return rtf.format(diffDay, "day");
2112 }
2113 const diffMonth = Math.round(diffDay / 30);
2114 if (abs(diffMonth) < 11) {
2115 return rtf.format(diffMonth, "month");
2116 }
2117 const diffYear = Math.round(diffDay / 365);
2118 return rtf.format(diffYear, "year");
2119 }
2120 const _WpdRelativeTime = class _WpdRelativeTime extends Component {
2121 connectedCallback() {
2122 super.connectedCallback();
2123 _instances.add(this);
2124 startTicker();
2125 }
2126 disconnectedCallback() {
2127 _instances.delete(this);
2128 stopTickerIfIdle();
2129 }
2130 /** Public — the shared ticker calls this on every interval. */
2131 tick() {
2132 this.requestUpdate();
2133 }
2134 render() {
2135 const raw = this.datetime;
2136 const date = parseDatetime(raw);
2137 if (!date) {
2138 return html`<span>${raw ?? ""}</span>`;
2139 }
2140 const text = relativeText(date, Date.now());
2141 const absolute = date.toLocaleString();
2142 return html`<time datetime=${date.toISOString()} title=${absolute}
2143 >${text}</time
2144 >`;
2145 }
2146 };
2147 _WpdRelativeTime.props = ["datetime"];
2148 _WpdRelativeTime.styles = [styles];
2149 _WpdRelativeTime.help = {
2150 title: "Relative time",
2151 summary: 'Auto-ticking relative timestamp. Renders "5 minutes ago" / "yesterday" / "in 3 hours" via Intl.RelativeTimeFormat and updates itself every 30s while connected. Useful for any list cell that should age live (recycle bin, notifications, activity log) without forcing the surrounding view to repaint.',
2152 status: "experimental",
2153 since: "0.6.0",
2154 props: [
2155 {
2156 name: "datetime",
2157 type: 'ISO 8601 string OR MySQL-style "Y-m-d H:i:s" (treated as UTC)',
2158 description: "The moment the relative copy is anchored to. Accepts the format WordPress hands back from `*_gmt` columns directly."
2159 }
2160 ],
2161 slots: [],
2162 cssProps: [],
2163 example: html`<wpd-relative-time
2164 datetime="${new Date(Date.now() - 1e3 * 60 * 5).toISOString()}"
2165 ></wpd-relative-time>`
2166 };
2167 let WpdRelativeTime = _WpdRelativeTime;
2168 defineComponent("wpd-relative-time", WpdRelativeTime);
2169 const segmentedStyles = css`:host{display:inline-flex;padding:3px;background:var( --wpd-segmented-bg,rgba( 0,0,0,0.05 ) );border-radius:7px;gap:2px}`;
2170 const segmentStyles = css`:host{flex:1 1 auto;min-width:0}button{appearance:none;display:block;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;font-size:13px;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:5px;transition:background-color 0.12s ease,color 0.12s ease;white-space:nowrap}:host( [ aria-checked='true' ] ) button{background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );box-shadow:0 1px 3px rgba( 0,0,0,0.12 );font-weight:500}`;
2171 const _WpdSegment = class _WpdSegment extends Component {
2172 render() {
2173 this.setAttribute("role", "radio");
2174 return html`
2175 <button type="button" @click=${() => this._onPick()}>
2176 <slot></slot>
2177 </button>
2178 `;
2179 }
2180 _onPick() {
2181 this.emit("wpd-segment-pick", {
2182 value: this.value
2183 });
2184 }
2185 };
2186 _WpdSegment.props = ["value"];
2187 _WpdSegment.styles = [segmentStyles];
2188 _WpdSegment.help = {
2189 title: "Segment",
2190 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
2191 status: "stable",
2192 since: "0.9.0",
2193 props: [
2194 {
2195 name: "value",
2196 type: "string",
2197 description: "Identifier this segment contributes to the parent group selection."
2198 }
2199 ],
2200 slots: [
2201 { name: "(default)", description: "Visible segment label." }
2202 ],
2203 events: [
2204 {
2205 name: "wpd-segment-pick",
2206 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
2207 detail: "{ value: string }"
2208 }
2209 ]
2210 };
2211 let WpdSegment = _WpdSegment;
2212 defineComponent("wpd-segment", WpdSegment);
2213 const _WpdSegmented = class _WpdSegmented extends Component {
2214 connectedCallback() {
2215 super.connectedCallback();
2216 this.addEventListener("wpd-segment-pick", (e) => {
2217 const detail = e.detail;
2218 e.stopPropagation();
2219 this.value = detail.value;
2220 this.emit("wpd-pick", { value: detail.value });
2221 });
2222 }
2223 /**
2224 * Declarative item-list setter. Replaces the existing
2225 * `<wpd-segment>` children with a fresh set built from a
2226 * `{ value, label }` array; preserves the current selection
2227 * when the value still matches an entry, otherwise falls back
2228 * to the first item.
2229 *
2230 * Collapses the pre-0.11 imperative dance (clear children,
2231 * `createElement`, set `textContent`, `appendChild`, then
2232 * `setAttribute('value', …)` on the group — order matters) to
2233 * a single assignment:
2234 *
2235 * ```js
2236 * segmented.items = [
2237 * { value: 'm', label: 'm' },
2238 * { value: 'km', label: 'km' },
2239 * ];
2240 * ```
2241 *
2242 * @since 0.5.0
2243 */
2244 set items(list) {
2245 const existing = this.querySelectorAll(":scope > wpd-segment");
2246 for (const el of Array.from(existing)) {
2247 el.remove();
2248 }
2249 for (const item of list) {
2250 const seg = document.createElement("wpd-segment");
2251 seg.setAttribute("value", item.value);
2252 seg.textContent = item.label;
2253 this.appendChild(seg);
2254 }
2255 const current = this.value;
2256 const stillValid = current !== null && list.some((i) => i.value === current);
2257 if (!stillValid && list.length > 0) {
2258 this.value = list[0].value;
2259 } else {
2260 this.requestUpdate();
2261 }
2262 }
2263 render() {
2264 const label = this.label || "";
2265 if (label) {
2266 this.setAttribute("aria-label", label);
2267 }
2268 this.setAttribute("role", "radiogroup");
2269 const current = this.value;
2270 queueMicrotask(() => {
2271 const segs = this.querySelectorAll("wpd-segment");
2272 for (const seg of Array.from(segs)) {
2273 const v = seg.getAttribute("value");
2274 seg.setAttribute(
2275 "aria-checked",
2276 v === current ? "true" : "false"
2277 );
2278 }
2279 });
2280 return html`<slot></slot>`;
2281 }
2282 };
2283 _WpdSegmented.props = ["value", "label"];
2284 _WpdSegmented.styles = [segmentedStyles];
2285 _WpdSegmented.help = {
2286 title: "Segmented",
2287 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
2288 status: "stable",
2289 since: "0.9.0",
2290 props: [
2291 {
2292 name: "value",
2293 type: "string",
2294 description: "Currently selected segment value. Mirrored onto child aria-checked."
2295 },
2296 {
2297 name: "label",
2298 type: "string",
2299 description: "aria-label for the radiogroup."
2300 }
2301 ],
2302 slots: [
2303 { name: "(default)", description: '<wpd-segment value="…"> children.' }
2304 ],
2305 events: [
2306 {
2307 name: "wpd-pick",
2308 description: "Fires when the selected segment changes.",
2309 detail: "{ value: string }"
2310 }
2311 ],
2312 cssProps: [
2313 { name: "--desktop-mode-window-bg", description: "Pill background." },
2314 { name: "--desktop-mode-text", description: "Active label colour." },
2315 { name: "--desktop-mode-muted", description: "Inactive label colour." }
2316 ],
2317 example: html`
2318 <wpd-segmented value="md" label="Dock size">
2319 <wpd-segment value="sm">Small</wpd-segment>
2320 <wpd-segment value="md">Medium</wpd-segment>
2321 <wpd-segment value="lg">Large</wpd-segment>
2322 </wpd-segmented>
2323 `
2324 };
2325 let WpdSegmented = _WpdSegmented;
2326 defineComponent("wpd-segmented", WpdSegmented);
2327 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
2328 function resolveSlot() {
2329 const w = window;
2330 let slot = w[SHARED_STORES_SLOT];
2331 if (!slot) {
2332 slot = /* @__PURE__ */ new Map();
2333 w[SHARED_STORES_SLOT] = slot;
2334 }
2335 return slot;
2336 }
2337 function createSharedStore(key, initialState) {
2338 const slot = resolveSlot();
2339 let record = slot.get(key);
2340 if (!record) {
2341 record = {
2342 state: initialState(),
2343 listeners: /* @__PURE__ */ new Set(),
2344 rebuild: initialState
2345 };
2346 slot.set(key, record);
2347 }
2348 const handle = {
2349 // `record.state` is the live reference. The getter on the
2350 // `state` field reads the latest value even if `reset()`
2351 // reassigned it to a fresh object.
2352 get state() {
2353 return record.state;
2354 },
2355 set state(next) {
2356 record.state = next;
2357 },
2358 getState() {
2359 return record.state;
2360 },
2361 notify() {
2362 for (const cb of Array.from(record.listeners)) {
2363 try {
2364 cb(record.state);
2365 } catch (err) {
2366 console.error(
2367 `[desktop-mode/shared-store:${key}] subscriber threw:`,
2368 err
2369 );
2370 }
2371 }
2372 },
2373 subscribe(cb) {
2374 record.listeners.add(cb);
2375 return () => {
2376 record.listeners.delete(cb);
2377 };
2378 },
2379 setState(patch) {
2380 const cur = record.state;
2381 if (typeof cur !== "object" || cur === null) {
2382 console.warn(
2383 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
2384 );
2385 return;
2386 }
2387 Object.assign(cur, patch);
2388 handle.notify();
2389 },
2390 reset() {
2391 const fresh = record.rebuild();
2392 const cur = record.state;
2393 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
2394 const target = cur;
2395 for (const k of Object.keys(target)) {
2396 delete target[k];
2397 }
2398 Object.assign(target, fresh);
2399 } else {
2400 record.state = fresh;
2401 }
2402 record.listeners.clear();
2403 }
2404 };
2405 return handle;
2406 }
2407 const LOG_PREFIX = "[desktop-mode-bin badge]";
2408 function log(...args) {
2409 try {
2410 if (window.localStorage?.getItem("desktopModeBinDebug")) {
2411 console.info(LOG_PREFIX, ...args);
2412 }
2413 } catch {
2414 }
2415 }
2416 const TARGET_ID = "desktop-mode-recycle-bin";
2417 function getDesktopApi() {
2418 return window.wp?.desktop;
2419 }
2420 const store = createSharedStore(
2421 "desktop-mode/recycle-bin/badge",
2422 () => ({
2423 current: 0,
2424 seenTs: 0,
2425 started: false,
2426 countUrl: ""
2427 })
2428 );
2429 function setRecycleBinBadge(next) {
2430 const safe = Math.max(0, Math.floor(next));
2431 const prev = store.state.current;
2432 store.state.current = safe;
2433 log("setRecycleBinBadge", { prev, next: safe });
2434 paintBadge(safe);
2435 }
2436 function paintBadge(count) {
2437 const desktop = getDesktopApi();
2438 const active = isBinWindowActive();
2439 const visible = active ? 0 : count;
2440 log("paintBadge", { count, visible, active });
2441 desktop?.dock?.setBadge?.(TARGET_ID, visible);
2442 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
2443 desktop?.icons?.setBadge?.(TARGET_ID, visible);
2444 }
2445 function isBinWindowActive() {
2446 const mgr = getDesktopApi()?.windowManager;
2447 if (mgr?.isActiveByBaseId) {
2448 return mgr.isActiveByBaseId(TARGET_ID);
2449 }
2450 return !!mgr?.isActive?.(TARGET_ID);
2451 }
2452 const DEFAULT_MAX_ITERATIONS = 1e3;
2453 async function runEmptyLoop(options) {
2454 const { emptyBin: emptyBin2, onProgress, maxIterations = DEFAULT_MAX_ITERATIONS } = options;
2455 let purged = 0;
2456 let skipped = 0;
2457 let initialTotal = 0;
2458 let remaining = 0;
2459 let stoppedBecause = "iteration-cap";
2460 for (let i = 0; i < maxIterations; i++) {
2461 const result = await emptyBin2();
2462 purged += result.purged;
2463 skipped += result.skipped;
2464 remaining = result.remaining;
2465 if (i === 0) {
2466 initialTotal = purged + result.remaining;
2467 }
2468 onProgress?.({ purged, skipped, initialTotal });
2469 if (result.remaining === 0) {
2470 stoppedBecause = "empty";
2471 break;
2472 }
2473 if (result.purged === 0 && result.skipped > 0) {
2474 stoppedBecause = "no-progress";
2475 break;
2476 }
2477 }
2478 return { purged, skipped, initialTotal, remaining, stoppedBecause };
2479 }
2480 const EVENT_NAME = "desktop-mode-recycle-bin-changed";
2481 const HEARTBEAT_FIELD = "desktop_mode_recycle_bin_seen_ts";
2482 const POSTMESSAGE_TYPE = "desktop-mode-recycle-bin-changed";
2483 const state = {
2484 started: false,
2485 seenTs: 0,
2486 postMessageHandler: null,
2487 heartbeatSendHandler: null,
2488 heartbeatTickHandler: null
2489 };
2490 function dispatchChanged(source, ts) {
2491 const detail = {
2492 kind: "external",
2493 ok: 0,
2494 errors: [],
2495 source,
2496 ts
2497 };
2498 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
2499 const hooks = window.wp?.hooks;
2500 if (hooks && typeof hooks.doAction === "function") {
2501 hooks.doAction("desktop_mode.recycleBin.changed", detail);
2502 }
2503 }
2504 function start() {
2505 if (state.started) {
2506 return;
2507 }
2508 state.started = true;
2509 state.seenTs = Date.now();
2510 const expectedOrigin = window.location.origin;
2511 state.postMessageHandler = (e) => {
2512 if (e.origin !== expectedOrigin) {
2513 return;
2514 }
2515 const data = e.data;
2516 if (!data || data.type !== POSTMESSAGE_TYPE) {
2517 return;
2518 }
2519 const ts = typeof data.ts === "number" ? data.ts : Date.now();
2520 if (ts <= state.seenTs) {
2521 return;
2522 }
2523 state.seenTs = ts;
2524 dispatchChanged("chromeless", ts);
2525 };
2526 window.addEventListener("message", state.postMessageHandler);
2527 const $ = window.jQuery;
2528 if (!$) {
2529 return;
2530 }
2531 state.heartbeatSendHandler = (...args) => {
2532 const data = args[1];
2533 if (data) {
2534 data[HEARTBEAT_FIELD] = state.seenTs;
2535 }
2536 };
2537 $(document).on("heartbeat-send", state.heartbeatSendHandler);
2538 state.heartbeatTickHandler = (...args) => {
2539 const response = args[1];
2540 const block = response?.desktop_mode_recycle_bin;
2541 if (!block) {
2542 return;
2543 }
2544 const ts = typeof block.ts === "number" ? block.ts : 0;
2545 if (ts > state.seenTs) {
2546 state.seenTs = ts;
2547 if (block.changed) {
2548 dispatchChanged("heartbeat", ts);
2549 }
2550 }
2551 };
2552 $(document).on("heartbeat-tick", state.heartbeatTickHandler);
2553 }
2554 function stop() {
2555 if (!state.started) {
2556 return;
2557 }
2558 state.started = false;
2559 if (state.postMessageHandler) {
2560 window.removeEventListener("message", state.postMessageHandler);
2561 state.postMessageHandler = null;
2562 }
2563 const $ = window.jQuery;
2564 if ($) {
2565 if (state.heartbeatSendHandler) {
2566 $(document).off("heartbeat-send", state.heartbeatSendHandler);
2567 }
2568 if (state.heartbeatTickHandler) {
2569 $(document).off("heartbeat-tick", state.heartbeatTickHandler);
2570 }
2571 }
2572 state.heartbeatSendHandler = null;
2573 state.heartbeatTickHandler = null;
2574 }
2575 const NONCE_HEADER = "X-WP-Nonce";
2576 function injectRestNonce(input, init) {
2577 const nonce = readRestNonce();
2578 if (!nonce) {
2579 return init;
2580 }
2581 const url = resolveUrl(input);
2582 if (!url || !isSameOriginRestUrl(url)) {
2583 return init;
2584 }
2585 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
2586 const headers = new Headers(baseHeaders ?? {});
2587 if (headers.has(NONCE_HEADER)) {
2588 return init;
2589 }
2590 headers.set(NONCE_HEADER, nonce);
2591 return { ...init ?? {}, headers };
2592 }
2593 function readRestNonce() {
2594 if (typeof window === "undefined") {
2595 return void 0;
2596 }
2597 const cfg = window.desktopModeConfig;
2598 const value = cfg?.restNonce;
2599 return typeof value === "string" && value.length > 0 ? value : void 0;
2600 }
2601 function resolveUrl(input) {
2602 try {
2603 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
2604 if (typeof input === "string") {
2605 return new URL(input, base);
2606 }
2607 if (input instanceof URL) {
2608 return input;
2609 }
2610 if (typeof Request !== "undefined" && input instanceof Request) {
2611 return new URL(input.url, base);
2612 }
2613 return null;
2614 } catch {
2615 return null;
2616 }
2617 }
2618 function isSameOriginRestUrl(url) {
2619 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
2620 return false;
2621 }
2622 if (url.pathname.includes("/wp-json/")) {
2623 return true;
2624 }
2625 if (url.searchParams.has("rest_route")) {
2626 return true;
2627 }
2628 return false;
2629 }
2630 function trackedFetch(input, init, opts = {}) {
2631 const fn = window.wp?.desktop?.fetch;
2632 if (typeof fn === "function") {
2633 return fn(input, init, opts);
2634 }
2635 const finalInit = injectRestNonce(input, init);
2636 return fetch(input, finalInit);
2637 }
2638 function config() {
2639 const cfg = window.desktopModeRecycleBinConfig;
2640 if (!cfg) {
2641 throw new Error(
2642 "desktopModeRecycleBinConfig is missing — config blob did not reach the page. This typically means the recycle-bin script handle was lazy-loaded by desktop-mode without its `wp_localize_script` data being included in the payload. See docs/examples/window-with-config.md."
2643 );
2644 }
2645 return cfg;
2646 }
2647 async function request(url, init) {
2648 const cfg = config();
2649 const response = await trackedFetch(
2650 url,
2651 {
2652 ...init,
2653 credentials: "same-origin",
2654 headers: {
2655 "X-WP-Nonce": cfg.restNonce,
2656 Accept: "application/json",
2657 ...init.body ? { "Content-Type": "application/json" } : {},
2658 ...init.headers ?? {}
2659 }
2660 },
2661 { source: "desktop-mode/recycle-bin" }
2662 );
2663 if (!response.ok) {
2664 let message = `${response.status} ${response.statusText}`;
2665 try {
2666 const json = await response.json();
2667 if (json && typeof json.message === "string") {
2668 message = json.message;
2669 }
2670 } catch {
2671 }
2672 throw new Error(message);
2673 }
2674 return await response.json();
2675 }
2676 function fetchList(params = {}) {
2677 const url = new URL(config().listUrl);
2678 if (params.page) {
2679 url.searchParams.set("page", String(params.page));
2680 }
2681 if (params.perPage) {
2682 url.searchParams.set("per_page", String(params.perPage));
2683 }
2684 if (params.type) {
2685 url.searchParams.set("type", params.type);
2686 }
2687 if (params.search) {
2688 url.searchParams.set("search", params.search);
2689 }
2690 return request(url.toString(), { method: "GET" });
2691 }
2692 function restoreItems(items) {
2693 return request(config().restoreUrl, {
2694 method: "POST",
2695 body: JSON.stringify({ items })
2696 });
2697 }
2698 function purgeItems(items) {
2699 return request(config().purgeUrl, {
2700 method: "POST",
2701 body: JSON.stringify({ items })
2702 });
2703 }
2704 function emptyBin() {
2705 return request(config().emptyUrl, {
2706 method: "POST",
2707 body: JSON.stringify({})
2708 });
2709 }
2710 function wpdConfirmGlobal(options) {
2711 const fn = window.wp?.desktop?.confirm;
2712 if (typeof fn !== "function") {
2713 return Promise.reject(
2714 new Error(
2715 "[desktop-mode] wp.desktop.confirm is missing — the main desktop bundle must load before the recycle-bin script."
2716 )
2717 );
2718 }
2719 return fn(options);
2720 }
2721 function mapRecycleTypeToFileType(recycleType) {
2722 if (recycleType === "attachment") {
2723 return "attachment";
2724 }
2725 if (recycleType === "comment") {
2726 return "comment";
2727 }
2728 return "post";
2729 }
2730 const TYPE_BADGE_COLORS = {
2731 post: { bg: "#dbe9fe", fg: "#1d4ed8" },
2732 page: { bg: "#e0f2fe", fg: "#075985" },
2733 attachment: { bg: "#fef3c7", fg: "#92400e" },
2734 comment: { bg: "#dcfce7", fg: "#166534" },
2735 _default: { bg: "#e5e7eb", fg: "#374151" }
2736 };
2737 function humanizeType(slug) {
2738 if (!slug) {
2739 return "";
2740 }
2741 return slug.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2742 }
2743 function makeTypeBadge(row) {
2744 const label = row.type_label && row.type_label.length > 0 ? row.type_label : humanizeType(row.type);
2745 const colors = TYPE_BADGE_COLORS[row.type] ?? TYPE_BADGE_COLORS._default;
2746 const badge = document.createElement("span");
2747 badge.setAttribute("data-desktop-mode-recycle-bin-type-badge", row.type);
2748 badge.textContent = label;
2749 badge.style.cssText = [
2750 "display: inline-flex",
2751 "align-items: center",
2752 "flex-shrink: 0",
2753 "padding: 2px 8px",
2754 "border-radius: 999px",
2755 "font-size: 11px",
2756 "font-weight: 600",
2757 "line-height: 1.4",
2758 "letter-spacing: 0.2px",
2759 "text-transform: uppercase",
2760 "white-space: nowrap",
2761 "background: " + colors.bg,
2762 "color: " + colors.fg
2763 ].join(";");
2764 return badge;
2765 }
2766 const ROOT = "[data-desktop-mode-recycle-bin-root]";
2767 const FILTER = "[data-desktop-mode-recycle-bin-filter]";
2768 const SEARCH = "[data-desktop-mode-recycle-bin-search]";
2769 const REFRESH = "[data-desktop-mode-recycle-bin-refresh]";
2770 const TABLE = "[data-desktop-mode-recycle-bin-table]";
2771 const BULK = "[data-desktop-mode-recycle-bin-bulk]";
2772 const COUNT = "[data-desktop-mode-recycle-bin-count]";
2773 const RESTORE_SEL = "[data-desktop-mode-recycle-bin-restore-selected]";
2774 const PIN_TO_DESKTOP = "[data-desktop-mode-recycle-bin-pin-to-desktop]";
2775 const PURGE_SEL = "[data-desktop-mode-recycle-bin-purge-selected]";
2776 const EMPTY_BTN = "[data-desktop-mode-recycle-bin-empty]";
2777 let currentRowActionRestore = () => {
2778 };
2779 let currentRowActionPurge = () => {
2780 };
2781 const rowActionRestore = (ref) => currentRowActionRestore(ref);
2782 const rowActionPurge = (ref) => currentRowActionPurge(ref);
2783 let cachedItems = null;
2784 function itemsFingerprint(items) {
2785 if (items.length === 0) {
2786 return "";
2787 }
2788 const parts = items.map((i) => `${i.type}:${i.id}:${i.deleted_at}`).sort();
2789 return parts.join("|");
2790 }
2791 function buildColumns() {
2792 const cols = [
2793 {
2794 key: "title",
2795 label: __("Title"),
2796 sortable: true,
2797 filter: "text",
2798 render: (_v, row) => {
2799 const wrap = document.createElement("span");
2800 wrap.style.cssText = "display:flex;align-items:center;gap:10px;min-width:0;";
2801 const showsThumb = row.preview && row.type === "attachment" && row.mime.startsWith("image/");
2802 if (showsThumb) {
2803 const img = document.createElement("img");
2804 img.src = row.preview;
2805 img.alt = "";
2806 img.loading = "lazy";
2807 img.style.cssText = "width:36px;height:36px;border-radius:4px;object-fit:cover;display:block;flex-shrink:0;";
2808 wrap.appendChild(img);
2809 }
2810 const stack = document.createElement("span");
2811 stack.style.cssText = "display:flex;flex-direction:column;gap:2px;min-width:0;";
2812 const titleRow = document.createElement("span");
2813 titleRow.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
2814 titleRow.appendChild(makeTypeBadge(row));
2815 const title = document.createElement("span");
2816 title.style.cssText = "font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:320px;";
2817 title.textContent = row.title;
2818 title.title = row.title;
2819 titleRow.appendChild(title);
2820 stack.appendChild(titleRow);
2821 if (row.subtitle) {
2822 const sub = document.createElement("span");
2823 sub.style.cssText = "font-size:12px;color:#50575e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:320px;";
2824 sub.textContent = row.subtitle;
2825 sub.title = row.subtitle;
2826 stack.appendChild(sub);
2827 }
2828 wrap.appendChild(stack);
2829 return wrap;
2830 }
2831 },
2832 // No explicit Type column — the inline type badge in the
2833 // title cell and the toolbar's type filter tabs already
2834 // convey the entity kind, and an extra column inflates the
2835 // row visually for no signal gain.
2836 {
2837 key: "deleted_at",
2838 label: __("Deleted"),
2839 sortable: true,
2840 width: "180px",
2841 sortValue: (row) => Date.parse(row.deleted_at + "Z") || 0,
2842 render: (_v, row) => {
2843 const el = document.createElement("wpd-relative-time");
2844 el.setAttribute("datetime", row.deleted_at);
2845 return el;
2846 }
2847 },
2848 {
2849 key: "deleted_by",
2850 label: __("By"),
2851 sortable: true,
2852 filter: "text",
2853 width: "160px",
2854 render: (_v, row) => row.deleted_by || "—"
2855 },
2856 {
2857 key: "__actions",
2858 label: "",
2859 width: "96px",
2860 align: "end",
2861 render: (_v, row) => {
2862 const wrap = document.createElement("span");
2863 wrap.style.cssText = "display:inline-flex;gap:4px;justify-content:flex-end;align-items:center;flex-wrap:nowrap;white-space:nowrap;line-height:1;";
2864 if (row.can_restore) {
2865 wrap.appendChild(makeRowButton({
2866 label: __("Restore"),
2867 icon: "restore",
2868 onClick: () => rowActionRestore({ id: row.id, type: row.type })
2869 }));
2870 }
2871 if (row.can_purge) {
2872 wrap.appendChild(makeRowButton({
2873 label: __("Delete forever"),
2874 icon: "trash",
2875 variant: "danger",
2876 onClick: () => rowActionPurge({ id: row.id, type: row.type })
2877 }));
2878 }
2879 return wrap;
2880 }
2881 }
2882 ];
2883 const hooks = window.wp?.hooks;
2884 if (hooks && typeof hooks.applyFilters === "function") {
2885 return hooks.applyFilters(
2886 "desktop_mode.recycleBin.columns",
2887 cols
2888 );
2889 }
2890 return cols;
2891 }
2892 const ICON_SVG = {
2893 restore: '<path d="M12 5V2L7 6l5 4V7c2.76 0 5 2.24 5 5 0 .83-.21 1.61-.57 2.3l1.46 1.46A6.96 6.96 0 0 0 19 12c0-3.87-3.13-7-7-7zm0 12c-2.76 0-5-2.24-5-5 0-.83.21-1.61.57-2.3L6.11 8.24A6.96 6.96 0 0 0 5 12c0 3.87 3.13 7 7 7v3l5-4-5-4v3z" fill="currentColor"/>',
2894 trash: '<path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9zm0 5h2v9H9V8zm4 0h2v9h-2V8z" fill="currentColor"/>'
2895 };
2896 function makeRowButton(opts) {
2897 const btn = document.createElement("button");
2898 btn.type = "button";
2899 btn.setAttribute("data-noclick", "");
2900 btn.setAttribute("aria-label", opts.label);
2901 btn.title = opts.label;
2902 const isDanger = opts.variant === "danger";
2903 const restColor = isDanger ? "#d63638" : "#50575e";
2904 const restBorder = isDanger ? "#d63638" : "#c3c4c7";
2905 const applyRest = () => {
2906 btn.style.background = "#fff";
2907 btn.style.color = restColor;
2908 btn.style.borderColor = restBorder;
2909 };
2910 const applyHover = () => {
2911 if (isDanger) {
2912 btn.style.background = "#d63638";
2913 btn.style.color = "#fff";
2914 btn.style.borderColor = "#d63638";
2915 } else {
2916 btn.style.background = "#f0f0f1";
2917 btn.style.color = "#1d2327";
2918 btn.style.borderColor = "#8c8f94";
2919 }
2920 };
2921 btn.style.cssText = [
2922 "display: inline-flex",
2923 "align-items: center",
2924 "justify-content: center",
2925 "flex: 0 0 30px",
2926 "width: 30px",
2927 "height: 30px",
2928 "padding: 0",
2929 "margin: 0",
2930 "border: 1px solid " + restBorder,
2931 "border-radius: 6px",
2932 "background: #fff",
2933 "color: " + restColor,
2934 "cursor: pointer",
2935 "box-sizing: border-box",
2936 "line-height: 1",
2937 "font: inherit",
2938 "transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease"
2939 ].join(";");
2940 btn.addEventListener("mouseenter", applyHover);
2941 btn.addEventListener("mouseleave", applyRest);
2942 btn.addEventListener("focus", applyHover);
2943 btn.addEventListener("blur", applyRest);
2944 const svgNs = "http://www.w3.org/2000/svg";
2945 const svg = document.createElementNS(svgNs, "svg");
2946 svg.setAttribute("width", "18");
2947 svg.setAttribute("height", "18");
2948 svg.setAttribute("viewBox", "0 0 24 24");
2949 svg.setAttribute("aria-hidden", "true");
2950 svg.setAttribute("focusable", "false");
2951 svg.style.display = "block";
2952 svg.innerHTML = ICON_SVG[opts.icon] ?? "";
2953 btn.appendChild(svg);
2954 btn.addEventListener("click", (e) => {
2955 e.stopPropagation();
2956 opts.onClick();
2957 });
2958 return btn;
2959 }
2960 function renderRecycleBin(body) {
2961 const root = body.querySelector(ROOT);
2962 const table = body.querySelector(TABLE);
2963 if (!root || !table) {
2964 return;
2965 }
2966 const state2 = {
2967 filter: "",
2968 search: "",
2969 searchDebounce: null
2970 };
2971 currentRowActionRestore = (ref) => void handleRestore([ref]);
2972 currentRowActionPurge = (ref) => void handlePurge([ref]);
2973 table.columns = buildColumns();
2974 table.getRowId = (row) => `${row.type}:${row.id}`;
2975 let currentFingerprint = "";
2976 if (cachedItems) {
2977 table.data = cachedItems;
2978 currentFingerprint = itemsFingerprint(cachedItems);
2979 table.removeAttribute("loading");
2980 }
2981 let refreshSeq = 0;
2982 const refresh = async () => {
2983 const showSkeleton = !cachedItems;
2984 const mySeq = ++refreshSeq;
2985 if (showSkeleton) {
2986 table.toggleAttribute("loading", true);
2987 }
2988 try {
2989 const { items, total } = await fetchList({
2990 type: state2.filter,
2991 search: state2.search,
2992 perPage: 200
2993 });
2994 if (mySeq !== refreshSeq) {
2995 return;
2996 }
2997 const next = itemsFingerprint(items);
2998 if (next !== currentFingerprint) {
2999 table.data = items;
3000 currentFingerprint = next;
3001 cachedItems = items;
3002 const visible = new Set(
3003 (table.visibleRows ?? []).map(
3004 (row) => `${row.type}:${row.id}`
3005 )
3006 );
3007 const kept = Array.from(table.selection ?? [], String).filter((key) => visible.has(key));
3008 if (kept.length !== (table.selection?.size ?? 0)) {
3009 table.selection = kept;
3010 }
3011 } else {
3012 cachedItems = items;
3013 }
3014 setRecycleBinBadge(total);
3015 } catch (err) {
3016 if (mySeq !== refreshSeq) {
3017 return;
3018 }
3019 console.error("[recycle-bin] list failed", err);
3020 if (showSkeleton) {
3021 table.data = [];
3022 currentFingerprint = "";
3023 }
3024 } finally {
3025 if (mySeq === refreshSeq) {
3026 if (showSkeleton) {
3027 table.toggleAttribute("loading", false);
3028 }
3029 refreshBulkBar();
3030 }
3031 }
3032 };
3033 const bulk = root.querySelector(BULK);
3034 const countEl = root.querySelector(COUNT);
3035 const refreshBulkBar = () => {
3036 if (!bulk || !countEl) {
3037 return;
3038 }
3039 const selected = Array.from(table.selection ?? []);
3040 if (selected.length === 0) {
3041 bulk.hidden = true;
3042 return;
3043 }
3044 bulk.hidden = false;
3045 countEl.textContent = sprintf(
3046 /* translators: %d: selected row count. */
3047 __("%d selected"),
3048 selected.length
3049 );
3050 };
3051 const collectSelectedItems = () => {
3052 const sel = new Set(Array.from(table.selection ?? [], String));
3053 const out = [];
3054 for (const row of table.visibleRows ?? []) {
3055 if (sel.has(`${row.type}:${row.id}`)) {
3056 out.push({ id: row.id, type: row.type });
3057 }
3058 }
3059 return out;
3060 };
3061 const handleRestore = async (refs) => {
3062 if (refs.length === 0) {
3063 return;
3064 }
3065 const types = Array.from(new Set(refs.map((r) => r.type)));
3066 try {
3067 const result = await restoreItems(refs);
3068 emitDoneEvent("restore", result.ok, result.errors, types, result.ok);
3069 } catch (err) {
3070 console.error("[recycle-bin] restore failed", err);
3071 }
3072 table.clearSelection();
3073 await refresh();
3074 };
3075 const handlePinToDesktop = async (refs) => {
3076 if (refs.length === 0) {
3077 return;
3078 }
3079 const types = Array.from(new Set(refs.map((r) => r.type)));
3080 const okIds = [];
3081 const allErrors = [];
3082 const filesApi = window.wp?.desktop?.files?.rest;
3083 let placed = 0;
3084 for (const ref of refs) {
3085 let restored;
3086 try {
3087 restored = await restoreItems([ref]);
3088 } catch (err) {
3089 console.error("[recycle-bin] pin-to-desktop restore failed", err);
3090 continue;
3091 }
3092 allErrors.push(...restored.errors);
3093 if (!restored.ok.includes(ref.id)) {
3094 continue;
3095 }
3096 okIds.push(ref.id);
3097 const desktopType = mapRecycleTypeToFileType(ref.type);
3098 if (!filesApi || !desktopType) {
3099 continue;
3100 }
3101 try {
3102 await filesApi.createPlacement({
3103 type: desktopType,
3104 ref: String(ref.id),
3105 x: 16 + placed % 5 * 96,
3106 y: 16 + Math.floor(placed / 5) * 110
3107 });
3108 } catch (err) {
3109 console.error("[recycle-bin] pin-to-desktop placement failed", err);
3110 }
3111 placed += 1;
3112 }
3113 emitDoneEvent("restore", okIds, allErrors, types, okIds);
3114 table.clearSelection();
3115 await refresh();
3116 };
3117 const handlePurge = async (refs) => {
3118 if (refs.length === 0) {
3119 return;
3120 }
3121 const ok = await wpdConfirmGlobal({
3122 title: __("Delete forever?"),
3123 message: sprintf(
3124 /* translators: %d: row count. */
3125 __("Permanently delete %d item(s)? This cannot be undone."),
3126 refs.length
3127 ),
3128 confirmLabel: __("Delete forever"),
3129 danger: true
3130 });
3131 if (!ok) {
3132 return;
3133 }
3134 const types = Array.from(new Set(refs.map((r) => r.type)));
3135 try {
3136 const result = await purgeItems(refs);
3137 emitDoneEvent("purge", result.ok, result.errors, types, result.ok);
3138 } catch (err) {
3139 console.error("[recycle-bin] purge failed", err);
3140 }
3141 table.clearSelection();
3142 await refresh();
3143 };
3144 const emptyButton = root.querySelector(EMPTY_BTN);
3145 let emptyButtonLabelEl = null;
3146 let emptyButtonOriginalLabel = "";
3147 if (emptyButton) {
3148 const trailingText = Array.from(emptyButton.childNodes).find(
3149 (n) => n.nodeType === Node.TEXT_NODE && (n.textContent ?? "").trim() !== ""
3150 );
3151 emptyButtonOriginalLabel = (trailingText?.textContent ?? "").trim();
3152 emptyButtonLabelEl = document.createElement("span");
3153 emptyButtonLabelEl.setAttribute(
3154 "data-desktop-mode-recycle-bin-empty-label",
3155 ""
3156 );
3157 emptyButtonLabelEl.textContent = emptyButtonOriginalLabel;
3158 if (trailingText) {
3159 trailingText.replaceWith(emptyButtonLabelEl);
3160 } else {
3161 emptyButton.appendChild(emptyButtonLabelEl);
3162 }
3163 }
3164 const setEmptyButtonState = (mode, purged = 0, total = 0) => {
3165 if (!emptyButton || !emptyButtonLabelEl) {
3166 return;
3167 }
3168 if (mode === "idle") {
3169 emptyButton.removeAttribute("disabled");
3170 emptyButton.removeAttribute("aria-busy");
3171 emptyButtonLabelEl.textContent = emptyButtonOriginalLabel;
3172 return;
3173 }
3174 emptyButton.setAttribute("disabled", "");
3175 emptyButton.setAttribute("aria-busy", "true");
3176 emptyButtonLabelEl.textContent = mode === "starting" || total === 0 ? __("Emptying…") : sprintf(
3177 /* translators: 1: items purged so far, 2: items in bin when emptying began. */
3178 __("Emptying… %1$d of %2$d"),
3179 purged,
3180 total
3181 );
3182 };
3183 const handleEmpty = async () => {
3184 const ok = await wpdConfirmGlobal({
3185 title: __("Empty bin?"),
3186 message: __(
3187 "Permanently delete ALL items in the recycle bin? This includes every type and any items hidden by the current filter or search. This cannot be undone."
3188 ),
3189 confirmLabel: __("Empty bin"),
3190 danger: true
3191 });
3192 if (!ok) {
3193 return;
3194 }
3195 const allTypes = Array.from(
3196 new Set((table.data ?? []).map((r) => r.type))
3197 );
3198 setEmptyButtonState("starting");
3199 try {
3200 const loop = await runEmptyLoop({
3201 emptyBin,
3202 onProgress: ({ purged, initialTotal }) => setEmptyButtonState("progress", purged, initialTotal)
3203 });
3204 emitDoneEvent(
3205 "empty",
3206 new Array(loop.purged).fill(0),
3207 loop.skipped > 0 ? [{
3208 id: 0,
3209 code: "desktop_mode_recycle_bin_skipped",
3210 message: sprintf(
3211 /* translators: %d: skipped count. */
3212 __("%d item(s) skipped (insufficient permissions)."),
3213 loop.skipped
3214 )
3215 }] : [],
3216 allTypes,
3217 []
3218 );
3219 if (loop.stoppedBecause === "empty") {
3220 setRecycleBinBadge(0);
3221 }
3222 } catch (err) {
3223 console.error("[recycle-bin] empty failed", err);
3224 } finally {
3225 setEmptyButtonState("idle");
3226 }
3227 await refresh();
3228 };
3229 root.querySelector(FILTER)?.addEventListener("wpd-pick", (e) => {
3230 const detail = e.detail;
3231 state2.filter = detail?.value ?? "";
3232 table.clearSelection();
3233 void refresh();
3234 });
3235 const search = root.querySelector(SEARCH);
3236 search?.addEventListener("wpd-input-change", (e) => {
3237 const value = e.detail?.value ?? "";
3238 state2.search = value;
3239 if (state2.searchDebounce !== null) {
3240 window.clearTimeout(state2.searchDebounce);
3241 }
3242 state2.searchDebounce = window.setTimeout(() => {
3243 table.clearSelection();
3244 void refresh();
3245 }, 250);
3246 });
3247 body.addEventListener("click", (e) => {
3248 const target = e.target;
3249 if (!target) {
3250 return;
3251 }
3252 if (target.closest(REFRESH)) {
3253 void refresh();
3254 return;
3255 }
3256 if (target.closest(RESTORE_SEL)) {
3257 void handleRestore(collectSelectedItems());
3258 return;
3259 }
3260 if (target.closest(PIN_TO_DESKTOP)) {
3261 void handlePinToDesktop(collectSelectedItems());
3262 return;
3263 }
3264 if (target.closest(PURGE_SEL)) {
3265 void handlePurge(collectSelectedItems());
3266 return;
3267 }
3268 if (target.closest(EMPTY_BTN)) {
3269 void handleEmpty();
3270 }
3271 });
3272 table.addEventListener("wpd-table-selection-change", () => {
3273 refreshBulkBar();
3274 });
3275 table.addEventListener("wpd-table-filter-change", () => {
3276 table.clearSelection();
3277 });
3278 table.sort = { key: "deleted_at", direction: "desc" };
3279 start();
3280 let externalRefreshTimer = null;
3281 const onExternalChange = (e) => {
3282 const detail = e.detail;
3283 if (!detail?.source || detail.source === "local") {
3284 return;
3285 }
3286 if (externalRefreshTimer !== null) {
3287 window.clearTimeout(externalRefreshTimer);
3288 }
3289 externalRefreshTimer = window.setTimeout(() => {
3290 externalRefreshTimer = null;
3291 void refresh();
3292 }, 200);
3293 };
3294 document.addEventListener("desktop-mode-recycle-bin-changed", onExternalChange);
3295 const broadcastUnsubs = [];
3296 const api = window.wp?.desktop;
3297 if (api && typeof api.subscribe === "function") {
3298 const onDomainChanged = (payload) => {
3299 const detail = payload;
3300 if (detail?.source === "recycle-bin") {
3301 return;
3302 }
3303 if (externalRefreshTimer !== null) {
3304 window.clearTimeout(externalRefreshTimer);
3305 }
3306 externalRefreshTimer = window.setTimeout(() => {
3307 externalRefreshTimer = null;
3308 void refresh();
3309 }, 200);
3310 };
3311 broadcastUnsubs.push(
3312 api.subscribe("desktop-mode.post.changed", onDomainChanged),
3313 api.subscribe("desktop-mode.page.changed", onDomainChanged),
3314 api.subscribe("desktop-mode.attachment.changed", onDomainChanged),
3315 api.subscribe("desktop-mode.comment.changed", onDomainChanged),
3316 api.subscribe("desktop-mode.placement.changed", onDomainChanged),
3317 api.subscribe("desktop-mode.shortcut.changed", onDomainChanged),
3318 api.subscribe("desktop-mode.folder.changed", onDomainChanged)
3319 );
3320 }
3321 const onWindowClosed = (e) => {
3322 const detail = e.detail;
3323 if (detail?.windowId !== "desktop-mode-recycle-bin") {
3324 return;
3325 }
3326 stop();
3327 document.removeEventListener(
3328 "desktop-mode-recycle-bin-changed",
3329 onExternalChange
3330 );
3331 for (const unsub of broadcastUnsubs) {
3332 try {
3333 unsub();
3334 } catch (err) {
3335 }
3336 }
3337 broadcastUnsubs.length = 0;
3338 if (externalRefreshTimer !== null) {
3339 window.clearTimeout(externalRefreshTimer);
3340 externalRefreshTimer = null;
3341 }
3342 currentRowActionRestore = () => {
3343 };
3344 currentRowActionPurge = () => {
3345 };
3346 document.removeEventListener("desktop-mode-window-closed", onWindowClosed);
3347 };
3348 document.addEventListener("desktop-mode-window-closed", onWindowClosed);
3349 void refresh();
3350 }
3351 function emitDoneEvent(kind, ok, errors, affectedTypes = [], affectedIds = []) {
3352 const detail = { kind, ok: ok.length, errors, source: "local" };
3353 document.dispatchEvent(
3354 new CustomEvent("desktop-mode-recycle-bin-changed", { detail })
3355 );
3356 const hooks = window.wp?.hooks;
3357 if (hooks && typeof hooks.doAction === "function") {
3358 hooks.doAction("desktop_mode.recycleBin.changed", detail);
3359 }
3360 const api = window.wp?.desktop;
3361 if (api && typeof api.broadcast === "function" && affectedTypes.length > 0) {
3362 const action = kind === "restore" ? "untrashed" : "deleted";
3363 for (const type of affectedTypes) {
3364 api.broadcast(`desktop-mode.${type}.changed`, {
3365 source: "recycle-bin",
3366 action,
3367 ids: affectedIds
3368 });
3369 }
3370 }
3371 }
3372 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
3373 registry["desktop-mode-recycle-bin"] = (body) => {
3374 renderRecycleBin(body);
3375 };
3376 exports.mapRecycleTypeToFileType = mapRecycleTypeToFileType;
3377 exports.renderRecycleBin = renderRecycleBin;
3378 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3379 return exports;
3380 }({});
3381