@rozie-ui/data-table-lit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +498 -0
- package/dist/index.cjs +5631 -0
- package/dist/index.d.cts +1120 -0
- package/dist/index.d.mts +1120 -0
- package/dist/index.mjs +5615 -0
- package/package.json +96 -0
- package/src/Column.ts +159 -0
- package/src/DataTable.ts +5473 -0
- package/src/DetailPanel.ts +53 -0
- package/src/EditorCheckbox.ts +67 -0
- package/src/EditorDate.ts +94 -0
- package/src/EditorNumber.ts +95 -0
- package/src/EditorSelect.ts +99 -0
- package/src/EditorText.ts +92 -0
- package/src/FilterNumberRange.ts +89 -0
- package/src/FilterSelect.ts +68 -0
- package/src/FilterText.ts +85 -0
- package/src/GroupBar.ts +206 -0
- package/src/Popover.ts +446 -0
- package/src/index.ts +13 -0
- package/src/internal/middleware.ts +54 -0
- package/src/themes/base.css +102 -0
- package/src/themes/bootstrap.css +37 -0
- package/src/themes/material.css +35 -0
- package/src/themes/shadcn.css +41 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { SignalWatcher, signal } from '@lit-labs/preact-signals';
|
|
4
|
+
|
|
5
|
+
@customElement('rozie-filter-text')
|
|
6
|
+
export default class FilterText extends SignalWatcher(LitElement) {
|
|
7
|
+
static styles = css`
|
|
8
|
+
:host{display:contents}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The column id (mirrors the `#filter` slot scope) — used as the filter key and the input `aria-label`.
|
|
13
|
+
*/
|
|
14
|
+
@property({ type: String, reflect: true }) columnId: string = '';
|
|
15
|
+
/**
|
|
16
|
+
* The table-core column object (opaque passthrough from the `#filter` slot scope).
|
|
17
|
+
*/
|
|
18
|
+
@property({ type: Object }) column: unknown = null;
|
|
19
|
+
/**
|
|
20
|
+
* The current column filter value the local draft seeds from (setup-once).
|
|
21
|
+
*/
|
|
22
|
+
@property({ type: Object }) value: unknown = null;
|
|
23
|
+
/**
|
|
24
|
+
* `(columnId, value) => void` — apply the column filter (Enter / blur applies, Escape clears). Null-guarded at call sites.
|
|
25
|
+
*/
|
|
26
|
+
@property({ type: Function }) setFilter: ((...args: unknown[]) => unknown) | null = null;
|
|
27
|
+
private _draft = signal('');
|
|
28
|
+
|
|
29
|
+
private _disconnectCleanups: Array<() => void> = [];
|
|
30
|
+
// Re-parenting guard: set true once the deferred teardown has actually
|
|
31
|
+
// run (a genuine un-mount), so a subsequent reconnect knows to re-arm.
|
|
32
|
+
private _rozieTornDown = false;
|
|
33
|
+
|
|
34
|
+
firstUpdated(): void {
|
|
35
|
+
// Seed the draft once at setup from the incoming value (setup-once, NOT in the
|
|
36
|
+
// template). Normalize null/undefined to '' so the input value binds to a string.
|
|
37
|
+
this._draft.value = this.value != null ? String(this.value) : '';
|
|
38
|
+
|
|
39
|
+
// Untyped handler param neutralizes to `any`, so reading e.target.value typechecks
|
|
40
|
+
// ×6 (the global-filter idiom). Never inline `$data.x = $event.target.value`.
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
disconnectedCallback(): void {
|
|
44
|
+
super.disconnectedCallback();
|
|
45
|
+
queueMicrotask(() => {
|
|
46
|
+
if (this.isConnected || this._rozieTornDown) return;
|
|
47
|
+
this._rozieTornDown = true;
|
|
48
|
+
for (const fn of this._disconnectCleanups) fn();
|
|
49
|
+
this._disconnectCleanups = [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render() {
|
|
54
|
+
return html`
|
|
55
|
+
<input class="rdt-col-filter" part="col-filter" type="text" aria-label=${this.columnId} .value=${this._draft.value} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onInput($event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onKeydown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onBlur(); }} data-rozie-s-18cbb44e />
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onInput = (e: any) => {
|
|
60
|
+
this._draft.value = e && e.target ? e.target.value : '';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
applyFilter = () => {
|
|
64
|
+
this.setFilter && this.setFilter(this.columnId, this._draft.value);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
clearFilter = () => {
|
|
68
|
+
this._draft.value = '';
|
|
69
|
+
this.setFilter && this.setFilter(this.columnId, '');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
onKeydown = (e: any) => {
|
|
73
|
+
if (e && e.key === 'Enter') {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
this.applyFilter();
|
|
76
|
+
} else if (e && e.key === 'Escape') {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
this.clearFilter();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
onBlur = () => {
|
|
83
|
+
this.applyFilter();
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/GroupBar.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { SignalWatcher, signal } from '@lit-labs/preact-signals';
|
|
4
|
+
import { rozieAttr, rozieDisplay } from '@rozie/runtime-lit';
|
|
5
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
6
|
+
|
|
7
|
+
@customElement('rozie-group-bar')
|
|
8
|
+
export default class GroupBar extends SignalWatcher(LitElement) {
|
|
9
|
+
static styles = css`
|
|
10
|
+
:host{display:contents}
|
|
11
|
+
.rdt-group-drop-zone[data-rozie-s-546c469a] {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: var(--rdt-group-bar-gap, 0.375rem);
|
|
16
|
+
min-width: var(--rdt-group-drop-zone-min, 8rem);
|
|
17
|
+
min-height: 1.75rem;
|
|
18
|
+
padding: var(--rdt-group-drop-zone-pad, 0.1875rem 0.5rem);
|
|
19
|
+
border: 1px dashed var(--rdt-group-drop-zone-border, rgba(0, 0, 0, 0.2));
|
|
20
|
+
border-radius: var(--rdt-group-drop-zone-radius, 0.375rem);
|
|
21
|
+
background: var(--rdt-group-drop-zone-bg, transparent);
|
|
22
|
+
transition: border-color 0.12s ease, background 0.12s ease;
|
|
23
|
+
}
|
|
24
|
+
.rdt-group-drop-zone.is-over[data-rozie-s-546c469a] {
|
|
25
|
+
border-color: var(--rdt-group-drop-zone-border-over, rgba(37, 99, 235, 0.7));
|
|
26
|
+
background: var(--rdt-group-drop-zone-bg-over, rgba(37, 99, 235, 0.08));
|
|
27
|
+
}
|
|
28
|
+
.rdt-group-drop-hint[data-rozie-s-546c469a] {
|
|
29
|
+
opacity: 0.55;
|
|
30
|
+
font-size: 0.8125em;
|
|
31
|
+
user-select: none;
|
|
32
|
+
pointer-events: none;
|
|
33
|
+
}
|
|
34
|
+
.rdt-group-bar[data-rozie-s-546c469a] {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--rdt-group-bar-gap, 0.375rem);
|
|
39
|
+
}
|
|
40
|
+
.rdt-group-token-remove[data-rozie-s-546c469a] {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
margin-inline-start: 0.125rem;
|
|
44
|
+
padding: 0;
|
|
45
|
+
border: none;
|
|
46
|
+
background: none;
|
|
47
|
+
color: inherit;
|
|
48
|
+
font: inherit;
|
|
49
|
+
line-height: 1;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
opacity: 0.6;
|
|
52
|
+
}
|
|
53
|
+
.rdt-group-token-remove[data-rozie-s-546c469a]:hover {
|
|
54
|
+
opacity: 1;
|
|
55
|
+
}
|
|
56
|
+
.rdt-group-clear[data-rozie-s-546c469a] {
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
.rdt-group-token-remove[data-rozie-s-546c469a]:focus-visible,
|
|
60
|
+
.rdt-group-clear[data-rozie-s-546c469a]:focus-visible {
|
|
61
|
+
outline: var(--rdt-focus-ring, 2px solid rgba(37, 99, 235, 0.7));
|
|
62
|
+
outline-offset: 1px;
|
|
63
|
+
border-radius: 2px;
|
|
64
|
+
}
|
|
65
|
+
.rdt-group-token.is-drop-target[data-rozie-s-546c469a] {
|
|
66
|
+
box-shadow: inset 3px 0 0 0 var(--rdt-group-drop-marker, rgba(37, 99, 235, 0.9));
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The ordered active grouping key array (read-only source of truth from the `#groupBar` slot scope). This drop-in never keeps its own copy — it always reads this and writes through `applyGrouping` / `clearGrouping`.
|
|
72
|
+
*/
|
|
73
|
+
@property({ type: Array }) grouping: any[] = [];
|
|
74
|
+
/**
|
|
75
|
+
* The columns offered as grouping targets — `[{ id, label }]` — rendered as draggable chips.
|
|
76
|
+
*/
|
|
77
|
+
@property({ type: Array }) groupableColumns: any[] = [];
|
|
78
|
+
/**
|
|
79
|
+
* `(cols: string[]) => void` — the only add/reorder writer for the grouping order. Null-guarded at call sites.
|
|
80
|
+
*/
|
|
81
|
+
@property({ type: Function }) applyGrouping: ((...args: unknown[]) => unknown) | null = null;
|
|
82
|
+
/**
|
|
83
|
+
* `() => void` — the only clear writer; resets grouping to empty. Null-guarded at call sites.
|
|
84
|
+
*/
|
|
85
|
+
@property({ type: Function }) clearGrouping: ((...args: unknown[]) => unknown) | null = null;
|
|
86
|
+
private _draggingId = signal('');
|
|
87
|
+
private _isOver = signal(false);
|
|
88
|
+
private _dragKind = signal('');
|
|
89
|
+
private _dropKey = signal('');
|
|
90
|
+
|
|
91
|
+
private _disconnectCleanups: Array<() => void> = [];
|
|
92
|
+
// Re-parenting guard: set true once the deferred teardown has actually
|
|
93
|
+
// run (a genuine un-mount), so a subsequent reconnect knows to re-arm.
|
|
94
|
+
private _rozieTornDown = false;
|
|
95
|
+
|
|
96
|
+
disconnectedCallback(): void {
|
|
97
|
+
super.disconnectedCallback();
|
|
98
|
+
queueMicrotask(() => {
|
|
99
|
+
if (this.isConnected || this._rozieTornDown) return;
|
|
100
|
+
this._rozieTornDown = true;
|
|
101
|
+
for (const fn of this._disconnectCleanups) fn();
|
|
102
|
+
this._disconnectCleanups = [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
render() {
|
|
107
|
+
return html`
|
|
108
|
+
<div class="rdt-group-bar" data-rozie-s-546c469a>
|
|
109
|
+
|
|
110
|
+
${repeat<any>(this.groupableColumns, (col, _idx) => col.id, (col, _idx) => html`<span class="rdt-group-token" part="group-token" key=${rozieAttr(col.id)} draggable="true" @dragstart=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onChipDragStart($event, col.id); }} @dragend=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onDragEnd(); }} data-rozie-s-546c469a>${rozieDisplay(col.label)}</span>`)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
<span class="${Object.entries({ "rdt-group-drop-zone": true, 'is-over': this._isOver.value }).filter(([, v]) => v).map(([k]) => k).join(' ')}" data-group-drop-zone="" @dragover=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onDragOver($event); }} @dragleave=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onDragLeave($event); }} @drop=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onDrop($event); }} data-rozie-s-546c469a>
|
|
114
|
+
|
|
115
|
+
${!this.grouping.length ? html`<span class="rdt-group-drop-hint" data-rozie-s-546c469a>Drag columns here to group</span>` : nothing}${repeat<any>(this.grouping, (gk, _idx) => gk, (gk, _idx) => html`<span class="${Object.entries({ "rdt-group-token": true, 'is-drop-target': this._dragKind.value === 'token' && this._dropKey.value === gk && this._draggingId.value !== gk }).filter(([, v]) => v).map(([k]) => k).join(' ')}" part="group-token" data-group-token="" key=${rozieAttr(gk)} draggable="true" @dragstart=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onTokenDragStart($event, gk); }} @dragover=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onTokenDragOver($event, gk); }} @dragend=${($event: Event & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onDragEnd(); }} data-rozie-s-546c469a>
|
|
116
|
+
${rozieDisplay(this.labelFor(gk))}
|
|
117
|
+
<button class="rdt-group-token-remove" type="button" aria-label=${rozieAttr('Remove ' + this.labelFor(gk) + ' grouping')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.removeKey(gk); }} data-rozie-s-546c469a>×</button>
|
|
118
|
+
</span>`)}
|
|
119
|
+
</span>
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
${this.grouping.length ? html`<button class="rdt-group-clear" type="button" @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.clearAll(); }} data-rozie-s-546c469a>Clear</button>` : nothing}</div>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
onChipDragStart = (e: any, id: any) => {
|
|
127
|
+
this._draggingId.value = id;
|
|
128
|
+
this._dragKind.value = 'chip';
|
|
129
|
+
if (e && e.dataTransfer) e.dataTransfer.setData('text/plain', id);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
onTokenDragStart = (e: any, gk: any) => {
|
|
133
|
+
this._draggingId.value = gk;
|
|
134
|
+
this._dragKind.value = 'token';
|
|
135
|
+
if (e && e.dataTransfer) e.dataTransfer.setData('text/plain', gk);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
onDragOver = (e: any) => {
|
|
139
|
+
if (e) e.preventDefault();
|
|
140
|
+
this._isOver.value = true;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
onTokenDragOver = (e: any, gk: any) => {
|
|
144
|
+
if (e) e.preventDefault();
|
|
145
|
+
if (this._dragKind.value === 'token') this._dropKey.value = gk;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
onDragLeave = (e: any) => {
|
|
149
|
+
if (e && e.currentTarget && e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) return;
|
|
150
|
+
this._isOver.value = false;
|
|
151
|
+
this._dropKey.value = '';
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
resetDrag = () => {
|
|
155
|
+
this._draggingId.value = '';
|
|
156
|
+
this._dragKind.value = '';
|
|
157
|
+
this._dropKey.value = '';
|
|
158
|
+
this._isOver.value = false;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
onDragEnd = () => {
|
|
162
|
+
this.resetDrag();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
onDrop = (e: any) => {
|
|
166
|
+
if (e) e.preventDefault();
|
|
167
|
+
const kind = this._dragKind.value;
|
|
168
|
+
const anchor = this._dropKey.value;
|
|
169
|
+
const id = e && e.dataTransfer && e.dataTransfer.getData('text/plain') || this._draggingId.value;
|
|
170
|
+
this.resetDrag();
|
|
171
|
+
if (!id) return;
|
|
172
|
+
if (kind === 'token') {
|
|
173
|
+
// REORDER: pull the dragged key out, then splice it back in BEFORE the anchor
|
|
174
|
+
// token (or at the end when dropped on empty zone space). Shift-safe because we
|
|
175
|
+
// resolve the anchor by KEY inside the already-filtered array, not by raw index.
|
|
176
|
+
if (this.grouping.indexOf(id) === -1) return;
|
|
177
|
+
const without = this.grouping.filter((k: any) => k !== id);
|
|
178
|
+
let to = without.length;
|
|
179
|
+
if (anchor && anchor !== id) {
|
|
180
|
+
const j = without.indexOf(anchor);
|
|
181
|
+
if (j !== -1) to = j;
|
|
182
|
+
}
|
|
183
|
+
const next = without.slice(0, to).concat([id]).concat(without.slice(to));
|
|
184
|
+
this.applyGrouping && this.applyGrouping(next);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// APPEND (chip): add the dragged column IF not already grouped — read the order
|
|
188
|
+
// from $props.grouping, write the NEW order through applyGrouping.
|
|
189
|
+
if (this.grouping.indexOf(id) !== -1) return;
|
|
190
|
+
const next = this.grouping.concat([id]);
|
|
191
|
+
this.applyGrouping && this.applyGrouping(next);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
removeKey = (key: any) => {
|
|
195
|
+
this.applyGrouping && this.applyGrouping(this.grouping.filter((k: any) => k !== key));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
clearAll = () => {
|
|
199
|
+
this.clearGrouping && this.clearGrouping();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
labelFor = (key: any) => {
|
|
203
|
+
const col = this.groupableColumns.find((c: any) => c.id === key);
|
|
204
|
+
return col && col.label || key;
|
|
205
|
+
};
|
|
206
|
+
}
|