@iyulab/data-components 0.16.0 → 0.18.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/CHANGELOG.md +29 -0
- package/README.md +16 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.144.0 → _@oxc-project_runtime@0.146.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.144.0 → _@oxc-project_runtime@0.146.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/components/data-view/UDataView.js +2 -2
- package/dist/components/simple-sheet/USimpleSheet.js +2 -2
- package/dist/components/simple-sheet/USimpleSheet.styles.js +15 -1
- package/dist/components/u-record-picker/URecordPicker.d.ts +80 -0
- package/dist/components/u-record-picker/URecordPicker.js +307 -0
- package/dist/components/u-record-picker/styles.d.ts +1 -0
- package/dist/components/u-record-picker/styles.js +83 -0
- package/dist/components/u-record-picker/types.d.ts +10 -0
- package/dist/components/u-rich-table/URichTable.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -1
- package/dist/utilities/messages.d.ts +1 -1
- package/dist/utilities/messages.js +12 -2
- package/package.json +5 -2
- package/skills/iyulab-data-components/SKILL.md +4 -3
- package/skills/iyulab-data-components/references/components/record-picker.md +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`u-record-picker`** — a form control for picking one record from a remote-searched list.
|
|
8
|
+
Typing filters an inline dropdown; Enter (with nothing highlighted) or a trailing find button
|
|
9
|
+
opens a modal lookup dialog (search bar + `u-rich-table`). A single click on a dialog row only
|
|
10
|
+
previews it — Confirm or a row double-click commits the pick, so a stray click in a long
|
|
11
|
+
result list can't accidentally close the dialog.
|
|
12
|
+
|
|
13
|
+
## [0.17.0] - 2026-08-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`u-simple-sheet` gained a `--dc-sheet-height` custom property**, defaulting to `400px` — the
|
|
18
|
+
value the host was previously hardcoded to. Every other size axis on this component (row height,
|
|
19
|
+
cell padding, font size) was already a consumer-adjustable custom property; the host's own height
|
|
20
|
+
was the one dimension left as a literal, so a consumer embedding the sheet in a constrained layout
|
|
21
|
+
(a form, a panel with a fixed footer) had no supported way to make it shorter or taller — only an
|
|
22
|
+
external `height` override that fought the component's own `:host` rule on specificity. Unset,
|
|
23
|
+
rendering is byte-identical to before. Like the other five tokens on this component, it does not
|
|
24
|
+
derive from a role-layer token and is set with an element selector (`u-simple-sheet { … }`) rather
|
|
25
|
+
than `:root`.
|
|
26
|
+
|
|
27
|
+
This token sets an upper bound, not a natural height: `rows` governs the grid's minimum row
|
|
28
|
+
*capacity*, not how many rows are visible, so lowering `--dc-sheet-height` does not shrink the
|
|
29
|
+
number of rows rendered — it constrains the box and leaves the existing `overflow-y: auto` to
|
|
30
|
+
reach the rest.
|
|
31
|
+
|
|
3
32
|
## [0.16.0] - 2026-08-16
|
|
4
33
|
|
|
5
34
|
### Added
|
package/README.md
CHANGED
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
- **URichTable** — 정렬/필터/편집/페이지네이션 지원 테이블 (Lit)
|
|
10
10
|
|
|
11
11
|
> **UDataGrid (DevExtreme)는 v0.4.0에서 제거되었습니다.**
|
|
12
|
-
> OData 서버 사이드 그리드가 필요하면 `@iyulab/flex-table`을 사용하세요.
|
|
13
12
|
> → [마이그레이션 가이드](./docs/migrating-from-datagrid.md)
|
|
14
13
|
|
|
14
|
+
**`URichTable`과 `@iyulab/flex-table` 중 무엇을 쓸지**는 OData 연동 여부가 아니라 데이터
|
|
15
|
+
규모와 상호작용 축으로 가릅니다 — 둘 다 OData 등 외부 데이터 소스를 소비할 수 있습니다.
|
|
16
|
+
|
|
17
|
+
- **`URichTable`** — 중소 규모 데이터, 행 단위 CRUD, selection/필터 UX 중심의 목록·상세 화면
|
|
18
|
+
- **`@iyulab/flex-table`** — 대량 데이터, 셀 단위 편집, 엑셀급 상호작용이 필요한 화면
|
|
19
|
+
|
|
15
20
|
## Installation
|
|
16
21
|
|
|
17
22
|
```bash
|
|
@@ -221,6 +226,12 @@ u-rich-table { --dc-icon-color: #B0B0B0; }
|
|
|
221
226
|
| `--dc-font-size` | `13px`(`var(--u-density, 13px)`) | 본문 셀 · 편집 입력 · 드롭다운 항목 |
|
|
222
227
|
| `--dc-header-font-size` | `12px` | 열 머리 — `--u-density` 와 무관(본문과 독립) |
|
|
223
228
|
| `--dc-header-font-weight` | `600` | 〃 |
|
|
229
|
+
| `--dc-sheet-height` | `400px` | `:host` 전체 높이 (0.17.0~) |
|
|
230
|
+
|
|
231
|
+
⚠**`--dc-sheet-height` 는 "자연 높이로 준다"가 아니라 "상한을 정한다"입니다.** `rows` 는
|
|
232
|
+
표시 개수가 아니라 데이터 격자의 **최소 용량**이라, 이 값을 낮춰도 `rows` 만큼의 빈 행이
|
|
233
|
+
줄지는 않습니다 — 그 안에서 내장 `overflow-y: auto` 로 스크롤해 접근하십시오. 콘텐츠
|
|
234
|
+
분량에 맞춰 자동으로 줄어드는 옵트인은 별도 스코프입니다.
|
|
224
235
|
|
|
225
236
|
⚠**`--dc-font-size` 만 예외입니다 — `@iyulab/components`/`@iyulab/flex-table` 의 밀도
|
|
226
237
|
스위치(`--u-density`)를 폴백 원본으로 갖습니다**(0.15.0~). 조상에 `--u-density` 를 걸면
|
|
@@ -245,11 +256,11 @@ u-simple-sheet {
|
|
|
245
256
|
`30.5px` 를 선언하십시오. (같은 이유로 **빈 행은 데이터 행보다 1.5px 낮습니다** —
|
|
246
257
|
`0.11.0` 이전부터 그랬고, 교정하면 기본 렌더가 바뀌므로 이 릴리스에서는 두었습니다.)
|
|
247
258
|
|
|
248
|
-
|
|
259
|
+
⚠**조절점만 엽니다** — 선언하지 않으면 종전과 같은 값으로 그려집니다.
|
|
249
260
|
`--dc-row-height`·`--dc-cell-padding-block`·`--dc-cell-padding-inline`·
|
|
250
|
-
`--dc-header-font-size`·`--dc-header-font-weight
|
|
251
|
-
(`u-data-view`·`u-rich-table` 은 읽지 않습니다).
|
|
252
|
-
아래 `u-rich-table` 절 참조.
|
|
261
|
+
`--dc-header-font-size`·`--dc-header-font-weight`·`--dc-sheet-height` 여섯은
|
|
262
|
+
`u-simple-sheet` 전용입니다(`u-data-view`·`u-rich-table` 은 읽지 않습니다).
|
|
263
|
+
`--dc-font-size` 만 예외입니다 — 바로 아래 `u-rich-table` 절 참조.
|
|
253
264
|
|
|
254
265
|
### 그리드 밀도 조절 (`u-rich-table` · 0.16.0~)
|
|
255
266
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styles } from "./UDataView.styles.js";
|
|
2
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js";
|
|
4
4
|
import { html } from "lit";
|
|
5
5
|
import { customElement, property } from "lit/decorators.js";
|
|
6
6
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js";
|
|
3
3
|
import { messages } from "../../utilities/messages.js";
|
|
4
4
|
import { styles } from "./USimpleSheet.styles.js";
|
|
5
5
|
import { html } from "lit";
|
|
@@ -44,9 +44,23 @@ var baseStyles = css`
|
|
|
44
44
|
--dc-header-font-size: 12px; /* 열 머리 — --u-density 와 무관(독립 계약 유지) */
|
|
45
45
|
--dc-header-font-weight: 600; /* 〃 */
|
|
46
46
|
|
|
47
|
+
/* ── 소비자 조절점 — 호스트 높이 ──
|
|
48
|
+
★위 다섯과 같은 이유로 열렸지만 축이 다르다 — 저것들은 "셀 하나"의 치수이고
|
|
49
|
+
이것은 "시트 전체(:host)"의 치수다. 종전에는 여기만 리터럴이라, 셀 치수는 전부
|
|
50
|
+
조절되는데 그 합인 호스트 크기만 조절 불가능한 비대칭이 있었다.
|
|
51
|
+
⚠**역할 토큰에서 파생하지 않는다** — --dc-font-size 와 달리 대응하는 --u- 층
|
|
52
|
+
스위치가 없다(호스트 크기는 밀도 개념이 아니다). 그래서 var() 폴백이 아니라
|
|
53
|
+
이 리터럴 기본값 자체가 정본이다.
|
|
54
|
+
⚠**rows 를 낮춰도 이 값은 안 줄어든다 — 그리고 그것이 의도다.** rows 는 표시
|
|
55
|
+
개수가 아니라 데이터 격자의 최소 용량이라, 자연 높이로 접으면 빈 행 개수만큼
|
|
56
|
+
상자가 늘어나는 별개 결함이 생긴다. 이 토큰이 여는 것은 "소비자가 상한을
|
|
57
|
+
명시하고 그 안에서 overflow-y:auto 로 스크롤"이지 "내용에 맞춰 줄어든다"가
|
|
58
|
+
아니다 — 후자가 필요하면 별도 옵트인으로 낸다. */
|
|
59
|
+
--dc-sheet-height: 400px;
|
|
60
|
+
|
|
47
61
|
display: block;
|
|
48
62
|
width: 100%;
|
|
49
|
-
height:
|
|
63
|
+
height: var(--dc-sheet-height);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
.sheet-container {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { PropertyValues } from 'lit';
|
|
2
|
+
import { UFormControlElement } from '@iyulab/components/dist/components/UFormControlElement.js';
|
|
3
|
+
import { UPopover } from '@iyulab/components/dist/components/popover/UPopover.js';
|
|
4
|
+
import { UDialog } from '@iyulab/components/dist/components/dialog/UDialog.js';
|
|
5
|
+
import { URichTable } from '../u-rich-table/URichTable.js';
|
|
6
|
+
import { ColumnDef } from '../u-rich-table/types.js';
|
|
7
|
+
import { PickerItem } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Form control that picks one record from a remote-searched list. Typing filters an inline
|
|
10
|
+
* dropdown; Enter (with nothing highlighted) or the trailing find button opens a modal lookup
|
|
11
|
+
* dialog backed by `u-rich-table`.
|
|
12
|
+
*
|
|
13
|
+
* @slot header - Extra content above the dialog's search bar.
|
|
14
|
+
* @slot footer - Replaces the dialog's default Cancel/Confirm buttons.
|
|
15
|
+
*
|
|
16
|
+
* @event change - Fired when the selected record changes (bubbles, composed, non-cancelable —
|
|
17
|
+
* same contract as `USelect`/`UInput`). Read `.value` (id) and `.selectedItem` (full record)
|
|
18
|
+
* from the target.
|
|
19
|
+
*/
|
|
20
|
+
export declare class URecordPicker extends UFormControlElement<string> {
|
|
21
|
+
static styles: import('lit').CSSResultGroup[];
|
|
22
|
+
/** Async lookup — shared by the inline dropdown and the dialog's search bar. */
|
|
23
|
+
search: (query: string) => Promise<PickerItem[]>;
|
|
24
|
+
/** Column definitions for the lookup dialog's `u-rich-table` (same shape as `u-rich-table`). */
|
|
25
|
+
columns: ColumnDef[];
|
|
26
|
+
/** Lookup dialog header title. Defaults to a localized "Select a record". */
|
|
27
|
+
dialogTitle?: string;
|
|
28
|
+
/** Placeholder text for the main input. */
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
/** Show a clear ("x") button when a value is selected. */
|
|
31
|
+
clearable: boolean;
|
|
32
|
+
/** Inline-dropdown search debounce, in ms. */
|
|
33
|
+
debounce: number;
|
|
34
|
+
containerEl?: HTMLElement;
|
|
35
|
+
inputEl?: HTMLInputElement;
|
|
36
|
+
popoverEl?: UPopover;
|
|
37
|
+
dialogEl?: UDialog;
|
|
38
|
+
tableEl?: URichTable;
|
|
39
|
+
/** Typed text — distinct from `.value` (the committed id). */
|
|
40
|
+
private query;
|
|
41
|
+
private items;
|
|
42
|
+
private loading;
|
|
43
|
+
private activeIndex;
|
|
44
|
+
private error;
|
|
45
|
+
private inlineDebounceTimer?;
|
|
46
|
+
private inlineSearchSeq;
|
|
47
|
+
private dialogDebounceTimer?;
|
|
48
|
+
private dialogSearchSeq;
|
|
49
|
+
private dialogQuery;
|
|
50
|
+
private dialogItems;
|
|
51
|
+
private dialogLoading;
|
|
52
|
+
private pendingId;
|
|
53
|
+
private dialogError;
|
|
54
|
+
private _selectedItem;
|
|
55
|
+
/** The full record behind `.value`, or `null` before any selection. */
|
|
56
|
+
get selectedItem(): PickerItem | null;
|
|
57
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
58
|
+
private handleInlineInput;
|
|
59
|
+
private runInlineSearch;
|
|
60
|
+
private commitInline;
|
|
61
|
+
private handleInlineKeydown;
|
|
62
|
+
private openDialog;
|
|
63
|
+
private handleDialogSearchInput;
|
|
64
|
+
private runDialogSearch;
|
|
65
|
+
/** Single click / focused-row Enter on a non-editable cell — preview only, never closes. */
|
|
66
|
+
private handleRowActivate;
|
|
67
|
+
private handleRowDblClick;
|
|
68
|
+
private confirmDialogSelection;
|
|
69
|
+
private commitFromDialog;
|
|
70
|
+
private handleDialogHide;
|
|
71
|
+
private emitChange;
|
|
72
|
+
protected updated(changedProperties: PropertyValues): void;
|
|
73
|
+
protected setValidity(): void;
|
|
74
|
+
reset(): void;
|
|
75
|
+
}
|
|
76
|
+
declare global {
|
|
77
|
+
interface HTMLElementTagNameMap {
|
|
78
|
+
'u-record-picker': URecordPicker;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js";
|
|
3
|
+
import { messages } from "../../utilities/messages.js";
|
|
4
|
+
import "../u-rich-table/URichTable.js";
|
|
5
|
+
import { styles } from "./styles.js";
|
|
6
|
+
import { html } from "lit";
|
|
7
|
+
import { customElement, property, query, state } from "lit/decorators.js";
|
|
8
|
+
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
9
|
+
import "@iyulab/components/dist/components/button/UButton.js";
|
|
10
|
+
import { Locale } from "@iyulab/components/dist/utilities/Locale.js";
|
|
11
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
12
|
+
import { live } from "lit/directives/live.js";
|
|
13
|
+
import "@iyulab/components/dist/components/field/UField.js";
|
|
14
|
+
import "@iyulab/components/dist/components/spinner/USpinner.js";
|
|
15
|
+
import "@iyulab/components/dist/components/popover/UPopover.js";
|
|
16
|
+
import "@iyulab/components/dist/components/option/UOption.js";
|
|
17
|
+
import "@iyulab/components/dist/components/dialog/UDialog.js";
|
|
18
|
+
import { UFormControlElement } from "@iyulab/components/dist/components/UFormControlElement.js";
|
|
19
|
+
//#region src/components/u-record-picker/URecordPicker.ts
|
|
20
|
+
var URecordPicker = class URecordPicker extends UFormControlElement {
|
|
21
|
+
constructor(..._args) {
|
|
22
|
+
super(..._args);
|
|
23
|
+
this.columns = [];
|
|
24
|
+
this.clearable = false;
|
|
25
|
+
this.debounce = 250;
|
|
26
|
+
this.query = "";
|
|
27
|
+
this.items = [];
|
|
28
|
+
this.loading = false;
|
|
29
|
+
this.activeIndex = -1;
|
|
30
|
+
this.error = false;
|
|
31
|
+
this.inlineSearchSeq = 0;
|
|
32
|
+
this.dialogSearchSeq = 0;
|
|
33
|
+
this.dialogQuery = "";
|
|
34
|
+
this.dialogItems = [];
|
|
35
|
+
this.dialogLoading = false;
|
|
36
|
+
this.pendingId = null;
|
|
37
|
+
this.dialogError = false;
|
|
38
|
+
this._selectedItem = null;
|
|
39
|
+
this.handleInlineInput = (e) => {
|
|
40
|
+
this.query = e.target.value;
|
|
41
|
+
this.activeIndex = -1;
|
|
42
|
+
window.clearTimeout(this.inlineDebounceTimer);
|
|
43
|
+
if (!this.query) {
|
|
44
|
+
this.inlineSearchSeq++;
|
|
45
|
+
this.items = [];
|
|
46
|
+
this.popoverEl?.hide();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.inlineDebounceTimer = window.setTimeout(() => {
|
|
50
|
+
this.runInlineSearch(this.query);
|
|
51
|
+
}, this.debounce);
|
|
52
|
+
};
|
|
53
|
+
this.handleInlineKeydown = (e) => {
|
|
54
|
+
if (e.key === "Enter" && this.items.length === 0) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
this.openDialog();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (this.items.length === 0) return;
|
|
60
|
+
switch (e.key) {
|
|
61
|
+
case "ArrowDown":
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
this.activeIndex = (this.activeIndex + 1) % this.items.length;
|
|
64
|
+
break;
|
|
65
|
+
case "ArrowUp":
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.activeIndex = (this.activeIndex - 1 + this.items.length) % this.items.length;
|
|
68
|
+
break;
|
|
69
|
+
case "Enter":
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (this.activeIndex >= 0) this.commitInline(this.items[this.activeIndex]);
|
|
72
|
+
else this.openDialog();
|
|
73
|
+
break;
|
|
74
|
+
case "Escape":
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
this.items = [];
|
|
77
|
+
this.popoverEl?.hide();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
this.openDialog = async () => {
|
|
81
|
+
this.dialogQuery = this.query;
|
|
82
|
+
this.pendingId = null;
|
|
83
|
+
this.popoverEl?.hide();
|
|
84
|
+
await this.dialogEl?.show();
|
|
85
|
+
this.runDialogSearch(this.dialogQuery);
|
|
86
|
+
};
|
|
87
|
+
this.handleDialogSearchInput = (e) => {
|
|
88
|
+
this.dialogQuery = e.target.value;
|
|
89
|
+
window.clearTimeout(this.dialogDebounceTimer);
|
|
90
|
+
this.dialogDebounceTimer = window.setTimeout(() => {
|
|
91
|
+
this.runDialogSearch(this.dialogQuery);
|
|
92
|
+
}, this.debounce);
|
|
93
|
+
};
|
|
94
|
+
this.handleRowActivate = (e) => {
|
|
95
|
+
this.pendingId = e.detail.id;
|
|
96
|
+
};
|
|
97
|
+
this.handleRowDblClick = (e) => {
|
|
98
|
+
const tr = e.composedPath().find((node) => node instanceof HTMLElement && node.tagName === "TR");
|
|
99
|
+
if (!tr) return;
|
|
100
|
+
const index = Array.from(this.tableEl.shadowRoot.querySelectorAll("tbody tr")).indexOf(tr);
|
|
101
|
+
const item = this.dialogItems[index];
|
|
102
|
+
if (item) this.commitFromDialog(item);
|
|
103
|
+
};
|
|
104
|
+
this.confirmDialogSelection = () => {
|
|
105
|
+
const item = this.dialogItems.find((i) => i.id === this.pendingId);
|
|
106
|
+
if (item) this.commitFromDialog(item);
|
|
107
|
+
};
|
|
108
|
+
this.handleDialogHide = () => {
|
|
109
|
+
this.pendingId = null;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
static {
|
|
113
|
+
this.styles = [super.styles, styles];
|
|
114
|
+
}
|
|
115
|
+
/** The full record behind `.value`, or `null` before any selection. */
|
|
116
|
+
get selectedItem() {
|
|
117
|
+
return this._selectedItem;
|
|
118
|
+
}
|
|
119
|
+
render() {
|
|
120
|
+
return html`
|
|
121
|
+
<u-field part="field"
|
|
122
|
+
?required=${this.required}
|
|
123
|
+
?disabled=${this.disabled}
|
|
124
|
+
?invalid=${this.invalid}
|
|
125
|
+
.label=${this.label}
|
|
126
|
+
.description=${this.description}
|
|
127
|
+
.validationMessage=${this.validationMessage}
|
|
128
|
+
>
|
|
129
|
+
<div class="container" part="container">
|
|
130
|
+
<input class="main-input" type="text"
|
|
131
|
+
aria-label=${ifDefined(this.label)}
|
|
132
|
+
?disabled=${this.disabled}
|
|
133
|
+
?readonly=${this.readonly}
|
|
134
|
+
placeholder=${ifDefined(this.placeholder)}
|
|
135
|
+
.value=${live(this.query)}
|
|
136
|
+
@input=${this.handleInlineInput}
|
|
137
|
+
@keydown=${this.handleInlineKeydown}
|
|
138
|
+
/>
|
|
139
|
+
<u-icon class="suffix-item"
|
|
140
|
+
?hidden=${!this.clearable || !this.value || this.disabled || this.readonly}
|
|
141
|
+
lib="internal" name="x"
|
|
142
|
+
></u-icon>
|
|
143
|
+
<u-button class="suffix-item find-btn" variant="ghost"
|
|
144
|
+
?disabled=${this.disabled}
|
|
145
|
+
aria-label=${messages.text("pickerFind")}
|
|
146
|
+
@click=${this.openDialog}
|
|
147
|
+
>
|
|
148
|
+
<u-icon lib="internal" name="search"></u-icon>
|
|
149
|
+
</u-button>
|
|
150
|
+
</div>
|
|
151
|
+
</u-field>
|
|
152
|
+
|
|
153
|
+
<u-popover part="popover" role="listbox" for=".container" trigger="manual"
|
|
154
|
+
strategy="fixed" placement="bottom-start" offset="1"
|
|
155
|
+
>
|
|
156
|
+
${this.loading ? html`<div class="popover-loading"><u-spinner></u-spinner></div>` : this.error ? html`<div class="no-results">${messages.text("pickerSearchError")}</div>` : this.items.length === 0 ? html`<div class="no-results">${messages.text("noMatch")}</div>` : this.items.map((item, i) => html`
|
|
157
|
+
<u-option .value=${item.id} ?selected=${i === this.activeIndex}
|
|
158
|
+
@click=${() => this.commitInline(item)}
|
|
159
|
+
>${item.label}</u-option>
|
|
160
|
+
`)}
|
|
161
|
+
</u-popover>
|
|
162
|
+
|
|
163
|
+
<u-dialog placement="center" @hide=${this.handleDialogHide}>
|
|
164
|
+
<span slot="header">${this.dialogTitle ?? messages.text("pickerDialogTitle")}</span>
|
|
165
|
+
<slot name="header"></slot>
|
|
166
|
+
<div class="dialog-search">
|
|
167
|
+
<input type="text" .value=${live(this.dialogQuery)}
|
|
168
|
+
@input=${this.handleDialogSearchInput}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
${this.dialogError ? html`<div class="dialog-error">${messages.text("pickerSearchError")}</div>` : ""}
|
|
172
|
+
<div class="dialog-table-wrap">
|
|
173
|
+
<u-rich-table
|
|
174
|
+
.columns=${this.columns}
|
|
175
|
+
.data=${this.dialogItems.map((item) => ({
|
|
176
|
+
...item,
|
|
177
|
+
_id: item.id
|
|
178
|
+
}))}
|
|
179
|
+
.loading=${this.dialogLoading}
|
|
180
|
+
@row-activate=${this.handleRowActivate}
|
|
181
|
+
@dblclick=${this.handleRowDblClick}
|
|
182
|
+
></u-rich-table>
|
|
183
|
+
</div>
|
|
184
|
+
<slot name="footer">
|
|
185
|
+
<div class="dialog-footer">
|
|
186
|
+
<u-button variant="ghost" @click=${() => this.dialogEl?.hide()}
|
|
187
|
+
>${messages.text("pickerCancel")}</u-button>
|
|
188
|
+
<u-button color="primary" ?disabled=${!this.pendingId}
|
|
189
|
+
@click=${this.confirmDialogSelection}
|
|
190
|
+
>${messages.text("pickerConfirm")}</u-button>
|
|
191
|
+
</div>
|
|
192
|
+
</slot>
|
|
193
|
+
</u-dialog>
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
async runInlineSearch(query) {
|
|
197
|
+
const seq = ++this.inlineSearchSeq;
|
|
198
|
+
this.loading = true;
|
|
199
|
+
this.error = false;
|
|
200
|
+
this.popoverEl?.show(this.containerEl);
|
|
201
|
+
try {
|
|
202
|
+
const results = await this.search(query);
|
|
203
|
+
if (seq !== this.inlineSearchSeq) return;
|
|
204
|
+
this.items = results;
|
|
205
|
+
await this.updateComplete;
|
|
206
|
+
if (results.length > 0) this.popoverEl?.show(this.containerEl);
|
|
207
|
+
else this.popoverEl?.hide();
|
|
208
|
+
} catch {
|
|
209
|
+
if (seq !== this.inlineSearchSeq) return;
|
|
210
|
+
this.items = [];
|
|
211
|
+
this.error = true;
|
|
212
|
+
await this.updateComplete;
|
|
213
|
+
this.popoverEl?.show(this.containerEl);
|
|
214
|
+
} finally {
|
|
215
|
+
if (seq === this.inlineSearchSeq) this.loading = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
commitInline(item) {
|
|
219
|
+
this._selectedItem = item;
|
|
220
|
+
this.query = item.label;
|
|
221
|
+
const changed = this.value !== item.id;
|
|
222
|
+
this.value = item.id;
|
|
223
|
+
this.items = [];
|
|
224
|
+
this.popoverEl?.hide();
|
|
225
|
+
if (changed) this.emitChange();
|
|
226
|
+
}
|
|
227
|
+
async runDialogSearch(query) {
|
|
228
|
+
const seq = ++this.dialogSearchSeq;
|
|
229
|
+
this.dialogLoading = true;
|
|
230
|
+
this.dialogError = false;
|
|
231
|
+
try {
|
|
232
|
+
const results = await this.search(query);
|
|
233
|
+
if (seq !== this.dialogSearchSeq) return;
|
|
234
|
+
this.dialogItems = results;
|
|
235
|
+
} catch {
|
|
236
|
+
if (seq !== this.dialogSearchSeq) return;
|
|
237
|
+
this.dialogItems = [];
|
|
238
|
+
this.dialogError = true;
|
|
239
|
+
} finally {
|
|
240
|
+
if (seq === this.dialogSearchSeq) this.dialogLoading = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
commitFromDialog(item) {
|
|
244
|
+
this._selectedItem = item;
|
|
245
|
+
this.query = item.label;
|
|
246
|
+
const changed = this.value !== item.id;
|
|
247
|
+
this.value = item.id;
|
|
248
|
+
this.dialogEl?.hide();
|
|
249
|
+
if (changed) this.emitChange();
|
|
250
|
+
}
|
|
251
|
+
emitChange() {
|
|
252
|
+
if (!this.novalidate) this.validate();
|
|
253
|
+
this.dispatchEvent(new Event("change", {
|
|
254
|
+
bubbles: true,
|
|
255
|
+
composed: true
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
updated(changedProperties) {
|
|
259
|
+
super.updated(changedProperties);
|
|
260
|
+
if (changedProperties.has("value")) this.internals?.setFormValue(this.value ?? "");
|
|
261
|
+
}
|
|
262
|
+
setValidity() {
|
|
263
|
+
let flags = {};
|
|
264
|
+
let message = "";
|
|
265
|
+
if (this.required && !this.value) {
|
|
266
|
+
flags = { valueMissing: true };
|
|
267
|
+
message = Locale.getValue("valueMissing");
|
|
268
|
+
}
|
|
269
|
+
this.commit(flags, message, this.containerEl ?? void 0);
|
|
270
|
+
}
|
|
271
|
+
reset() {
|
|
272
|
+
this.value = void 0;
|
|
273
|
+
this.query = "";
|
|
274
|
+
this._selectedItem = null;
|
|
275
|
+
this.invalid = false;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
__decorate([property({ attribute: false }), __decorateMetadata("design:type", Function)], URecordPicker.prototype, "search", void 0);
|
|
279
|
+
__decorate([property({ type: Array }), __decorateMetadata("design:type", Array)], URecordPicker.prototype, "columns", void 0);
|
|
280
|
+
__decorate([property({
|
|
281
|
+
type: String,
|
|
282
|
+
attribute: "dialog-title"
|
|
283
|
+
}), __decorateMetadata("design:type", String)], URecordPicker.prototype, "dialogTitle", void 0);
|
|
284
|
+
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], URecordPicker.prototype, "placeholder", void 0);
|
|
285
|
+
__decorate([property({
|
|
286
|
+
type: Boolean,
|
|
287
|
+
reflect: true
|
|
288
|
+
}), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "clearable", void 0);
|
|
289
|
+
__decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "debounce", void 0);
|
|
290
|
+
__decorate([query(".container", true), __decorateMetadata("design:type", typeof HTMLElement === "undefined" ? Object : HTMLElement)], URecordPicker.prototype, "containerEl", void 0);
|
|
291
|
+
__decorate([query("input.main-input", true), __decorateMetadata("design:type", typeof HTMLInputElement === "undefined" ? Object : HTMLInputElement)], URecordPicker.prototype, "inputEl", void 0);
|
|
292
|
+
__decorate([query("u-popover", true), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "popoverEl", void 0);
|
|
293
|
+
__decorate([query("u-dialog", true), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "dialogEl", void 0);
|
|
294
|
+
__decorate([query("u-rich-table", true), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "tableEl", void 0);
|
|
295
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "query", void 0);
|
|
296
|
+
__decorate([state(), __decorateMetadata("design:type", Array)], URecordPicker.prototype, "items", void 0);
|
|
297
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "loading", void 0);
|
|
298
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "activeIndex", void 0);
|
|
299
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "error", void 0);
|
|
300
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "dialogQuery", void 0);
|
|
301
|
+
__decorate([state(), __decorateMetadata("design:type", Array)], URecordPicker.prototype, "dialogItems", void 0);
|
|
302
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "dialogLoading", void 0);
|
|
303
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "pendingId", void 0);
|
|
304
|
+
__decorate([state(), __decorateMetadata("design:type", Object)], URecordPicker.prototype, "dialogError", void 0);
|
|
305
|
+
URecordPicker = __decorate([customElement("u-record-picker")], URecordPicker);
|
|
306
|
+
//#endregion
|
|
307
|
+
export { URecordPicker };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import('lit').CSSResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { css } from "lit";
|
|
2
|
+
//#region src/components/u-record-picker/styles.ts
|
|
3
|
+
var styles = css`
|
|
4
|
+
:host {
|
|
5
|
+
display: inline-block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.container {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 0.25rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.container input {
|
|
15
|
+
flex: 1;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
border: none;
|
|
18
|
+
outline: none;
|
|
19
|
+
background: transparent;
|
|
20
|
+
font: inherit;
|
|
21
|
+
color: inherit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.suffix-item {
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
flex: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
u-popover {
|
|
30
|
+
display: block;
|
|
31
|
+
max-height: var(--record-picker-popover-max-height, 50vh);
|
|
32
|
+
overflow: auto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.no-results {
|
|
36
|
+
padding: 0.5rem 0.75rem;
|
|
37
|
+
opacity: 0.6;
|
|
38
|
+
font-size: 0.875rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.popover-loading {
|
|
42
|
+
padding: 0.5rem 0.75rem;
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dialog-error {
|
|
48
|
+
padding: 0.5rem 0.75rem;
|
|
49
|
+
margin-bottom: 0.5rem;
|
|
50
|
+
color: var(--u-color-danger, #b91c1c);
|
|
51
|
+
font-size: 0.875rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.dialog-search {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 0.5rem;
|
|
58
|
+
padding-bottom: 0.75rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dialog-search input {
|
|
62
|
+
flex: 1;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
padding: 0.5rem 0.75rem;
|
|
65
|
+
border: 1px solid currentColor;
|
|
66
|
+
border-radius: 0.25rem;
|
|
67
|
+
font: inherit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dialog-table-wrap {
|
|
71
|
+
min-height: 16rem;
|
|
72
|
+
max-height: 60vh;
|
|
73
|
+
overflow: auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dialog-footer {
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: flex-end;
|
|
79
|
+
gap: 0.5rem;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
//#endregion
|
|
83
|
+
export { styles };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One searchable/selectable record. `id` is the form value; `label` is what the inline
|
|
3
|
+
* dropdown displays. Extra fields are addressed by `URecordPickerProps.columns[].key` for the
|
|
4
|
+
* lookup dialog's table — the shape those fields take is entirely up to `search()`'s caller.
|
|
5
|
+
*/
|
|
6
|
+
export interface PickerItem {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js";
|
|
3
3
|
import { messages } from "../../utilities/messages.js";
|
|
4
4
|
import { richTableStyles } from "./styles.js";
|
|
5
5
|
import { parseTSV, toTSV } from "./utils/clipboard.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './components/data-view/UDataView';
|
|
2
2
|
export * from './components/simple-sheet/USimpleSheet';
|
|
3
3
|
export * from './components/u-rich-table/URichTable';
|
|
4
|
+
export * from './components/u-record-picker/URecordPicker';
|
|
4
5
|
/**
|
|
5
6
|
* `URichTable` 의 공개 API 가 요구하는 타입.
|
|
6
7
|
*
|
|
@@ -13,3 +14,4 @@ export * from './components/u-rich-table/URichTable';
|
|
|
13
14
|
* `URichTable` 만 타입을 `types.ts` 로 분리해 배럴이 지나치고 있었다.
|
|
14
15
|
*/
|
|
15
16
|
export type { ColumnDef, CellPosition, SortState, FilterState, RichTableEventMap, } from './components/u-rich-table/types';
|
|
17
|
+
export type { PickerItem } from './components/u-record-picker/types';
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,5 @@ import "./utilities/shadowDomProtection.js";
|
|
|
2
2
|
import { UDataView } from "./components/data-view/UDataView.js";
|
|
3
3
|
import { USimpleSheet } from "./components/simple-sheet/USimpleSheet.js";
|
|
4
4
|
import { URichTable } from "./components/u-rich-table/URichTable.js";
|
|
5
|
-
|
|
5
|
+
import { URecordPicker } from "./components/u-record-picker/URecordPicker.js";
|
|
6
|
+
export { UDataView, URecordPicker, URichTable, USimpleSheet };
|
|
@@ -21,5 +21,5 @@
|
|
|
21
21
|
* 딸려 온다**(`Locale.getValue('valueMissing')`). 같은 문장을 이 표에 복제하면 두 곳이
|
|
22
22
|
* 갈라진다.
|
|
23
23
|
*/
|
|
24
|
-
export type DataMessageKey = 'empty' | 'loading' | 'filterPlaceholder' | 'filterAll' | 'addRow' | 'pageInfo' | 'noMatch' | 'selected' | 'selectedAcrossPages' | 'rowsPerPage';
|
|
24
|
+
export type DataMessageKey = 'empty' | 'loading' | 'filterPlaceholder' | 'filterAll' | 'addRow' | 'pageInfo' | 'noMatch' | 'selected' | 'selectedAcrossPages' | 'rowsPerPage' | 'pickerDialogTitle' | 'pickerFind' | 'pickerCancel' | 'pickerConfirm' | 'pickerSearchError';
|
|
25
25
|
export declare const messages: import('@iyulab/components/dist/utilities/Locale.js').LocaleNamespace<DataMessageKey>;
|
|
@@ -11,7 +11,12 @@ messages.register("en", {
|
|
|
11
11
|
noMatch: "No matching item",
|
|
12
12
|
selected: "{count} selected",
|
|
13
13
|
selectedAcrossPages: "{count} selected ({onPage} on this page)",
|
|
14
|
-
rowsPerPage: "{size} rows"
|
|
14
|
+
rowsPerPage: "{size} rows",
|
|
15
|
+
pickerDialogTitle: "Select a record",
|
|
16
|
+
pickerFind: "Find",
|
|
17
|
+
pickerCancel: "Cancel",
|
|
18
|
+
pickerConfirm: "Confirm",
|
|
19
|
+
pickerSearchError: "Search failed. Try again."
|
|
15
20
|
});
|
|
16
21
|
messages.register("ko", {
|
|
17
22
|
empty: "데이터가 없습니다",
|
|
@@ -23,7 +28,12 @@ messages.register("ko", {
|
|
|
23
28
|
noMatch: "일치하는 항목 없음",
|
|
24
29
|
selected: "{count}건 선택됨",
|
|
25
30
|
selectedAcrossPages: "{count}건 선택됨 (이 페이지 {onPage}건)",
|
|
26
|
-
rowsPerPage: "{size}행"
|
|
31
|
+
rowsPerPage: "{size}행",
|
|
32
|
+
pickerDialogTitle: "레코드 선택",
|
|
33
|
+
pickerFind: "찾기",
|
|
34
|
+
pickerCancel: "취소",
|
|
35
|
+
pickerConfirm: "확인",
|
|
36
|
+
pickerSearchError: "검색에 실패했습니다. 다시 시도해 주세요."
|
|
27
37
|
});
|
|
28
38
|
//#endregion
|
|
29
39
|
export { messages };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/data-components",
|
|
3
3
|
"description": "iyulab data visualization components",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-components",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"./simple-sheet": "./dist/components/simple-sheet/USimpleSheet.js",
|
|
41
41
|
"./data-view": "./dist/components/data-view/UDataView.js",
|
|
42
|
-
"./u-rich-table": "./dist/components/u-rich-table/URichTable.js"
|
|
42
|
+
"./u-rich-table": "./dist/components/u-rich-table/URichTable.js",
|
|
43
|
+
"./u-record-picker": "./dist/components/u-record-picker/URecordPicker.js"
|
|
43
44
|
},
|
|
44
45
|
"sideEffects": [
|
|
45
46
|
"./dist/components/data-view/UDataView.js",
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
"./src/components/simple-sheet/USimpleSheet.ts",
|
|
49
50
|
"./dist/components/u-rich-table/URichTable.js",
|
|
50
51
|
"./src/components/u-rich-table/URichTable.ts",
|
|
52
|
+
"./dist/components/u-record-picker/URecordPicker.js",
|
|
53
|
+
"./src/components/u-record-picker/URecordPicker.ts",
|
|
51
54
|
"./dist/index.js",
|
|
52
55
|
"./src/index.ts",
|
|
53
56
|
"./dist/init.js",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: iyulab-data-components
|
|
3
|
-
description: Data-oriented web components built on Lit — a server-paged rich table with inline editing and cross-page selection, a spreadsheet-like grid,
|
|
3
|
+
description: Data-oriented web components built on Lit — a server-paged rich table with inline editing and cross-page selection, a spreadsheet-like grid, a multi-layout data viewer, and a remote-search record picker. Use when working with @iyulab/data-components package.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: iyulab
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.18.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# @iyulab/data-components
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Four custom elements for displaying and editing tabular data. Built on
|
|
13
13
|
[Lit](https://lit.dev/); usable from any framework or plain HTML.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
@@ -25,6 +25,7 @@ npm install @iyulab/data-components
|
|
|
25
25
|
| `u-rich-table` | The rows come from a **server** — paging, sorting and filtering are the app's job, and you need selection, inline editing or expandable detail rows | [rich-table.md](references/components/rich-table.md) |
|
|
26
26
|
| `u-simple-sheet` | The data is a **free-form grid** of cells the user types into, spreadsheet-style (fill handle, ranges, clipboard) | [simple-sheet.md](references/components/simple-sheet.md) |
|
|
27
27
|
| `u-data-view` | You are **displaying** records and want to switch between grid / list / table layouts without rebuilding markup | [data-view.md](references/components/data-view.md) |
|
|
28
|
+
| `u-record-picker` | The user needs to **pick one record** from a remote-searched list — an inline typeahead dropdown or a modal lookup dialog, sharing one `search` callback | [record-picker.md](references/components/record-picker.md) |
|
|
28
29
|
|
|
29
30
|
`u-rich-table` and `u-simple-sheet` overlap only superficially. The table is
|
|
30
31
|
**record-oriented** (a row is an object with named columns, and the app owns the
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# u-record-picker
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import '@iyulab/data-components/dist/components/u-record-picker/URecordPicker.js';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
**Tag:** `u-record-picker`
|
|
8
|
+
|
|
9
|
+
Form control that picks one record from a remote-searched list. Typing filters an inline
|
|
10
|
+
dropdown; Enter (with nothing highlighted) or the trailing find button opens a modal lookup
|
|
11
|
+
dialog backed by [`u-rich-table`](./rich-table.md). Form-associated.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<u-record-picker label="Owner" placeholder="Search people…" clearable></u-record-picker>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const picker = document.querySelector('u-record-picker')!;
|
|
19
|
+
picker.search = async (query) => {
|
|
20
|
+
const res = await fetch(`/api/people?q=${encodeURIComponent(query)}`);
|
|
21
|
+
const rows = await res.json();
|
|
22
|
+
return rows.map((r) => ({ id: r.id, label: r.name, ...r }));
|
|
23
|
+
};
|
|
24
|
+
picker.columns = [ // used by the lookup dialog's u-rich-table
|
|
25
|
+
{ key: 'name', label: 'Name' },
|
|
26
|
+
{ key: 'email', label: 'Email' },
|
|
27
|
+
];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The inline dropdown and the lookup dialog share the single `search` callback — one async
|
|
31
|
+
lookup, two entry points. Both keyboard (`ArrowDown`/`ArrowUp`/`Enter`/`Escape` inline) and
|
|
32
|
+
mouse work; a single click on a dialog row previews the selection without closing the dialog,
|
|
33
|
+
and only the Confirm button or a row double-click commits it.
|
|
34
|
+
|
|
35
|
+
## Display only vs. actionable
|
|
36
|
+
|
|
37
|
+
The dialog's table only previews and commits a row — it does not expose `u-rich-table`'s own
|
|
38
|
+
edit affordances. If a `columns` entry marks a column `editable`, a double-click on that cell
|
|
39
|
+
enters cell-edit mode **and** commits the dialog selection at the same time (the two gestures
|
|
40
|
+
are not distinguished) — avoid `editable` columns in `u-record-picker`'s `columns` until this
|
|
41
|
+
is resolved.
|
|
42
|
+
|
|
43
|
+
## Properties
|
|
44
|
+
|
|
45
|
+
| Property | Type | Default | Reflect | Description |
|
|
46
|
+
|----------|------|---------|---------|-------------|
|
|
47
|
+
| `search` | `(query: string) => Promise<PickerItem[]>` | — | — | Async lookup, shared by the inline dropdown and the dialog's search bar. Required |
|
|
48
|
+
| `columns` | `ColumnDef[]` | `[]` | — | Column definitions for the lookup dialog's `u-rich-table` (same shape as `u-rich-table`) |
|
|
49
|
+
| `dialogTitle` | `string` | — | — | Lookup dialog header title (attribute: `dialog-title`). Defaults to a localized "Select a record" |
|
|
50
|
+
| `placeholder` | `string` | — | — | Placeholder text for the main input |
|
|
51
|
+
| `clearable` | `boolean` | `false` | ✓ | Show a clear ("x") button when a value is selected |
|
|
52
|
+
| `debounce` | `number` | `250` | — | Inline-dropdown search debounce, in ms |
|
|
53
|
+
| `value` | `string` | — | — | Selected record's id |
|
|
54
|
+
| `disabled` | `boolean` | `false` | ✓ | Disable |
|
|
55
|
+
| `readonly` | `boolean` | `false` | ✓ | Read-only |
|
|
56
|
+
| `required` | `boolean` | `false` | ✓ | Required |
|
|
57
|
+
| `invalid` | `boolean` | `false` | ✓ | Validation failed |
|
|
58
|
+
| `name` | `string` | — | — | Form field name |
|
|
59
|
+
| `label` | `string` | — | — | Field label |
|
|
60
|
+
| `description` | `string` | — | — | Helper text |
|
|
61
|
+
|
|
62
|
+
`selectedItem` (read-only getter) returns the full `PickerItem` behind `.value`, or `null`
|
|
63
|
+
before any selection.
|
|
64
|
+
|
|
65
|
+
## PickerItem
|
|
66
|
+
|
|
67
|
+
| Field | Type | Description |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `id` | `string` | Value committed to `.value` and the form |
|
|
70
|
+
| `label` | `string` | Text shown in the input and dropdown once selected |
|
|
71
|
+
| `...rest` | `any` | Any other fields the app's `search` result carries — read them from `.selectedItem` or a `u-rich-table` column |
|
|
72
|
+
|
|
73
|
+
## Events
|
|
74
|
+
|
|
75
|
+
| Event | Description |
|
|
76
|
+
|-------|-------------|
|
|
77
|
+
| `change` | Fires when the selected record changes (bubbles, composed, non-cancelable — same contract as `u-select`/`u-input`). Read `.value` (id) and `.selectedItem` (full record) from the target |
|
|
78
|
+
|
|
79
|
+
## Slots
|
|
80
|
+
|
|
81
|
+
| Slot | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `header` | Extra content above the dialog's search bar |
|
|
84
|
+
| `footer` | Replaces the dialog's default Cancel/Confirm buttons |
|