pict-section-picker 1.0.0 → 1.2.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/README.md +49 -0
- package/form.js +5 -0
- package/package.json +12 -2
- package/source/form/Pict-Section-Picker-FormInput.js +307 -0
- package/source/providers/Pict-Provider-Picker.js +156 -11
- package/source/views/PictView-Picker.js +99 -0
- package/types/form/Pict-Section-Picker-FormInput.d.ts +95 -0
- package/types/form/Pict-Section-Picker-FormInput.d.ts.map +1 -0
- package/types/providers/Pict-Provider-Picker.d.ts +50 -1
- package/types/providers/Pict-Provider-Picker.d.ts.map +1 -1
- package/types/views/PictView-Picker.d.ts +20 -0
- package/types/views/PictView-Picker.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,37 @@ Entity-source configuration:
|
|
|
117
117
|
|
|
118
118
|
The lower-level builders are also exposed: `createEntityDataProvider(cfg)` and `createEntityResolveValue(cfg)` return the raw functions if you want to wire them yourself.
|
|
119
119
|
|
|
120
|
+
## Joined display (parent-entity context)
|
|
121
|
+
|
|
122
|
+
Sometimes a searched entity is ambiguous on its own — a `LineItem` only makes sense next to its `Project`, a `Review` next to its `Book`. Set `JoinEntity` and the picker renders a **compound** label by joining each searched row to a parent entity through a foreign key the row carries:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
tmpPicker.createEntityPicker('ReviewPicker',
|
|
126
|
+
{
|
|
127
|
+
Entity: 'Review',
|
|
128
|
+
SearchFields: [ 'Summary' ],
|
|
129
|
+
JoinEntity: 'Book', // the parent entity to join
|
|
130
|
+
JoinField: 'IDBook', // the FK on the Review row -> Book
|
|
131
|
+
JoinEntityDisplayField: 'Title', // the Book field to show
|
|
132
|
+
DestinationAddress: '#ReviewPicker',
|
|
133
|
+
ValueAddress: 'AppData.Form.IDReview',
|
|
134
|
+
});
|
|
135
|
+
// options render as "Neuromancer - Loved it"; the Value is still IDReview.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Meadow can't join in a single read, so this is **fetch-then-merge**: after each search page the picker collects the rows' unique FK ids and issues **one** `FBL~ID{JoinEntity}~INN~<ids>` request, then stitches the joined display onto every row (also exposed as `Record.JoinName` / `Record.JoinRecord` for `MapRecord` / templates). The same join resolves a pre-bound value's label on first render.
|
|
139
|
+
|
|
140
|
+
| Option | Default | Purpose |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `JoinEntity` | — | Parent entity to join for the compound display. Setting it enables the feature. |
|
|
143
|
+
| `JoinField` | `ID<JoinEntity>` | The FK column **on the searched row** pointing at `JoinEntity`. |
|
|
144
|
+
| `JoinEntityValueField` | `ID<JoinEntity>` | The PK column on `JoinEntity` to match (the `INN` column). |
|
|
145
|
+
| `JoinEntityDisplayField` | `'Name'` | The `JoinEntity` field shown in the compound label. |
|
|
146
|
+
| `JoinEntityFirst` | `true` | `true` → `Parent - Row`; `false` → `Row - Parent`. |
|
|
147
|
+
| `JoinSeparator` | `' - '` | Separator between the two parts. |
|
|
148
|
+
|
|
149
|
+
The same options ride through the form-input adapter (`PictForm.JoinEntity`, …) and the pict-section-recordset entity filters (set `JoinEntity` on the clause) — so an entity filter can show parent context for its options with no host code, layered on top of either the 1:1 (direct-FK / `InternalJoin`) or 1:many (junction / `ExternalJoin`) filter relationship.
|
|
150
|
+
|
|
120
151
|
## Categories
|
|
121
152
|
|
|
122
153
|
Give option rows an optional `Group` field and the list renders headered sections (preserving order; rows without a `Group` fall into a leading unlabeled section):
|
|
@@ -162,6 +193,24 @@ OnCreate: (pTerm) =>
|
|
|
162
193
|
| `OnCreate` | — | `(term) => {Value, Text}` to enable creatable entries. |
|
|
163
194
|
| `OnChange` | — | Called after a selection: single → `(value, record)`, multi → `(values, records)`. |
|
|
164
195
|
|
|
196
|
+
## View methods
|
|
197
|
+
|
|
198
|
+
Call these on the picker view instance — `this.pict.views['<hash>']`:
|
|
199
|
+
|
|
200
|
+
| Method | Description |
|
|
201
|
+
|--------|-------------|
|
|
202
|
+
| `render()` | Paint (or repaint) the control into its destination. |
|
|
203
|
+
| `getValue()` | The current selection — a scalar in single mode, an array of values in multi mode. |
|
|
204
|
+
| `setValue(pValue)` | Set the selection programmatically — the supported counterpart to `getValue()`. Single mode takes a scalar; multi mode takes an array (or a csv string). Writes through to the bound address(es), resolves the display label of any unknown value (from the loaded options, else via `ResolveValue` in async mode), and repaints. Does **not** fire `OnChange` — it is a programmatic set (e.g. a host marshaling a form value into the control), not a user pick. Returns the view for chaining. |
|
|
205
|
+
| `getSelectedRecords()` | (multi) The full `{Value, Text}` record list for the current selection. |
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
const tmpPicker = this.pict.views['AuthorPicker'];
|
|
209
|
+
tmpPicker.setValue(141); // single: select author 141 (label resolves via ResolveValue if async)
|
|
210
|
+
tmpPicker.setValue([ 2, 10, 141 ]); // multi: select these values (array or "2,10,141" csv both accepted)
|
|
211
|
+
const tmpSelected = tmpPicker.getValue();
|
|
212
|
+
```
|
|
213
|
+
|
|
165
214
|
## Theming
|
|
166
215
|
|
|
167
216
|
The widget paints from `--theme-color-*` tokens with sensible hex fallbacks, so it inherits the host app's theme. Relevant tokens: `--theme-color-brand-primary`, `--theme-color-text-primary`, `--theme-color-text-muted`, `--theme-color-border-default`, `--theme-color-border-light`, `--theme-color-border-strong`, `--theme-color-background-primary`, `--theme-color-background-panel`, `--theme-color-background-tertiary`.
|
package/form.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Sub-path entry: `require('pict-section-picker/form')`.
|
|
2
|
+
//
|
|
3
|
+
// The pict-section-form input-type adapter — kept off the package's main entry so the picker core
|
|
4
|
+
// stays usable without pict-section-form (which is an OPTIONAL peer dependency, required only here).
|
|
5
|
+
module.exports = require('./source/form/Pict-Section-Picker-FormInput.js');
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-picker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Pict-native themeable searchable select / combobox — single & multi select, server pagination, categorized groups and creatable entries, driven by a host-agnostic async DataProvider (with a built-in Meadow EntityProvider adapter). A jQuery/select2-free replacement.",
|
|
5
5
|
"main": "source/Pict-Section-Picker.js",
|
|
6
6
|
"types": "types/Pict-Section-Picker.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"source",
|
|
9
|
-
"types"
|
|
9
|
+
"types",
|
|
10
|
+
"form.js"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"test": "npx mocha -u tdd -R spec",
|
|
@@ -48,11 +49,20 @@
|
|
|
48
49
|
"pict-provider": "^1.0.13",
|
|
49
50
|
"pict-view": "^1.0.68"
|
|
50
51
|
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"pict-section-form": ">=1.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"pict-section-form": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
51
60
|
"devDependencies": {
|
|
52
61
|
"@types/mocha": "^10.0.10",
|
|
53
62
|
"@types/node": "^16.18.126",
|
|
54
63
|
"browser-env": "^3.3.0",
|
|
55
64
|
"pict": "^1.0.372",
|
|
65
|
+
"pict-section-form": "^1.0.0",
|
|
56
66
|
"quackage": "^1.3.0",
|
|
57
67
|
"typescript": "^5.9.3"
|
|
58
68
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pict-section-form input-type adapter for pict-section-picker.
|
|
3
|
+
*
|
|
4
|
+
* Renders a picker widget into a dynamic-form cell — the host-agnostic replacement for a select2
|
|
5
|
+
* entity input. Registered as a pict-section-form InputType (default name `Picker`); a host opts a
|
|
6
|
+
* field in with `PictForm.InputType: 'Picker'` (+ Entity / SearchFields / Multiple / …). Used by the
|
|
7
|
+
* pict-section-recordset entity filters and by document forms alike.
|
|
8
|
+
*
|
|
9
|
+
* Contextual scoping (project / spec-year / tenant / …) stays host configuration: the descriptor may
|
|
10
|
+
* carry a `PictForm.GetContextScopeFilter()` hook, OR a host subclass overrides
|
|
11
|
+
* `getContextualSearchFilters(pInput)`. Either way the module never learns what the context means.
|
|
12
|
+
*
|
|
13
|
+
* Requires `pict-section-form` (an OPTIONAL peer dependency — only loaded when you require this file)
|
|
14
|
+
* and the `Pict-Section-Picker` provider registered on the pict instance.
|
|
15
|
+
*/
|
|
16
|
+
const libPictInputExtension = require('pict-section-form').PictInputExtensionProvider;
|
|
17
|
+
|
|
18
|
+
/** @type {Record<string, any>} */
|
|
19
|
+
const _DEFAULT_CONFIGURATION =
|
|
20
|
+
{
|
|
21
|
+
ProviderIdentifier: 'Pict-Input-Picker',
|
|
22
|
+
AutoInitialize: true,
|
|
23
|
+
AutoInitializeOrdinal: 0,
|
|
24
|
+
AutoSolveWithApp: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the InputType metatemplate entries (a hidden informary input + a host element the picker
|
|
29
|
+
* renders into) for a given InputType name + provider hash. Injected via injectTemplateSet.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} pInputTypeName - e.g. 'Picker'.
|
|
32
|
+
* @param {string} pProviderHash - the input-extension provider service hash to auto-attach.
|
|
33
|
+
* @return {Array<Record<string, any>>}
|
|
34
|
+
*/
|
|
35
|
+
const buildPickerInputTemplates = (pInputTypeName, pProviderHash) =>
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
// Mirror the host's DEFAULT input metatemplate structure exactly — a label span + the control
|
|
39
|
+
// where the <input>/<select> would be — so the picker inherits the host's filter/form chrome
|
|
40
|
+
// (label style, spacing, and the row's flex-end trash alignment) instead of inventing its own.
|
|
41
|
+
HashPostfix: `-Template-Input-InputType-${pInputTypeName}`,
|
|
42
|
+
DefaultInputExtensions: [ pProviderHash ],
|
|
43
|
+
Template: /*html*/`
|
|
44
|
+
<!-- InputType ${pInputTypeName} {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
45
|
+
<input type="hidden" id="{~D:Record.Macro.RawHTMLID~}" tabindex="-1" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="" />
|
|
46
|
+
<span>{~D:Record.Name~}:</span> <div class="pps-form-host" id="PICKER-FOR-{~D:Record.Macro.RawHTMLID~}"></div>`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
HashPostfix: `-VerticalTemplate-Input-InputType-${pInputTypeName}`,
|
|
50
|
+
DefaultInputExtensions: [ pProviderHash ],
|
|
51
|
+
Template: /*html*/`
|
|
52
|
+
<!-- InputType ${pInputTypeName} {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
53
|
+
<input type="hidden" id="{~D:Record.Macro.RawHTMLID~}" tabindex="-1" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="" />
|
|
54
|
+
<span>{~D:Record.Name~}:</span> <div class="pps-form-host" id="PICKER-FOR-{~D:Record.Macro.RawHTMLID~}"></div>`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
HashPostfix: `-TabularTemplate-Begin-Input-InputType-${pInputTypeName}`,
|
|
58
|
+
DefaultInputExtensions: [ pProviderHash ],
|
|
59
|
+
Template: /*html*/`
|
|
60
|
+
<input type="hidden" id="PICKER-TABULAR-DATA-{~D:Record.Macro.RawHTMLID~}-{~D:Context[2].Key~}" tabindex="-1" {~D:Record.Macro.InformaryTabular~} `,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
HashPostfix: `-TabularTemplate-End-Input-InputType-${pInputTypeName}`,
|
|
64
|
+
DefaultInputExtensions: [ pProviderHash ],
|
|
65
|
+
Template: /*html*/`
|
|
66
|
+
value="" />
|
|
67
|
+
<div class="pps-form-host" id="PICKER-TABULAR-{~D:Record.Macro.RawHTMLID~}-{~D:Context[2].Key~}"></div>`,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
class PictInputTypePicker extends libPictInputExtension
|
|
72
|
+
{
|
|
73
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
74
|
+
{
|
|
75
|
+
super(pFable, Object.assign({}, _DEFAULT_CONFIGURATION, pOptions), pServiceHash);
|
|
76
|
+
/** @type {any} */
|
|
77
|
+
this.pict;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Visible host + picker-view-hash ids. Must match the metatemplate element ids above.
|
|
81
|
+
getPickerHostID(pRawHTMLID) { return `#PICKER-FOR-${pRawHTMLID}`; }
|
|
82
|
+
getPickerHash(pRawHTMLID) { return `Picker-${pRawHTMLID}`; }
|
|
83
|
+
getTabularPickerHostID(pRawHTMLID, pRowIndex) { return `#PICKER-TABULAR-${pRawHTMLID}-${pRowIndex}`; }
|
|
84
|
+
getTabularPickerHash(pRawHTMLID, pRowIndex) { return `Picker-${pRawHTMLID}-${pRowIndex}`; }
|
|
85
|
+
getTabularHiddenID(pRawHTMLID, pRowIndex) { return `#PICKER-TABULAR-DATA-${pRawHTMLID}-${pRowIndex}`; }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Overridable: extra FoxHound scope stanza(s) AND-applied to the entity search. Default reads the
|
|
89
|
+
* descriptor's `GetContextScopeFilter()` hook (set by the host / recordset filter base), else its
|
|
90
|
+
* static `BaseFilter`. Host subclasses override this to read app state (project / spec-year / …).
|
|
91
|
+
*
|
|
92
|
+
* @param {Record<string, any>} pInput @return {string|Array<string>}
|
|
93
|
+
*/
|
|
94
|
+
getContextualSearchFilters(pInput)
|
|
95
|
+
{
|
|
96
|
+
const tmpHook = pInput && pInput.PictForm && pInput.PictForm.GetContextScopeFilter;
|
|
97
|
+
if (typeof tmpHook === 'function')
|
|
98
|
+
{
|
|
99
|
+
try { return tmpHook() || ''; }
|
|
100
|
+
catch (pError) { this.pict.log.warn(`Pict-Input-Picker: GetContextScopeFilter threw.`, pError); return ''; }
|
|
101
|
+
}
|
|
102
|
+
return (pInput && pInput.PictForm && pInput.PictForm.BaseFilter) || '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build the picker config from a form input descriptor. */
|
|
106
|
+
_buildPickerConfig(pInput, pHostSelector, fOnChange)
|
|
107
|
+
{
|
|
108
|
+
const tmpPF = pInput.PictForm || {};
|
|
109
|
+
return {
|
|
110
|
+
DestinationAddress: pHostSelector,
|
|
111
|
+
Mode: tmpPF.Multiple ? 'multi' : 'single',
|
|
112
|
+
Placeholder: tmpPF.Placeholder || (tmpPF.Entity ? `Select ${tmpPF.Entity}…` : 'Select…'),
|
|
113
|
+
Searchable: (tmpPF.Searchable !== false),
|
|
114
|
+
Entity: tmpPF.Entity,
|
|
115
|
+
SearchFields: tmpPF.SearchFields,
|
|
116
|
+
ValueField: tmpPF.ValueField,
|
|
117
|
+
TextField: tmpPF.TextField,
|
|
118
|
+
PageSize: tmpPF.PageSize || 20,
|
|
119
|
+
Options: tmpPF.Options || [],
|
|
120
|
+
// JoinEntity compound display (1:1 / 1:many parent-entity context) — passed straight through
|
|
121
|
+
// to the picker's entity adapter, which fetch-then-merges the join. No-op when JoinEntity unset.
|
|
122
|
+
JoinEntity: tmpPF.JoinEntity,
|
|
123
|
+
JoinField: tmpPF.JoinField,
|
|
124
|
+
JoinEntityValueField: tmpPF.JoinEntityValueField,
|
|
125
|
+
JoinEntityDisplayField: tmpPF.JoinEntityDisplayField,
|
|
126
|
+
JoinEntityFirst: tmpPF.JoinEntityFirst,
|
|
127
|
+
JoinSeparator: tmpPF.JoinSeparator,
|
|
128
|
+
// Per-search contextual scope — the generic hook the host fills.
|
|
129
|
+
BaseFilter: () => this.getContextualSearchFilters(pInput),
|
|
130
|
+
OnChange: fOnChange,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Instantiate (or reuse) the picker view for a config — entity-backed when Entity is set. */
|
|
135
|
+
_instantiatePicker(pPickerHash, pConfig)
|
|
136
|
+
{
|
|
137
|
+
const tmpProvider = this.pict.providers['Pict-Section-Picker'];
|
|
138
|
+
if (!tmpProvider)
|
|
139
|
+
{
|
|
140
|
+
this.pict.log.warn('Pict-Input-Picker: the Pict-Section-Picker provider is not registered.');
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return pConfig.Entity
|
|
144
|
+
? tmpProvider.createEntityPicker(pPickerHash, pConfig)
|
|
145
|
+
: tmpProvider.createPicker(pPickerHash, pConfig);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Write a picker value into the form: csv to the hidden informary input (+ dataChanged), plus the
|
|
150
|
+
* raw array to `PictForm.ValueArrayAddress` when set (the recordset filter reads Values as an
|
|
151
|
+
* array). The csv-vs-array bridge lives HERE (generic) instead of in each host.
|
|
152
|
+
*/
|
|
153
|
+
_commit(pView, pInput, pValue, pHTMLSelector)
|
|
154
|
+
{
|
|
155
|
+
const tmpCSV = Array.isArray(pValue) ? pValue.join(',') : (pValue === undefined || pValue === null ? '' : pValue);
|
|
156
|
+
this.pict.ContentAssignment.assignContent(pHTMLSelector, tmpCSV);
|
|
157
|
+
if (pInput.PictForm && pInput.PictForm.ValueArrayAddress && pView.Bundle)
|
|
158
|
+
{
|
|
159
|
+
const tmpArray = Array.isArray(pValue) ? pValue : (tmpCSV === '' ? [] : String(tmpCSV).split(','));
|
|
160
|
+
this.pict.manifest.setValueAtAddress(pView.Bundle, pInput.PictForm.ValueArrayAddress, tmpArray);
|
|
161
|
+
}
|
|
162
|
+
pView.dataChanged(pInput.Hash);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_commitTabular(pView, pInput, pValue, pHiddenID, pRowIndex)
|
|
166
|
+
{
|
|
167
|
+
const tmpCSV = Array.isArray(pValue) ? pValue.join(',') : (pValue === undefined || pValue === null ? '' : pValue);
|
|
168
|
+
this.pict.ContentAssignment.assignContent(pHiddenID, tmpCSV);
|
|
169
|
+
pView.dataChangedTabular(pInput.PictForm.GroupIndex, pInput.PictForm.InputIndex, pRowIndex);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Idempotently mount (or reuse) the picker into its host element + seed its value. Called from both
|
|
174
|
+
* onInputInitialize and onDataMarshalToForm because, in the async-virtual filter render, the host
|
|
175
|
+
* element only exists in the real DOM by the marshal pass — whichever hook fires post-DOM wins, and
|
|
176
|
+
* re-calls are harmless (the picker view is reused by hash).
|
|
177
|
+
* @return {boolean} true if the picker is mounted.
|
|
178
|
+
*/
|
|
179
|
+
_mountPicker(pView, pInput, pValue, pHostSelector, pPickerHash, fOnChange)
|
|
180
|
+
{
|
|
181
|
+
if (!this.pict.ContentAssignment.getElement(pHostSelector)?.[0]) { return false; }
|
|
182
|
+
const tmpView = this._instantiatePicker(pPickerHash, this._buildPickerConfig(pInput, pHostSelector, fOnChange));
|
|
183
|
+
if (!tmpView) { return false; }
|
|
184
|
+
tmpView.render();
|
|
185
|
+
tmpView.setValue(pValue);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- non-tabular lifecycle ---
|
|
190
|
+
|
|
191
|
+
onInputInitialize(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID)
|
|
192
|
+
{
|
|
193
|
+
const tmpRaw = pInput.Macro.RawHTMLID;
|
|
194
|
+
this._mountPicker(pView, pInput, pValue, this.getPickerHostID(tmpRaw), this.getPickerHash(tmpRaw),
|
|
195
|
+
(pNewValue) => this._commit(pView, pInput, pNewValue, pInput.Macro.HTMLSelector));
|
|
196
|
+
return super.onInputInitialize(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onDataMarshalToForm(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID)
|
|
200
|
+
{
|
|
201
|
+
const tmpRaw = pInput.Macro.RawHTMLID;
|
|
202
|
+
const tmpPickerHash = this.getPickerHash(tmpRaw);
|
|
203
|
+
// Mount if it isn't already (post-DOM hook), else just re-seed the value.
|
|
204
|
+
if (!this._mountPicker(pView, pInput, pValue, this.getPickerHostID(tmpRaw), tmpPickerHash,
|
|
205
|
+
(pNewValue) => this._commit(pView, pInput, pNewValue, pInput.Macro.HTMLSelector)))
|
|
206
|
+
{
|
|
207
|
+
const tmpView = this.pict.views[tmpPickerHash];
|
|
208
|
+
if (tmpView) { tmpView.setValue(pValue); }
|
|
209
|
+
}
|
|
210
|
+
return super.onDataMarshalToForm(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onDataRequest(pView, pInput, pValue, pHTMLSelector)
|
|
214
|
+
{
|
|
215
|
+
const tmpView = this.pict.views[this.getPickerHash(pInput.Macro.RawHTMLID)];
|
|
216
|
+
const tmpVal = tmpView ? tmpView.getValue() : pValue;
|
|
217
|
+
this._commit(pView, pInput, tmpVal, pHTMLSelector);
|
|
218
|
+
return super.onDataRequest(pView, pInput, tmpVal, pHTMLSelector);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- tabular lifecycle (one picker view instance per (input, row)) ---
|
|
222
|
+
|
|
223
|
+
/** Idempotent tabular mount (see _mountPicker). @return {boolean} */
|
|
224
|
+
_mountPickerTabular(pView, pInput, pValue, pRowIndex)
|
|
225
|
+
{
|
|
226
|
+
const tmpRaw = pInput.Macro.RawHTMLID;
|
|
227
|
+
const tmpHostSelector = this.getTabularPickerHostID(tmpRaw, pRowIndex);
|
|
228
|
+
if (!this.pict.ContentAssignment.getElement(tmpHostSelector)?.[0]) { return false; }
|
|
229
|
+
const tmpHiddenID = this.getTabularHiddenID(tmpRaw, pRowIndex);
|
|
230
|
+
const tmpView = this._instantiatePicker(this.getTabularPickerHash(tmpRaw, pRowIndex),
|
|
231
|
+
this._buildPickerConfig(pInput, tmpHostSelector, (pNewValue) => this._commitTabular(pView, pInput, pNewValue, tmpHiddenID, pRowIndex)));
|
|
232
|
+
if (!tmpView) { return false; }
|
|
233
|
+
tmpView.render();
|
|
234
|
+
tmpView.setValue(pValue);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onInputInitializeTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID)
|
|
239
|
+
{
|
|
240
|
+
this._mountPickerTabular(pView, pInput, pValue, pRowIndex);
|
|
241
|
+
return super.onInputInitializeTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onDataMarshalToFormTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID)
|
|
245
|
+
{
|
|
246
|
+
if (!this._mountPickerTabular(pView, pInput, pValue, pRowIndex))
|
|
247
|
+
{
|
|
248
|
+
const tmpView = this.pict.views[this.getTabularPickerHash(pInput.Macro.RawHTMLID, pRowIndex)];
|
|
249
|
+
if (tmpView) { tmpView.setValue(pValue); }
|
|
250
|
+
}
|
|
251
|
+
return super.onDataMarshalToFormTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onDataRequestTabular(pView, pInput, pValue, pHTMLSelector, pRowIndex)
|
|
255
|
+
{
|
|
256
|
+
const tmpView = this.pict.views[this.getTabularPickerHash(pInput.Macro.RawHTMLID, pRowIndex)];
|
|
257
|
+
const tmpVal = tmpView ? tmpView.getValue() : pValue;
|
|
258
|
+
this._commitTabular(pView, pInput, tmpVal, this.getTabularHiddenID(pInput.Macro.RawHTMLID, pRowIndex), pRowIndex);
|
|
259
|
+
return super.onDataRequestTabular(pView, pInput, tmpVal, pHTMLSelector, pRowIndex);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Register the Picker InputType on a pict instance: the input-extension provider + its metatemplate(s).
|
|
265
|
+
* Idempotent. Requires `pict-section-form` loaded (PictFormSectionDefaultTemplateProvider present) and
|
|
266
|
+
* the `Pict-Section-Picker` provider registered.
|
|
267
|
+
*
|
|
268
|
+
* @param {any} pPict - the pict instance.
|
|
269
|
+
* @param {Record<string, any>} [pOptions]
|
|
270
|
+
* - InputTypeName {string} - the InputType string (default 'Picker').
|
|
271
|
+
* - ProviderHash {string} - the input-extension provider service hash (default 'Pict-Input-Picker').
|
|
272
|
+
* - ProviderClass {Function} - provider class to register (default PictInputTypePicker; a host
|
|
273
|
+
* passes a subclass that overrides getContextualSearchFilters for its scoping).
|
|
274
|
+
* - TemplatePrefix {string|Array<string>} - the form template prefix(es) to inject the metatemplate
|
|
275
|
+
* under (default 'Pict-MT-Base'; Headlight uses its theme prefixes).
|
|
276
|
+
* @return {boolean} true if registered.
|
|
277
|
+
*/
|
|
278
|
+
const registerPickerInputType = (pPict, pOptions) =>
|
|
279
|
+
{
|
|
280
|
+
const tmpOptions = pOptions || {};
|
|
281
|
+
const tmpInputTypeName = tmpOptions.InputTypeName || 'Picker';
|
|
282
|
+
const tmpProviderHash = tmpOptions.ProviderHash || 'Pict-Input-Picker';
|
|
283
|
+
const tmpProviderClass = tmpOptions.ProviderClass || PictInputTypePicker;
|
|
284
|
+
const tmpPrefixes = Array.isArray(tmpOptions.TemplatePrefix) ? tmpOptions.TemplatePrefix : [ tmpOptions.TemplatePrefix || 'Pict-MT-Base' ];
|
|
285
|
+
|
|
286
|
+
if (!pPict.providers[tmpProviderHash])
|
|
287
|
+
{
|
|
288
|
+
pPict.addProvider(tmpProviderHash, Object.assign({}, _DEFAULT_CONFIGURATION, { ProviderIdentifier: tmpProviderHash }), tmpProviderClass);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const tmpTemplateProvider = pPict.providers.PictFormSectionDefaultTemplateProvider;
|
|
292
|
+
if (!tmpTemplateProvider || typeof tmpTemplateProvider.injectTemplateSet !== 'function')
|
|
293
|
+
{
|
|
294
|
+
pPict.log.warn('Pict-Input-Picker: PictFormSectionDefaultTemplateProvider not available; cannot register the Picker metatemplate (is pict-section-form loaded?).');
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const tmpTemplates = buildPickerInputTemplates(tmpInputTypeName, tmpProviderHash);
|
|
298
|
+
tmpPrefixes.forEach((pPrefix) => tmpTemplateProvider.injectTemplateSet({ TemplatePrefix: pPrefix, Templates: tmpTemplates }));
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
module.exports = PictInputTypePicker;
|
|
303
|
+
|
|
304
|
+
module.exports.PictInputTypePicker = PictInputTypePicker;
|
|
305
|
+
module.exports.registerPickerInputType = registerPickerInputType;
|
|
306
|
+
module.exports.buildPickerInputTemplates = buildPickerInputTemplates;
|
|
307
|
+
module.exports.default_configuration = _DEFAULT_CONFIGURATION;
|
|
@@ -31,9 +31,15 @@ const _PickerCSS = /*css*/`
|
|
|
31
31
|
.pps-chip-x { flex: 0 0 auto; display: inline-flex; align-items: center; cursor: pointer; font-size: 0.78rem; border-radius: 4px; padding: 0.1rem; opacity: 0.7; }
|
|
32
32
|
.pps-chip-x:hover { opacity: 1; background: color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 22%, transparent); }
|
|
33
33
|
|
|
34
|
-
/* Transparent full-viewport backdrop: closes on outside click (no document listener).
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
/* Transparent full-viewport backdrop: closes on outside click (no document listener). Only present
|
|
35
|
+
while OPEN — otherwise a fixed full-viewport layer would swallow every click on the page. When open,
|
|
36
|
+
the control is raised above it so its chips/× stay clickable; the dropdown sits above both. */
|
|
37
|
+
.pps-backdrop { position: fixed; inset: 0; z-index: 0; display: none; }
|
|
38
|
+
.pps.pps-open .pps-backdrop { display: block; }
|
|
39
|
+
.pps.pps-open .pps-control { position: relative; z-index: 1; }
|
|
40
|
+
/* Fixed (viewport-anchored) + JS-positioned in open(), so no ancestor's overflow:hidden — a card, a
|
|
41
|
+
slide-out drawer, a scroll pane — can ever clip the dropdown, whatever the host's layout. */
|
|
42
|
+
.pps-pop { position: fixed; z-index: 40; min-width: 200px; display: none; }
|
|
37
43
|
.pps.pps-open .pps-pop { display: block; }
|
|
38
44
|
.pps-panel { position: relative; z-index: 1; display: flex; flex-direction: column; max-height: min(60vh, 360px);
|
|
39
45
|
background: var(--theme-color-background-panel, #fff); border: 1px solid var(--theme-color-border-default, #d7dce3);
|
|
@@ -60,6 +66,10 @@ const _PickerCSS = /*css*/`
|
|
|
60
66
|
padding: 0.45rem 0.6rem; border: none; border-radius: 6px; background: transparent; color: var(--theme-color-brand-primary, #156dd1); font-weight: 600; }
|
|
61
67
|
.pps-create:hover { background: var(--theme-color-background-tertiary, #eceef2); }
|
|
62
68
|
.pps-create-ic { flex: 0 0 auto; display: inline-flex; }
|
|
69
|
+
|
|
70
|
+
/* Form-input adapter (pict-section-picker/form): the picker host fills its row like the host's
|
|
71
|
+
native inputs (width:100% forces it to wrap below the label span + fill, matching a scalar input). */
|
|
72
|
+
.pps-form-host { flex: 1 1 100%; min-width: 0; width: 100%; }
|
|
63
73
|
`;
|
|
64
74
|
|
|
65
75
|
/** @type {Record<string, any>} */
|
|
@@ -159,8 +169,22 @@ class PictProviderPicker extends libPictProvider
|
|
|
159
169
|
* - TextField {string} - record field used as the option Text (default `Name`).
|
|
160
170
|
* - PageSize {number} - records per page (default 20).
|
|
161
171
|
* - Sort {string} - optional field to sort ascending (adds `FSF~<field>~ASC~0`).
|
|
162
|
-
* - BaseFilter {string} - optional always-applied FoxHound filter (AND),
|
|
172
|
+
* - BaseFilter {string|Array<string>|function} - optional always-applied FoxHound filter (AND),
|
|
173
|
+
* e.g. `FBV~IDCustomer~EQ~1`. May be a **function** `(searchTerm, page) => string|string[]`
|
|
174
|
+
* evaluated on every search — the generic hook for host-injected CONTEXTUAL scoping (project,
|
|
175
|
+
* tenant, spec-year, …). The module stays agnostic; the host supplies the closure.
|
|
163
176
|
* - MapRecord {function} - optional `(record) => {Value, Text}` mapper (overrides Value/TextField).
|
|
177
|
+
* - JoinEntity {string} - optional second entity to JOIN for a compound display (e.g. a `LineItem`
|
|
178
|
+
* shown with its `Project`). Each searched row must carry the FK (`JoinField`). Because Meadow
|
|
179
|
+
* can't join in one read, this is fetch-then-merge: after the primary page resolves, the unique
|
|
180
|
+
* FK ids drive ONE `FBL~ID{JoinEntity}~INN~<ids>` request, and the joined display field is
|
|
181
|
+
* stitched onto each row (as `Record.JoinName` / `Record.JoinRecord`) + composed into the Text.
|
|
182
|
+
* - JoinField {string} - the FK column ON THE SEARCHED ROW pointing at JoinEntity (default `ID{JoinEntity}`).
|
|
183
|
+
* - JoinEntityValueField {string} - the PK column on JoinEntity to match (default `ID{JoinEntity}`).
|
|
184
|
+
* - JoinEntityDisplayField {string} - the JoinEntity field to display (default `Name`).
|
|
185
|
+
* - JoinEntityFirst {boolean} - put the joined value first in the compound (default `true`):
|
|
186
|
+
* `JoinName - baseText`; when `false`, `baseText - JoinName`.
|
|
187
|
+
* - JoinSeparator {string} - the compound separator (default `' - '`).
|
|
164
188
|
* @return {(pSearchTerm: string, pPage: number) => Promise<{results: Array<any>, hasMore: boolean}>}
|
|
165
189
|
*/
|
|
166
190
|
createEntityDataProvider(pConfig)
|
|
@@ -171,8 +195,9 @@ class PictProviderPicker extends libPictProvider
|
|
|
171
195
|
const tmpTextField = pConfig.TextField || 'Name';
|
|
172
196
|
const tmpPageSize = pConfig.PageSize || 20;
|
|
173
197
|
const tmpSort = pConfig.Sort || false;
|
|
174
|
-
const
|
|
198
|
+
const tmpBaseFilterConfig = pConfig.BaseFilter || '';
|
|
175
199
|
const tmpMapRecord = (typeof pConfig.MapRecord === 'function') ? pConfig.MapRecord : false;
|
|
200
|
+
const tmpJoinConfig = this._resolveJoinConfig(pConfig);
|
|
176
201
|
|
|
177
202
|
return (pSearchTerm, pPage) => new Promise((resolve, reject) =>
|
|
178
203
|
{
|
|
@@ -181,6 +206,17 @@ class PictProviderPicker extends libPictProvider
|
|
|
181
206
|
return reject(new Error('Pict-Section-Picker: pict.EntityProvider is not available for entity-backed pickers.'));
|
|
182
207
|
}
|
|
183
208
|
|
|
209
|
+
// Resolve the base filter at SEARCH time. A function form lets the host inject contextual
|
|
210
|
+
// scoping (e.g. "only this project's line items") without the module knowing the context;
|
|
211
|
+
// it can return a single stanza, an array of stanzas, or nothing.
|
|
212
|
+
let tmpBaseFilter = tmpBaseFilterConfig;
|
|
213
|
+
if (typeof tmpBaseFilterConfig === 'function')
|
|
214
|
+
{
|
|
215
|
+
try { tmpBaseFilter = tmpBaseFilterConfig(pSearchTerm, pPage); }
|
|
216
|
+
catch (pScopeError) { this.pict.log.warn(`Pict-Section-Picker [${tmpEntity}] BaseFilter() threw; ignoring contextual scope.`, pScopeError); tmpBaseFilter = ''; }
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(tmpBaseFilter)) { tmpBaseFilter = tmpBaseFilter.filter(Boolean).join('~'); }
|
|
219
|
+
|
|
184
220
|
const tmpStanzas = [];
|
|
185
221
|
if (tmpBaseFilter) { tmpStanzas.push(tmpBaseFilter); }
|
|
186
222
|
if (pSearchTerm) { tmpStanzas.push(this.buildSearchFilter(tmpSearchFields, pSearchTerm)); }
|
|
@@ -193,11 +229,103 @@ class PictProviderPicker extends libPictProvider
|
|
|
193
229
|
{
|
|
194
230
|
if (pError) { return reject(pError); }
|
|
195
231
|
const tmpList = Array.isArray(pRecords) ? pRecords : [];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
232
|
+
// JoinEntity (when configured): one INN fetch for the joined rows, stitched onto each
|
|
233
|
+
// searched row, before mapping — so the option Text can show the compound display.
|
|
234
|
+
this._decorateRecordsWithJoin(tmpList, tmpJoinConfig).then((pDecorated) =>
|
|
235
|
+
{
|
|
236
|
+
const tmpResults = pDecorated.map((pRecord) =>
|
|
237
|
+
{
|
|
238
|
+
if (tmpMapRecord) { return tmpMapRecord(pRecord); }
|
|
239
|
+
const tmpText = tmpJoinConfig
|
|
240
|
+
? this._composeJoinedText(pRecord[tmpTextField], pRecord.JoinName, tmpJoinConfig.First, tmpJoinConfig.Separator)
|
|
241
|
+
: pRecord[tmpTextField];
|
|
242
|
+
return { Value: pRecord[tmpValueField], Text: tmpText, Record: pRecord };
|
|
243
|
+
});
|
|
244
|
+
// hasMore: a full page came back, so there is (probably) another. Avoids a Count round-trip.
|
|
245
|
+
return resolve({ results: tmpResults, hasMore: (tmpList.length >= tmpPageSize) });
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve the JoinEntity options off an entity-source config into a normalized internal shape, or
|
|
253
|
+
* `false` when no JoinEntity is configured. Centralizes the defaults so the DataProvider and the
|
|
254
|
+
* ResolveValue builders agree.
|
|
255
|
+
*
|
|
256
|
+
* @param {Record<string, any>} pConfig
|
|
257
|
+
* @return {false | {Entity:string, FKColumn:string, PKColumn:string, DisplayField:string, First:boolean, Separator:string}}
|
|
258
|
+
*/
|
|
259
|
+
_resolveJoinConfig(pConfig)
|
|
260
|
+
{
|
|
261
|
+
if (!pConfig || !pConfig.JoinEntity) { return false; }
|
|
262
|
+
return {
|
|
263
|
+
Entity: pConfig.JoinEntity,
|
|
264
|
+
// The FK on the SEARCHED row, and the PK it points at on the join entity (the INN column).
|
|
265
|
+
FKColumn: pConfig.JoinField || `ID${pConfig.JoinEntity}`,
|
|
266
|
+
PKColumn: pConfig.JoinEntityValueField || `ID${pConfig.JoinEntity}`,
|
|
267
|
+
DisplayField: pConfig.JoinEntityDisplayField || 'Name',
|
|
268
|
+
// Default join-first (mirrors the documented select2 EntitySelector default).
|
|
269
|
+
First: (pConfig.JoinEntityFirst !== false),
|
|
270
|
+
Separator: (typeof pConfig.JoinSeparator === 'string') ? pConfig.JoinSeparator : ' - ',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compose a compound display from a base text + a joined value, honoring ordering + separator.
|
|
276
|
+
* Falls back to just the base text when there is no joined value.
|
|
277
|
+
*
|
|
278
|
+
* @param {any} pBaseText @param {any} pJoinText @param {boolean} pFirst @param {string} pSeparator
|
|
279
|
+
* @return {any}
|
|
280
|
+
*/
|
|
281
|
+
_composeJoinedText(pBaseText, pJoinText, pFirst, pSeparator)
|
|
282
|
+
{
|
|
283
|
+
if (pJoinText === undefined || pJoinText === null || pJoinText === '') { return pBaseText; }
|
|
284
|
+
const tmpBase = (pBaseText === undefined || pBaseText === null) ? '' : pBaseText;
|
|
285
|
+
return pFirst ? `${pJoinText}${pSeparator}${tmpBase}` : `${tmpBase}${pSeparator}${pJoinText}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Fetch-then-merge the join entity for a page of searched records. Collects the unique FK ids the
|
|
290
|
+
* rows carry (`JoinConfig.FKColumn`), issues ONE `FBL~{PKColumn}~INN~<ids>` request against the join
|
|
291
|
+
* entity, and stitches `JoinRecord` + `JoinName` onto each searched row. Resolves the (mutated) same
|
|
292
|
+
* array; on any error or when there's nothing to join, resolves the records un-decorated (the Text
|
|
293
|
+
* gracefully degrades to the base field).
|
|
294
|
+
*
|
|
295
|
+
* @param {Array<any>} pRecords @param {false | Record<string, any>} pJoinConfig
|
|
296
|
+
* @return {Promise<Array<any>>}
|
|
297
|
+
*/
|
|
298
|
+
_decorateRecordsWithJoin(pRecords, pJoinConfig)
|
|
299
|
+
{
|
|
300
|
+
return new Promise((resolve) =>
|
|
301
|
+
{
|
|
302
|
+
if (!pJoinConfig || !Array.isArray(pRecords) || pRecords.length < 1 || !this.pict.EntityProvider) { return resolve(pRecords); }
|
|
303
|
+
const tmpIDs = [];
|
|
304
|
+
const tmpSeen = {};
|
|
305
|
+
for (let i = 0; i < pRecords.length; i++)
|
|
306
|
+
{
|
|
307
|
+
const tmpID = pRecords[i][pJoinConfig.FKColumn];
|
|
308
|
+
if (tmpID !== undefined && tmpID !== null && tmpID !== '' && !tmpSeen[tmpID]) { tmpSeen[tmpID] = true; tmpIDs.push(tmpID); }
|
|
309
|
+
}
|
|
310
|
+
if (tmpIDs.length < 1) { return resolve(pRecords); }
|
|
311
|
+
const tmpFilter = `FBL~${pJoinConfig.PKColumn}~INN~${tmpIDs.join(',')}`;
|
|
312
|
+
this.pict.EntityProvider.getEntitySetPage(pJoinConfig.Entity, tmpFilter, 0, tmpIDs.length,
|
|
313
|
+
(pError, pJoinRecords) =>
|
|
314
|
+
{
|
|
315
|
+
if (pError)
|
|
316
|
+
{
|
|
317
|
+
this.pict.log.warn(`Pict-Section-Picker [${pJoinConfig.Entity}] join fetch failed; showing un-joined text.`, pError);
|
|
318
|
+
return resolve(pRecords);
|
|
319
|
+
}
|
|
320
|
+
const tmpMap = {};
|
|
321
|
+
const tmpJoinList = Array.isArray(pJoinRecords) ? pJoinRecords : [];
|
|
322
|
+
for (let i = 0; i < tmpJoinList.length; i++) { tmpMap[tmpJoinList[i][pJoinConfig.PKColumn]] = tmpJoinList[i]; }
|
|
323
|
+
for (let i = 0; i < pRecords.length; i++)
|
|
324
|
+
{
|
|
325
|
+
const tmpJoinRecord = tmpMap[pRecords[i][pJoinConfig.FKColumn]];
|
|
326
|
+
if (tmpJoinRecord) { pRecords[i].JoinRecord = tmpJoinRecord; pRecords[i].JoinName = tmpJoinRecord[pJoinConfig.DisplayField]; }
|
|
327
|
+
}
|
|
328
|
+
return resolve(pRecords);
|
|
201
329
|
});
|
|
202
330
|
});
|
|
203
331
|
}
|
|
@@ -215,6 +343,7 @@ class PictProviderPicker extends libPictProvider
|
|
|
215
343
|
const tmpValueField = pConfig.ValueField || `ID${tmpEntity}`;
|
|
216
344
|
const tmpTextField = pConfig.TextField || 'Name';
|
|
217
345
|
const tmpMapRecord = (typeof pConfig.MapRecord === 'function') ? pConfig.MapRecord : false;
|
|
346
|
+
const tmpJoinConfig = this._resolveJoinConfig(pConfig);
|
|
218
347
|
|
|
219
348
|
return (pValue) => new Promise((resolve) =>
|
|
220
349
|
{
|
|
@@ -226,7 +355,23 @@ class PictProviderPicker extends libPictProvider
|
|
|
226
355
|
(pError, pRecord) =>
|
|
227
356
|
{
|
|
228
357
|
if (pError || !pRecord) { return resolve(null); }
|
|
229
|
-
|
|
358
|
+
const fFinish = () =>
|
|
359
|
+
{
|
|
360
|
+
if (tmpMapRecord) { return resolve(tmpMapRecord(pRecord)); }
|
|
361
|
+
const tmpText = tmpJoinConfig
|
|
362
|
+
? this._composeJoinedText(pRecord[tmpTextField], pRecord.JoinName, tmpJoinConfig.First, tmpJoinConfig.Separator)
|
|
363
|
+
: pRecord[tmpTextField];
|
|
364
|
+
return resolve({ Value: pRecord[tmpValueField], Text: tmpText, Record: pRecord });
|
|
365
|
+
};
|
|
366
|
+
// JoinEntity: resolve the single joined record (cached getEntity) for the compound label.
|
|
367
|
+
const tmpFK = tmpJoinConfig ? pRecord[tmpJoinConfig.FKColumn] : null;
|
|
368
|
+
if (!tmpJoinConfig || tmpFK === undefined || tmpFK === null || tmpFK === '') { return fFinish(); }
|
|
369
|
+
this.pict.EntityProvider.getEntity(tmpJoinConfig.Entity, tmpFK,
|
|
370
|
+
(pJoinError, pJoinRecord) =>
|
|
371
|
+
{
|
|
372
|
+
if (!pJoinError && pJoinRecord) { pRecord.JoinRecord = pJoinRecord; pRecord.JoinName = pJoinRecord[tmpJoinConfig.DisplayField]; }
|
|
373
|
+
return fFinish();
|
|
374
|
+
});
|
|
230
375
|
});
|
|
231
376
|
});
|
|
232
377
|
}
|
|
@@ -324,6 +324,65 @@ class PictViewPicker extends libPictView
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Public: set the picker's value programmatically (e.g. when a host form marshals data into it).
|
|
329
|
+
* Accepts a scalar (single mode) or an array / csv string (multi mode), seeds display text for any
|
|
330
|
+
* unknown values (from the source rows, else async ResolveValue), then repaints.
|
|
331
|
+
* @param {any} pValue
|
|
332
|
+
* @return {PictViewPicker} this
|
|
333
|
+
*/
|
|
334
|
+
setValue(pValue)
|
|
335
|
+
{
|
|
336
|
+
if (this._isMulti())
|
|
337
|
+
{
|
|
338
|
+
let tmpArray = pValue;
|
|
339
|
+
if (tmpArray === undefined || tmpArray === null || tmpArray === '') { tmpArray = []; }
|
|
340
|
+
else if (typeof tmpArray === 'string') { tmpArray = tmpArray.split(',').filter((pPart) => pPart !== ''); }
|
|
341
|
+
else if (!Array.isArray(tmpArray)) { tmpArray = [ tmpArray ]; }
|
|
342
|
+
this._setValue(tmpArray);
|
|
343
|
+
this._seedSelectedRecords(tmpArray);
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
{
|
|
347
|
+
this._setValue(pValue);
|
|
348
|
+
this._selectedText = null;
|
|
349
|
+
this._seedSelectedRecords((pValue === undefined || pValue === null || pValue === '') ? [] : [ pValue ]);
|
|
350
|
+
}
|
|
351
|
+
this.render();
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Ensure each value has a {Value,Text} in _selectedRecords — from the current source rows when
|
|
357
|
+
* present, else (async mode) fetched via ResolveValue and painted in when it resolves.
|
|
358
|
+
* @param {Array<any>} pValues
|
|
359
|
+
*/
|
|
360
|
+
_seedSelectedRecords(pValues)
|
|
361
|
+
{
|
|
362
|
+
pValues.forEach((pVal) =>
|
|
363
|
+
{
|
|
364
|
+
if (pVal === undefined || pVal === null || pVal === '' || this._selectedRecords[String(pVal)]) { return; }
|
|
365
|
+
const tmpRow = this._sourceRows().find((pRow) => String(pRow.Value) === String(pVal));
|
|
366
|
+
if (tmpRow)
|
|
367
|
+
{
|
|
368
|
+
this._selectedRecords[String(pVal)] = { Value: tmpRow.Value, Text: tmpRow.Text };
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (this._isAsync() && typeof this.options.ResolveValue === 'function')
|
|
372
|
+
{
|
|
373
|
+
Promise.resolve(this.options.ResolveValue(pVal)).then((pResolved) =>
|
|
374
|
+
{
|
|
375
|
+
if (pResolved && pResolved.Text)
|
|
376
|
+
{
|
|
377
|
+
this._selectedRecords[String(pVal)] = { Value: pResolved.Value !== undefined ? pResolved.Value : pVal, Text: pResolved.Text };
|
|
378
|
+
if (!this._isMulti()) { this._selectedText = pResolved.Text; }
|
|
379
|
+
this._renderValue();
|
|
380
|
+
}
|
|
381
|
+
}).catch(() => { /* leave the raw value showing */ });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
327
386
|
/** @return {Array<{Value:any, Text:string}>} The current option source rows (async results or static Options). */
|
|
328
387
|
_sourceRows()
|
|
329
388
|
{
|
|
@@ -516,11 +575,51 @@ class PictViewPicker extends libPictView
|
|
|
516
575
|
this._open = true;
|
|
517
576
|
this._highlight = -1;
|
|
518
577
|
this._paintOpen();
|
|
578
|
+
this._positionPop();
|
|
519
579
|
if (this._isAsync() && !this._loaded) { this._loadPage(0, false); }
|
|
520
580
|
const tmpSearch = /** @type {HTMLInputElement} */ (document.getElementById(`PPS_Search_${this.options.PickerHash}`));
|
|
521
581
|
if (tmpSearch) { tmpSearch.focus(); tmpSearch.select(); }
|
|
522
582
|
}
|
|
523
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Position the (fixed) dropdown against the control, flipping above when there's more room there.
|
|
586
|
+
* Because the popover is position:fixed (viewport-anchored), no ancestor overflow can clip it; the
|
|
587
|
+
* trade-off is we set its top/left/width ourselves from the control's rect on open.
|
|
588
|
+
*/
|
|
589
|
+
_positionPop()
|
|
590
|
+
{
|
|
591
|
+
const tmpRoot = document.getElementById(`PPS_${this.options.PickerHash}`);
|
|
592
|
+
if (!tmpRoot) { return; }
|
|
593
|
+
const tmpControl = tmpRoot.querySelector('.pps-control');
|
|
594
|
+
const tmpPop = /** @type {HTMLElement} */ (tmpRoot.querySelector('.pps-pop'));
|
|
595
|
+
const tmpPanel = /** @type {HTMLElement} */ (tmpRoot.querySelector('.pps-panel'));
|
|
596
|
+
if (!tmpControl || !tmpPop) { return; }
|
|
597
|
+
const tmpRect = tmpControl.getBoundingClientRect();
|
|
598
|
+
const tmpGap = 5;
|
|
599
|
+
const tmpMargin = 8;
|
|
600
|
+
const tmpVH = window.innerHeight;
|
|
601
|
+
const tmpVW = window.innerWidth;
|
|
602
|
+
const tmpWidth = Math.max(200, Math.round(tmpRect.width));
|
|
603
|
+
tmpPop.style.width = `${tmpWidth}px`;
|
|
604
|
+
tmpPop.style.left = `${Math.round(Math.max(tmpMargin, Math.min(tmpRect.left, tmpVW - tmpWidth - tmpMargin)))}px`;
|
|
605
|
+
tmpPop.style.right = 'auto';
|
|
606
|
+
const tmpSpaceBelow = tmpVH - tmpRect.bottom - tmpGap - tmpMargin;
|
|
607
|
+
const tmpSpaceAbove = tmpRect.top - tmpGap - tmpMargin;
|
|
608
|
+
// Prefer the natural downward direction; only flip above when the room below is genuinely cramped.
|
|
609
|
+
if (tmpSpaceBelow >= 200 || tmpSpaceBelow >= tmpSpaceAbove)
|
|
610
|
+
{
|
|
611
|
+
tmpPop.style.top = `${Math.round(tmpRect.bottom + tmpGap)}px`;
|
|
612
|
+
tmpPop.style.bottom = 'auto';
|
|
613
|
+
if (tmpPanel) { tmpPanel.style.maxHeight = `${Math.max(140, Math.min(tmpSpaceBelow, 360))}px`; }
|
|
614
|
+
}
|
|
615
|
+
else
|
|
616
|
+
{
|
|
617
|
+
tmpPop.style.top = 'auto';
|
|
618
|
+
tmpPop.style.bottom = `${Math.round(tmpVH - tmpRect.top + tmpGap)}px`;
|
|
619
|
+
if (tmpPanel) { tmpPanel.style.maxHeight = `${Math.max(140, Math.min(tmpSpaceAbove, 360))}px`; }
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
524
623
|
/** Async mode: load + append the next page of results. */
|
|
525
624
|
loadMore()
|
|
526
625
|
{
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export = PictInputTypePicker;
|
|
2
|
+
declare const PictInputTypePicker_base: typeof import("pict-section-form/types/source/providers/Pict-Provider-InputExtension");
|
|
3
|
+
declare class PictInputTypePicker extends PictInputTypePicker_base {
|
|
4
|
+
constructor(pFable: any, pOptions: any, pServiceHash: any);
|
|
5
|
+
getPickerHostID(pRawHTMLID: any): string;
|
|
6
|
+
getPickerHash(pRawHTMLID: any): string;
|
|
7
|
+
getTabularPickerHostID(pRawHTMLID: any, pRowIndex: any): string;
|
|
8
|
+
getTabularPickerHash(pRawHTMLID: any, pRowIndex: any): string;
|
|
9
|
+
getTabularHiddenID(pRawHTMLID: any, pRowIndex: any): string;
|
|
10
|
+
/**
|
|
11
|
+
* Overridable: extra FoxHound scope stanza(s) AND-applied to the entity search. Default reads the
|
|
12
|
+
* descriptor's `GetContextScopeFilter()` hook (set by the host / recordset filter base), else its
|
|
13
|
+
* static `BaseFilter`. Host subclasses override this to read app state (project / spec-year / …).
|
|
14
|
+
*
|
|
15
|
+
* @param {Record<string, any>} pInput @return {string|Array<string>}
|
|
16
|
+
*/
|
|
17
|
+
getContextualSearchFilters(pInput: Record<string, any>): string | Array<string>;
|
|
18
|
+
/** Build the picker config from a form input descriptor. */
|
|
19
|
+
_buildPickerConfig(pInput: any, pHostSelector: any, fOnChange: any): {
|
|
20
|
+
DestinationAddress: any;
|
|
21
|
+
Mode: string;
|
|
22
|
+
Placeholder: any;
|
|
23
|
+
Searchable: boolean;
|
|
24
|
+
Entity: any;
|
|
25
|
+
SearchFields: any;
|
|
26
|
+
ValueField: any;
|
|
27
|
+
TextField: any;
|
|
28
|
+
PageSize: any;
|
|
29
|
+
Options: any;
|
|
30
|
+
JoinEntity: any;
|
|
31
|
+
JoinField: any;
|
|
32
|
+
JoinEntityValueField: any;
|
|
33
|
+
JoinEntityDisplayField: any;
|
|
34
|
+
JoinEntityFirst: any;
|
|
35
|
+
JoinSeparator: any;
|
|
36
|
+
BaseFilter: () => string | string[];
|
|
37
|
+
OnChange: any;
|
|
38
|
+
};
|
|
39
|
+
/** Instantiate (or reuse) the picker view for a config — entity-backed when Entity is set. */
|
|
40
|
+
_instantiatePicker(pPickerHash: any, pConfig: any): any;
|
|
41
|
+
/**
|
|
42
|
+
* Write a picker value into the form: csv to the hidden informary input (+ dataChanged), plus the
|
|
43
|
+
* raw array to `PictForm.ValueArrayAddress` when set (the recordset filter reads Values as an
|
|
44
|
+
* array). The csv-vs-array bridge lives HERE (generic) instead of in each host.
|
|
45
|
+
*/
|
|
46
|
+
_commit(pView: any, pInput: any, pValue: any, pHTMLSelector: any): void;
|
|
47
|
+
_commitTabular(pView: any, pInput: any, pValue: any, pHiddenID: any, pRowIndex: any): void;
|
|
48
|
+
/**
|
|
49
|
+
* Idempotently mount (or reuse) the picker into its host element + seed its value. Called from both
|
|
50
|
+
* onInputInitialize and onDataMarshalToForm because, in the async-virtual filter render, the host
|
|
51
|
+
* element only exists in the real DOM by the marshal pass — whichever hook fires post-DOM wins, and
|
|
52
|
+
* re-calls are harmless (the picker view is reused by hash).
|
|
53
|
+
* @return {boolean} true if the picker is mounted.
|
|
54
|
+
*/
|
|
55
|
+
_mountPicker(pView: any, pInput: any, pValue: any, pHostSelector: any, pPickerHash: any, fOnChange: any): boolean;
|
|
56
|
+
onInputInitialize(pView: any, pGroup: any, pRow: any, pInput: any, pValue: any, pHTMLSelector: any, pTransactionGUID: any): boolean;
|
|
57
|
+
onDataMarshalToForm(pView: any, pGroup: any, pRow: any, pInput: any, pValue: any, pHTMLSelector: any, pTransactionGUID: any): boolean;
|
|
58
|
+
onDataRequest(pView: any, pInput: any, pValue: any, pHTMLSelector: any): boolean;
|
|
59
|
+
/** Idempotent tabular mount (see _mountPicker). @return {boolean} */
|
|
60
|
+
_mountPickerTabular(pView: any, pInput: any, pValue: any, pRowIndex: any): boolean;
|
|
61
|
+
onInputInitializeTabular(pView: any, pGroup: any, pInput: any, pValue: any, pHTMLSelector: any, pRowIndex: any, pTransactionGUID: any): boolean;
|
|
62
|
+
onDataMarshalToFormTabular(pView: any, pGroup: any, pInput: any, pValue: any, pHTMLSelector: any, pRowIndex: any, pTransactionGUID: any): boolean;
|
|
63
|
+
onDataRequestTabular(pView: any, pInput: any, pValue: any, pHTMLSelector: any, pRowIndex: any): boolean;
|
|
64
|
+
}
|
|
65
|
+
declare namespace PictInputTypePicker {
|
|
66
|
+
export { PictInputTypePicker, registerPickerInputType, buildPickerInputTemplates, _DEFAULT_CONFIGURATION as default_configuration };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Register the Picker InputType on a pict instance: the input-extension provider + its metatemplate(s).
|
|
70
|
+
* Idempotent. Requires `pict-section-form` loaded (PictFormSectionDefaultTemplateProvider present) and
|
|
71
|
+
* the `Pict-Section-Picker` provider registered.
|
|
72
|
+
*
|
|
73
|
+
* @param {any} pPict - the pict instance.
|
|
74
|
+
* @param {Record<string, any>} [pOptions]
|
|
75
|
+
* - InputTypeName {string} - the InputType string (default 'Picker').
|
|
76
|
+
* - ProviderHash {string} - the input-extension provider service hash (default 'Pict-Input-Picker').
|
|
77
|
+
* - ProviderClass {Function} - provider class to register (default PictInputTypePicker; a host
|
|
78
|
+
* passes a subclass that overrides getContextualSearchFilters for its scoping).
|
|
79
|
+
* - TemplatePrefix {string|Array<string>} - the form template prefix(es) to inject the metatemplate
|
|
80
|
+
* under (default 'Pict-MT-Base'; Headlight uses its theme prefixes).
|
|
81
|
+
* @return {boolean} true if registered.
|
|
82
|
+
*/
|
|
83
|
+
declare function registerPickerInputType(pPict: any, pOptions?: Record<string, any>): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Build the InputType metatemplate entries (a hidden informary input + a host element the picker
|
|
86
|
+
* renders into) for a given InputType name + provider hash. Injected via injectTemplateSet.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} pInputTypeName - e.g. 'Picker'.
|
|
89
|
+
* @param {string} pProviderHash - the input-extension provider service hash to auto-attach.
|
|
90
|
+
* @return {Array<Record<string, any>>}
|
|
91
|
+
*/
|
|
92
|
+
declare function buildPickerInputTemplates(pInputTypeName: string, pProviderHash: string): Array<Record<string, any>>;
|
|
93
|
+
/** @type {Record<string, any>} */
|
|
94
|
+
declare const _DEFAULT_CONFIGURATION: Record<string, any>;
|
|
95
|
+
//# sourceMappingURL=Pict-Section-Picker-FormInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pict-Section-Picker-FormInput.d.ts","sourceRoot":"","sources":["../../source/form/Pict-Section-Picker-FormInput.js"],"names":[],"mappings":";;AAsEA;IAEC,2DAKC;IAGD,yCAAmE;IACnE,uCAA4D;IAC5D,gEAAsG;IACtG,8DAA2F;IAC3F,4DAAuG;IAEvG;;;;;;OAMG;IACH,mCAFW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAkB,MAAM,GAAC,KAAK,CAAC,MAAM,CAAC,CAWnE;IAED,4DAA4D;IAC5D;;;;;;;;;;;;;;;;;;;MA0BC;IAED,8FAA8F;IAC9F,wDAWC;IAED;;;;OAIG;IACH,wEAUC;IAED,2FAKC;IAED;;;;;;OAMG;IACH,0GAFY,OAAO,CAUlB;IAID,oIAMC;IAED,sIAYC;IAED,iFAMC;IAID,qEAAqE;IACrE,2EAD0D,OAAO,CAahE;IAED,gJAIC;IAED,kJAQC;IAED,wGAMC;CACD;;;;AAED;;;;;;;;;;;;;;GAcG;AACH,gDAVW,GAAG,aACH,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAOlB,OAAO,CAwBlB;AAjRD;;;;;;;GAOG;AACH,2DAJW,MAAM,iBACN,MAAM,GACL,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAoCrC;AAnDD,kCAAkC;AAClC,sCADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAO5B"}
|
|
@@ -46,14 +46,63 @@ declare class PictProviderPicker extends libPictProvider {
|
|
|
46
46
|
* - TextField {string} - record field used as the option Text (default `Name`).
|
|
47
47
|
* - PageSize {number} - records per page (default 20).
|
|
48
48
|
* - Sort {string} - optional field to sort ascending (adds `FSF~<field>~ASC~0`).
|
|
49
|
-
* - BaseFilter {string} - optional always-applied FoxHound filter (AND),
|
|
49
|
+
* - BaseFilter {string|Array<string>|function} - optional always-applied FoxHound filter (AND),
|
|
50
|
+
* e.g. `FBV~IDCustomer~EQ~1`. May be a **function** `(searchTerm, page) => string|string[]`
|
|
51
|
+
* evaluated on every search — the generic hook for host-injected CONTEXTUAL scoping (project,
|
|
52
|
+
* tenant, spec-year, …). The module stays agnostic; the host supplies the closure.
|
|
50
53
|
* - MapRecord {function} - optional `(record) => {Value, Text}` mapper (overrides Value/TextField).
|
|
54
|
+
* - JoinEntity {string} - optional second entity to JOIN for a compound display (e.g. a `LineItem`
|
|
55
|
+
* shown with its `Project`). Each searched row must carry the FK (`JoinField`). Because Meadow
|
|
56
|
+
* can't join in one read, this is fetch-then-merge: after the primary page resolves, the unique
|
|
57
|
+
* FK ids drive ONE `FBL~ID{JoinEntity}~INN~<ids>` request, and the joined display field is
|
|
58
|
+
* stitched onto each row (as `Record.JoinName` / `Record.JoinRecord`) + composed into the Text.
|
|
59
|
+
* - JoinField {string} - the FK column ON THE SEARCHED ROW pointing at JoinEntity (default `ID{JoinEntity}`).
|
|
60
|
+
* - JoinEntityValueField {string} - the PK column on JoinEntity to match (default `ID{JoinEntity}`).
|
|
61
|
+
* - JoinEntityDisplayField {string} - the JoinEntity field to display (default `Name`).
|
|
62
|
+
* - JoinEntityFirst {boolean} - put the joined value first in the compound (default `true`):
|
|
63
|
+
* `JoinName - baseText`; when `false`, `baseText - JoinName`.
|
|
64
|
+
* - JoinSeparator {string} - the compound separator (default `' - '`).
|
|
51
65
|
* @return {(pSearchTerm: string, pPage: number) => Promise<{results: Array<any>, hasMore: boolean}>}
|
|
52
66
|
*/
|
|
53
67
|
createEntityDataProvider(pConfig: Record<string, any>): (pSearchTerm: string, pPage: number) => Promise<{
|
|
54
68
|
results: Array<any>;
|
|
55
69
|
hasMore: boolean;
|
|
56
70
|
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the JoinEntity options off an entity-source config into a normalized internal shape, or
|
|
73
|
+
* `false` when no JoinEntity is configured. Centralizes the defaults so the DataProvider and the
|
|
74
|
+
* ResolveValue builders agree.
|
|
75
|
+
*
|
|
76
|
+
* @param {Record<string, any>} pConfig
|
|
77
|
+
* @return {false | {Entity:string, FKColumn:string, PKColumn:string, DisplayField:string, First:boolean, Separator:string}}
|
|
78
|
+
*/
|
|
79
|
+
_resolveJoinConfig(pConfig: Record<string, any>): false | {
|
|
80
|
+
Entity: string;
|
|
81
|
+
FKColumn: string;
|
|
82
|
+
PKColumn: string;
|
|
83
|
+
DisplayField: string;
|
|
84
|
+
First: boolean;
|
|
85
|
+
Separator: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Compose a compound display from a base text + a joined value, honoring ordering + separator.
|
|
89
|
+
* Falls back to just the base text when there is no joined value.
|
|
90
|
+
*
|
|
91
|
+
* @param {any} pBaseText @param {any} pJoinText @param {boolean} pFirst @param {string} pSeparator
|
|
92
|
+
* @return {any}
|
|
93
|
+
*/
|
|
94
|
+
_composeJoinedText(pBaseText: any, pJoinText: any, pFirst: boolean, pSeparator: string): any;
|
|
95
|
+
/**
|
|
96
|
+
* Fetch-then-merge the join entity for a page of searched records. Collects the unique FK ids the
|
|
97
|
+
* rows carry (`JoinConfig.FKColumn`), issues ONE `FBL~{PKColumn}~INN~<ids>` request against the join
|
|
98
|
+
* entity, and stitches `JoinRecord` + `JoinName` onto each searched row. Resolves the (mutated) same
|
|
99
|
+
* array; on any error or when there's nothing to join, resolves the records un-decorated (the Text
|
|
100
|
+
* gracefully degrades to the base field).
|
|
101
|
+
*
|
|
102
|
+
* @param {Array<any>} pRecords @param {false | Record<string, any>} pJoinConfig
|
|
103
|
+
* @return {Promise<Array<any>>}
|
|
104
|
+
*/
|
|
105
|
+
_decorateRecordsWithJoin(pRecords: Array<any>, pJoinConfig: false | Record<string, any>): Promise<Array<any>>;
|
|
57
106
|
/**
|
|
58
107
|
* Build a `ResolveValue(value) => Promise<{Value,Text}>` for an entity-backed picker, so a
|
|
59
108
|
* pre-bound ID resolves to its display text on first render (fetched + cached by `getEntity`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pict-Provider-Picker.d.ts","sourceRoot":"","sources":["../../source/providers/Pict-Provider-Picker.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Pict-Provider-Picker.d.ts","sourceRoot":"","sources":["../../source/providers/Pict-Provider-Picker.js"],"names":[],"mappings":";AAmFA;;;GAGG;AACH;IAEC,2DASC;IAED;;;;;;;;;;;;;;OAcG;IACH,0BAZW,MAAM,WAEN,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAQlB,GAAG,CAqBd;IAED;;;;;;;;;;;OAWG;IACH,iCAJW,KAAK,CAAC,MAAM,CAAC,SACb,MAAM,GACL,MAAM,CAWjB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,kCAzBW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAuBlB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CA6DnG;IAED;;;;;;;OAOG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAClB,KAAK,GAAG;QAAC,MAAM,EAAC,MAAM,CAAC;QAAC,QAAQ,EAAC,MAAM,CAAC;QAAC,QAAQ,EAAC,MAAM,CAAC;QAAC,YAAY,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,OAAO,CAAC;QAAC,SAAS,EAAC,MAAM,CAAA;KAAC,CAe1H;IAED;;;;;;OAMG;IACH,8BAHW,GAAG,aAAoB,GAAG,UAAoB,OAAO,cAAiB,MAAM,GAC3E,GAAG,CAOd;IAED;;;;;;;;;OASG;IACH,mCAHW,KAAK,CAAC,GAAG,CAAC,eAAmB,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACvD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAmC9B;IAED;;;;;;OAMG;IACH,kCAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAClB,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAuCxC;IAED;;;;;;;;;OASG;IACH,gCAJW,MAAM,WACN,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAClB,GAAG,CAQd;CACD;;;;;AAjUD,kCAAkC;AAClC,sCADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAO5B"}
|
|
@@ -34,6 +34,20 @@ declare class PictViewPicker extends libPictView {
|
|
|
34
34
|
*/
|
|
35
35
|
_setValue(pValue: any): void;
|
|
36
36
|
_value: any;
|
|
37
|
+
/**
|
|
38
|
+
* Public: set the picker's value programmatically (e.g. when a host form marshals data into it).
|
|
39
|
+
* Accepts a scalar (single mode) or an array / csv string (multi mode), seeds display text for any
|
|
40
|
+
* unknown values (from the source rows, else async ResolveValue), then repaints.
|
|
41
|
+
* @param {any} pValue
|
|
42
|
+
* @return {PictViewPicker} this
|
|
43
|
+
*/
|
|
44
|
+
setValue(pValue: any): PictViewPicker;
|
|
45
|
+
/**
|
|
46
|
+
* Ensure each value has a {Value,Text} in _selectedRecords — from the current source rows when
|
|
47
|
+
* present, else (async mode) fetched via ResolveValue and painted in when it resolves.
|
|
48
|
+
* @param {Array<any>} pValues
|
|
49
|
+
*/
|
|
50
|
+
_seedSelectedRecords(pValues: Array<any>): void;
|
|
37
51
|
/** @return {Array<{Value:any, Text:string}>} The current option source rows (async results or static Options). */
|
|
38
52
|
_sourceRows(): Array<{
|
|
39
53
|
Value: any;
|
|
@@ -66,6 +80,12 @@ declare class PictViewPicker extends libPictView {
|
|
|
66
80
|
onControlKey(pEvent: any): void;
|
|
67
81
|
/** Open the dropdown and focus the search box. */
|
|
68
82
|
open(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Position the (fixed) dropdown against the control, flipping above when there's more room there.
|
|
85
|
+
* Because the popover is position:fixed (viewport-anchored), no ancestor overflow can clip it; the
|
|
86
|
+
* trade-off is we set its top/left/width ourselves from the control's rect on open.
|
|
87
|
+
*/
|
|
88
|
+
_positionPop(): void;
|
|
69
89
|
/** Async mode: load + append the next page of results. */
|
|
70
90
|
loadMore(): void;
|
|
71
91
|
/** Close the dropdown. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PictView-Picker.d.ts","sourceRoot":"","sources":["../../source/views/PictView-Picker.js"],"names":[],"mappings":";AA6KA;IAEC,2DA0CC;IAlCA,sBAA2E;IAQ3E,eAAkB;IAClB,gBAAiB;IACjB,mBAAoB;IAEpB,sBAAwB;IACxB,cAAc;IACd,kBAAqB;IACrB,kBAAqB;IACrB,iBAAoB;IACpB,6BAAwB;IACxB,mBAAyB;IAGzB,eAAiB;IACjB,qBAA0B;IAc3B,6FAA6F;IAC7F,YADa,OAAO,CAInB;IAED,8EAA8E;IAC9E,YADa,OAAO,CAInB;IAED,4EAA4E;IAC5E,UADa,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAM/B;IAED,qGAAqG;IACrG,8BA+BC;IAED;;;OAGG;IACH,YAHY,GAAG,CAgBd;IAED;;;;OAIG;IACH,kBAFW,GAAG,QA6Bb;IAvBC,YAAoB;IAyBtB,kHAAkH;IAClH,eADa,KAAK,CAAC;QAAC,KAAK,EAAC,GAAG,CAAC;QAAC,IAAI,EAAC,MAAM,CAAA;KAAC,CAAC,CAK3C;IAED;;;OAGG;IACH,mCAgGC;IAED;;;;;OAKG;IACH,sBAHW,GAAG,GACF;QAAC,KAAK,EAAC,GAAG,CAAC;QAAC,IAAI,EAAC,MAAM,CAAA;KAAC,GAAC,IAAI,CAQxC;IAED;;;;OAIG;IACH,iBAHW,MAAM,WACN,OAAO,QA4BjB;IAWD,uCAAuC;IACvC,0BAIC;IAED,+EAA+E;IAC/E,gCAWC;IAED,kDAAkD;IAClD,
|
|
1
|
+
{"version":3,"file":"PictView-Picker.d.ts","sourceRoot":"","sources":["../../source/views/PictView-Picker.js"],"names":[],"mappings":";AA6KA;IAEC,2DA0CC;IAlCA,sBAA2E;IAQ3E,eAAkB;IAClB,gBAAiB;IACjB,mBAAoB;IAEpB,sBAAwB;IACxB,cAAc;IACd,kBAAqB;IACrB,kBAAqB;IACrB,iBAAoB;IACpB,6BAAwB;IACxB,mBAAyB;IAGzB,eAAiB;IACjB,qBAA0B;IAc3B,6FAA6F;IAC7F,YADa,OAAO,CAInB;IAED,8EAA8E;IAC9E,YADa,OAAO,CAInB;IAED,4EAA4E;IAC5E,UADa,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAM/B;IAED,qGAAqG;IACrG,8BA+BC;IAED;;;OAGG;IACH,YAHY,GAAG,CAgBd;IAED;;;;OAIG;IACH,kBAFW,GAAG,QA6Bb;IAvBC,YAAoB;IAyBtB;;;;;;OAMG;IACH,iBAHW,GAAG,GACF,cAAc,CAqBzB;IAED;;;;OAIG;IACH,8BAFW,KAAK,CAAC,GAAG,CAAC,QA0BpB;IAED,kHAAkH;IAClH,eADa,KAAK,CAAC;QAAC,KAAK,EAAC,GAAG,CAAC;QAAC,IAAI,EAAC,MAAM,CAAA;KAAC,CAAC,CAK3C;IAED;;;OAGG;IACH,mCAgGC;IAED;;;;;OAKG;IACH,sBAHW,GAAG,GACF;QAAC,KAAK,EAAC,GAAG,CAAC;QAAC,IAAI,EAAC,MAAM,CAAA;KAAC,GAAC,IAAI,CAQxC;IAED;;;;OAIG;IACH,iBAHW,MAAM,WACN,OAAO,QA4BjB;IAWD,uCAAuC;IACvC,0BAIC;IAED,+EAA+E;IAC/E,gCAWC;IAED,kDAAkD;IAClD,aASC;IAED;;;;OAIG;IACH,qBAgCC;IAED,0DAA0D;IAC1D,iBAMC;IAED,0BAA0B;IAC1B,cAKC;IAED,6DAA6D;IAC7D,mBAIC;IAED,kFAAkF;IAClF,oBAKC;IAED;sGACkG;IAClG,qBAKC;IAED,2EAA2E;IAC3E,eADY,MAAM,QAejB;IAED,iGAAiG;IACjG,+BAgCC;IAED;;;;OAIG;IACH,kBAFW,MAAM,QA4ChB;IAED,sGAAsG;IACtG,sBADa,KAAK,CAAC;QAAC,KAAK,EAAC,GAAG,CAAC;QAAC,IAAI,EAAC,MAAM,CAAA;KAAC,CAAC,CAI3C;IAED;;;;OAIG;IACH,yBA6CC;IAED,oFAAoF;IACpF,iCAWC;CAWD;;;;;AA30BD,kCAAkC;AAClC,sCADW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAyK5B"}
|