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

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