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

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