pict-section-form 1.2.3 → 1.3.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/eslint.config.mjs +2 -2
- package/package.json +7 -2
- package/source/providers/Pict-Provider-DynamicSolver.js +2 -0
- package/source/providers/Pict-Provider-Informary.js +22 -21
- package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates-ReadOnly.js +10 -0
- package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates.js +23 -0
- package/source/providers/inputs/Pict-Provider-Input-ObjectEditor.js +478 -0
- package/test/Pict-Provider-Informary-RenderSelector_tests.js +145 -0
- package/test/Pict-Provider-Informary_tests.js +111 -0
- package/test/PictSectionForm-Basic_tests.js +3 -0
- package/tsconfig.build.json +16 -0
- package/tsconfig.json +1 -1
- package/types/source/providers/Pict-Provider-DynamicTabularData.d.ts +33 -0
- package/types/source/providers/Pict-Provider-DynamicTabularData.d.ts.map +1 -1
- package/types/source/providers/Pict-Provider-Informary.d.ts +12 -13
- package/types/source/providers/Pict-Provider-Informary.d.ts.map +1 -1
- package/types/source/providers/layouts/Pict-Layout-Custom.d.ts.map +1 -1
- package/types/source/providers/layouts/Pict-Layout-Record.d.ts.map +1 -1
- package/types/source/providers/layouts/Pict-Layout-Tabular.d.ts +202 -0
- package/types/source/providers/layouts/Pict-Layout-Tabular.d.ts.map +1 -1
- package/types/source/providers/layouts/Pict-Layout-VerticalRecord.d.ts.map +1 -1
- package/types/source/services/ManifestFactory.d.ts +2 -1
- package/types/source/services/ManifestFactory.d.ts.map +1 -1
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pict-Provider-Input-ObjectEditor.js
|
|
3
|
+
*
|
|
4
|
+
* A pict-section-form input provider that embeds pict-section-objecteditor as
|
|
5
|
+
* an interactive JSON-object editor. In edit mode the tree is editable
|
|
6
|
+
* (Editable:true); in view / read-only mode the same tree renders read-only
|
|
7
|
+
* (Editable:false). Unlike the Diagram input, the object editor is a light,
|
|
8
|
+
* pure-JS view, so there is no lazy heavy-bundle split — both modes mount the
|
|
9
|
+
* same view and only the Editable flag differs.
|
|
10
|
+
*
|
|
11
|
+
* The object the editor mutates lives in a per-input AppData stash at
|
|
12
|
+
*
|
|
13
|
+
* AppData._PictInputObjectEditor.<InputHash>.Data
|
|
14
|
+
*
|
|
15
|
+
* The object editor mutates that object in place. On a data request (the form
|
|
16
|
+
* gathering its values for a save) the stash is serialized to JSON and written
|
|
17
|
+
* into the input's hidden field, so the form marshals it like any other value.
|
|
18
|
+
*
|
|
19
|
+
* Incoming values may be a live object OR a JSON string — both are accepted.
|
|
20
|
+
*
|
|
21
|
+
* Descriptor shape:
|
|
22
|
+
*
|
|
23
|
+
* {
|
|
24
|
+
* "Name": "Custom Properties",
|
|
25
|
+
* "Hash": "CustomProperties",
|
|
26
|
+
* "DataType": "Object",
|
|
27
|
+
* "PictForm": {
|
|
28
|
+
* "InputType": "ObjectEditor",
|
|
29
|
+
* "ReadOnly": false, // true renders a read-only tree
|
|
30
|
+
* "ObjectEditor": { // all optional
|
|
31
|
+
* "Editable": true, // explicit override of ReadOnly
|
|
32
|
+
* "InitialExpandDepth": 2
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Runtime API (parity with the other section inputs):
|
|
38
|
+
*
|
|
39
|
+
* provider.setMode(inputHash, 'edit' | 'view', fCallback)
|
|
40
|
+
* provider.getMode(inputHash)
|
|
41
|
+
* provider.toggleMode(inputHash)
|
|
42
|
+
* provider.commit(inputHash, fCallback)
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const libPictSectionInputExtension = require('../Pict-Provider-InputExtension.js');
|
|
46
|
+
const libPictSectionObjectEditor = require('pict-section-objecteditor');
|
|
47
|
+
|
|
48
|
+
const _DefaultProviderConfiguration =
|
|
49
|
+
{
|
|
50
|
+
ProviderIdentifier: 'Pict-Input-ObjectEditor',
|
|
51
|
+
|
|
52
|
+
AutoInitialize: true,
|
|
53
|
+
AutoInitializeOrdinal: 0,
|
|
54
|
+
|
|
55
|
+
AutoSolveWithApp: false
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} Instance
|
|
60
|
+
* @property {string} mode - 'edit' or 'view'
|
|
61
|
+
* @property {string} slotID - The HTML ID selector of the content slot for this input
|
|
62
|
+
* @property {Object} viewInstance - The object editor view instance
|
|
63
|
+
* @property {string} viewHash - The hash of the object editor view
|
|
64
|
+
* @property {Object} input - The input definition object
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
class PictInputObjectEditor extends libPictSectionInputExtension
|
|
68
|
+
{
|
|
69
|
+
/**
|
|
70
|
+
* Creates an instance of the PictInputObjectEditor class.
|
|
71
|
+
*
|
|
72
|
+
* @param {import('pict')} pFable - The Pict instance.
|
|
73
|
+
* @param {Record<string, any>} [pOptions] - The options for the provider.
|
|
74
|
+
* @param {string} [pServiceHash] - The service hash for the provider.
|
|
75
|
+
*/
|
|
76
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
77
|
+
{
|
|
78
|
+
let tmpOptions = Object.assign({}, JSON.parse(JSON.stringify(_DefaultProviderConfiguration)), pOptions);
|
|
79
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
80
|
+
|
|
81
|
+
/** @type {import('pict')} */ this.pict;
|
|
82
|
+
/** @type {any} */ this.log;
|
|
83
|
+
|
|
84
|
+
// inputHash -> Instance
|
|
85
|
+
/** @type {Record<String, Instance>} */
|
|
86
|
+
this._instances = {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ----------------------------------------------------------------------------
|
|
90
|
+
// Helpers
|
|
91
|
+
// ----------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} pInputHTMLID - The RawHTMLID of the input.
|
|
95
|
+
* @returns {string} The HTML ID selector for the content display slot.
|
|
96
|
+
*/
|
|
97
|
+
getContentDisplayHTMLID(pInputHTMLID)
|
|
98
|
+
{
|
|
99
|
+
return `#DISPLAY-FOR-${pInputHTMLID}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The AppData address where this input's editable object is stashed.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} pInputHash - The input Hash.
|
|
106
|
+
* @returns {string} The dotted AppData address the object editor binds to.
|
|
107
|
+
*/
|
|
108
|
+
getObjectDataAddress(pInputHash)
|
|
109
|
+
{
|
|
110
|
+
return `AppData._PictInputObjectEditor.${pInputHash}.Data`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the incoming value to a plain object, accepting either a live
|
|
115
|
+
* object or a JSON string. Falls back to the input's Content / Default /
|
|
116
|
+
* an empty object.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} pInput - The input definition object.
|
|
119
|
+
* @param {any} pValue - The value provided for the input.
|
|
120
|
+
* @returns {Object} The resolved object to edit/display.
|
|
121
|
+
*/
|
|
122
|
+
_resolveValue(pInput, pValue)
|
|
123
|
+
{
|
|
124
|
+
let tmpCandidate = pValue;
|
|
125
|
+
if (tmpCandidate === null || typeof tmpCandidate === 'undefined' || tmpCandidate === '')
|
|
126
|
+
{
|
|
127
|
+
if (pInput && typeof pInput.Content !== 'undefined' && pInput.Content !== null && pInput.Content !== '')
|
|
128
|
+
{
|
|
129
|
+
tmpCandidate = pInput.Content;
|
|
130
|
+
}
|
|
131
|
+
else if (pInput && typeof pInput.Default !== 'undefined' && pInput.Default !== null && pInput.Default !== '')
|
|
132
|
+
{
|
|
133
|
+
tmpCandidate = pInput.Default;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof tmpCandidate === 'string')
|
|
138
|
+
{
|
|
139
|
+
let tmpTrimmed = tmpCandidate.trim();
|
|
140
|
+
if (tmpTrimmed.length < 1)
|
|
141
|
+
{
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
try
|
|
145
|
+
{
|
|
146
|
+
let tmpParsed = JSON.parse(tmpTrimmed);
|
|
147
|
+
return (tmpParsed && typeof tmpParsed === 'object') ? tmpParsed : { Value: tmpParsed };
|
|
148
|
+
}
|
|
149
|
+
catch (pErr)
|
|
150
|
+
{
|
|
151
|
+
if (this.log)
|
|
152
|
+
{
|
|
153
|
+
this.log.warn('[Pict-Input-ObjectEditor] value was a non-JSON string; wrapping as { Value }',
|
|
154
|
+
{ error: pErr.message });
|
|
155
|
+
}
|
|
156
|
+
return { Value: tmpCandidate };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (tmpCandidate && typeof tmpCandidate === 'object')
|
|
161
|
+
{
|
|
162
|
+
return tmpCandidate;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Determine whether the editor should be editable for this input. An
|
|
170
|
+
* explicit PictForm.ObjectEditor.Editable boolean wins; otherwise the tree
|
|
171
|
+
* is editable unless PictForm.ReadOnly is true.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} pInput - The input definition object.
|
|
174
|
+
* @returns {boolean} True if the tree should be editable.
|
|
175
|
+
*/
|
|
176
|
+
_resolveEditable(pInput)
|
|
177
|
+
{
|
|
178
|
+
let tmpPictForm = (pInput && pInput.PictForm) || {};
|
|
179
|
+
if (tmpPictForm.ObjectEditor && typeof tmpPictForm.ObjectEditor.Editable === 'boolean')
|
|
180
|
+
{
|
|
181
|
+
return tmpPictForm.ObjectEditor.Editable;
|
|
182
|
+
}
|
|
183
|
+
return (tmpPictForm.ReadOnly !== true);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Ensure the per-input AppData stash exists and return its wrapper.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} pInputHash - The input Hash.
|
|
190
|
+
* @returns {Object} The stash wrapper { Data }.
|
|
191
|
+
*/
|
|
192
|
+
_ensureStash(pInputHash)
|
|
193
|
+
{
|
|
194
|
+
if (!this.pict.AppData._PictInputObjectEditor)
|
|
195
|
+
{
|
|
196
|
+
this.pict.AppData._PictInputObjectEditor = {};
|
|
197
|
+
}
|
|
198
|
+
if (!this.pict.AppData._PictInputObjectEditor[pInputHash])
|
|
199
|
+
{
|
|
200
|
+
this.pict.AppData._PictInputObjectEditor[pInputHash] = { Data: {} };
|
|
201
|
+
}
|
|
202
|
+
return this.pict.AppData._PictInputObjectEditor[pInputHash];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} pInputHash - The input Hash.
|
|
207
|
+
* @returns {Object} The current edited object from the stash.
|
|
208
|
+
*/
|
|
209
|
+
_currentObject(pInputHash)
|
|
210
|
+
{
|
|
211
|
+
return this._ensureStash(pInputHash).Data;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Serialize an object into the input's hidden field.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} pInputHTMLID - The RawHTMLID of the input.
|
|
218
|
+
* @param {Object} pObject - The object to serialize.
|
|
219
|
+
* @param {boolean} [pDispatchChange] - When true, dispatch a DOM change event.
|
|
220
|
+
* @returns {boolean} True if the value was written.
|
|
221
|
+
*/
|
|
222
|
+
_writeHiddenInputValue(pInputHTMLID, pObject, pDispatchChange)
|
|
223
|
+
{
|
|
224
|
+
let tmpEl = (typeof document !== 'undefined') ? document.getElementById(pInputHTMLID) : null;
|
|
225
|
+
if (!tmpEl) return false;
|
|
226
|
+
|
|
227
|
+
let tmpString = '';
|
|
228
|
+
try
|
|
229
|
+
{
|
|
230
|
+
tmpString = (pObject === null || typeof pObject === 'undefined') ? '' : JSON.stringify(pObject);
|
|
231
|
+
}
|
|
232
|
+
catch (pErr)
|
|
233
|
+
{
|
|
234
|
+
if (this.log)
|
|
235
|
+
{
|
|
236
|
+
this.log.warn('[Pict-Input-ObjectEditor] could not stringify object for hidden input',
|
|
237
|
+
{ error: pErr.message });
|
|
238
|
+
}
|
|
239
|
+
tmpString = '';
|
|
240
|
+
}
|
|
241
|
+
tmpEl.value = tmpString;
|
|
242
|
+
|
|
243
|
+
if (pDispatchChange)
|
|
244
|
+
{
|
|
245
|
+
try { tmpEl.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
246
|
+
catch (pErr) { /* jsdom may lack Event */ }
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ----------------------------------------------------------------------------
|
|
252
|
+
// Mount / render
|
|
253
|
+
// ----------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Mount (or re-render) the object editor for an input. Re-uses the view
|
|
257
|
+
* instance across re-renders, re-painting into the (possibly fresh) slot.
|
|
258
|
+
*
|
|
259
|
+
* @param {Object} pInput - The input definition object.
|
|
260
|
+
* @param {any} pValue - The value to edit/display.
|
|
261
|
+
* @param {boolean} pEditable - Whether the tree is editable.
|
|
262
|
+
* @param {Function} [fCallback] - Optional completion callback.
|
|
263
|
+
*/
|
|
264
|
+
_mount(pInput, pValue, pEditable, fCallback)
|
|
265
|
+
{
|
|
266
|
+
let tmpRawHTMLID = pInput.Macro.RawHTMLID;
|
|
267
|
+
let tmpSlotID = this.getContentDisplayHTMLID(tmpRawHTMLID);
|
|
268
|
+
let tmpViewHash = `Pict-Input-ObjectEditor-${pInput.Hash}`;
|
|
269
|
+
|
|
270
|
+
// Seed the per-input stash with the resolved object (the editor mutates
|
|
271
|
+
// this object in place).
|
|
272
|
+
let tmpStash = this._ensureStash(pInput.Hash);
|
|
273
|
+
tmpStash.Data = this._resolveValue(pInput, pValue);
|
|
274
|
+
|
|
275
|
+
let tmpView = this.pict.views[tmpViewHash];
|
|
276
|
+
if (!tmpView)
|
|
277
|
+
{
|
|
278
|
+
let tmpOverrides = (pInput.PictForm && pInput.PictForm.ObjectEditor) || {};
|
|
279
|
+
let tmpEditorOptions =
|
|
280
|
+
{
|
|
281
|
+
ViewIdentifier: tmpViewHash,
|
|
282
|
+
AutoRender: false,
|
|
283
|
+
Editable: pEditable,
|
|
284
|
+
ObjectDataAddress: this.getObjectDataAddress(pInput.Hash),
|
|
285
|
+
// Point this instance's container renderable at our own slot so
|
|
286
|
+
// multiple editors on a page never share a destination.
|
|
287
|
+
Renderables:
|
|
288
|
+
[
|
|
289
|
+
{
|
|
290
|
+
RenderableHash: 'ObjectEditor-Container',
|
|
291
|
+
TemplateHash: 'ObjectEditor-Container-Template',
|
|
292
|
+
DestinationAddress: tmpSlotID,
|
|
293
|
+
RenderMethod: 'replace'
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
if (typeof tmpOverrides.InitialExpandDepth === 'number')
|
|
298
|
+
{
|
|
299
|
+
tmpEditorOptions.InitialExpandDepth = tmpOverrides.InitialExpandDepth;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.pict.addView(tmpViewHash, tmpEditorOptions, libPictSectionObjectEditor);
|
|
303
|
+
tmpView = this.pict.views[tmpViewHash];
|
|
304
|
+
// The object editor builds its per-type node renderers in onBeforeInitialize; a view added
|
|
305
|
+
// after the boot cycle isn't initialized automatically, so initialize it before first render.
|
|
306
|
+
if (tmpView && typeof tmpView.initialize === 'function') { tmpView.initialize(); }
|
|
307
|
+
}
|
|
308
|
+
else
|
|
309
|
+
{
|
|
310
|
+
// Re-use: update the mutable option before re-rendering into the slot.
|
|
311
|
+
tmpView.options.Editable = pEditable;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!tmpView)
|
|
315
|
+
{
|
|
316
|
+
let tmpErr = new Error('Failed to instantiate ObjectEditor view ' + tmpViewHash);
|
|
317
|
+
if (this.log) this.log.error('[Pict-Input-ObjectEditor] addView returned nothing', { viewHash: tmpViewHash });
|
|
318
|
+
if (typeof fCallback === 'function') fCallback(tmpErr);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let tmpInst = this._instances[pInput.Hash] || {};
|
|
323
|
+
tmpInst.mode = pEditable ? 'edit' : 'view';
|
|
324
|
+
tmpInst.slotID = tmpSlotID;
|
|
325
|
+
tmpInst.viewInstance = tmpView;
|
|
326
|
+
tmpInst.viewHash = tmpViewHash;
|
|
327
|
+
tmpInst.input = pInput;
|
|
328
|
+
this._instances[pInput.Hash] = tmpInst;
|
|
329
|
+
|
|
330
|
+
try
|
|
331
|
+
{
|
|
332
|
+
let tmpResult = tmpView.render();
|
|
333
|
+
if (tmpResult && typeof tmpResult.then === 'function')
|
|
334
|
+
{
|
|
335
|
+
tmpResult.then(
|
|
336
|
+
() => { if (typeof fCallback === 'function') fCallback(null); },
|
|
337
|
+
(pErr) => { if (typeof fCallback === 'function') fCallback(pErr); }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
else if (typeof fCallback === 'function')
|
|
341
|
+
{
|
|
342
|
+
fCallback(null);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (pErr)
|
|
346
|
+
{
|
|
347
|
+
if (this.log) this.log.error('[Pict-Input-ObjectEditor] render threw', { error: pErr.message });
|
|
348
|
+
if (typeof fCallback === 'function') fCallback(pErr);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ----------------------------------------------------------------------------
|
|
353
|
+
// Public runtime API
|
|
354
|
+
// ----------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
getMode(pInputHash)
|
|
357
|
+
{
|
|
358
|
+
let tmpInst = this._instances[pInputHash];
|
|
359
|
+
return tmpInst ? tmpInst.mode : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setMode(pInputHash, pMode, fCallback)
|
|
363
|
+
{
|
|
364
|
+
let tmpInst = this._instances[pInputHash];
|
|
365
|
+
if (!tmpInst)
|
|
366
|
+
{
|
|
367
|
+
let tmpErr = new Error('Cannot setMode — input is not mounted: ' + pInputHash);
|
|
368
|
+
if (typeof fCallback === 'function') fCallback(tmpErr);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (pMode !== 'edit' && pMode !== 'view')
|
|
372
|
+
{
|
|
373
|
+
let tmpErr = new Error('setMode: unknown mode "' + pMode + '" (use "edit" or "view")');
|
|
374
|
+
if (typeof fCallback === 'function') fCallback(tmpErr);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
this._mount(tmpInst.input, this._currentObject(pInputHash), (pMode === 'edit'), fCallback);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
toggleMode(pInputHash, fCallback)
|
|
381
|
+
{
|
|
382
|
+
let tmpMode = this.getMode(pInputHash);
|
|
383
|
+
let tmpNext = (tmpMode === 'edit') ? 'view' : 'edit';
|
|
384
|
+
this.setMode(pInputHash, tmpNext, fCallback);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Flush the current edited object to the input's hidden field.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} pInputHash - The input Hash.
|
|
391
|
+
* @param {Function} [fCallback] - Optional completion callback.
|
|
392
|
+
*/
|
|
393
|
+
commit(pInputHash, fCallback)
|
|
394
|
+
{
|
|
395
|
+
let tmpInst = this._instances[pInputHash];
|
|
396
|
+
if (!tmpInst)
|
|
397
|
+
{
|
|
398
|
+
if (typeof fCallback === 'function') fCallback(null);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this._writeHiddenInputValue(tmpInst.input.Macro.RawHTMLID, this._currentObject(pInputHash), true);
|
|
402
|
+
if (typeof fCallback === 'function') fCallback(null);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ----------------------------------------------------------------------------
|
|
406
|
+
// Lifecycle hooks
|
|
407
|
+
// ----------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
onInputInitialize(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID)
|
|
410
|
+
{
|
|
411
|
+
this._mount(pInput, pValue, this._resolveEditable(pInput));
|
|
412
|
+
return super.onInputInitialize(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The ObjectEditor InputType is not supported inside Tabular rows.
|
|
417
|
+
*
|
|
418
|
+
* @param {Object} pView - The view object.
|
|
419
|
+
* @param {Object} pGroup - The group definition object.
|
|
420
|
+
* @param {Object} pInput - The input object.
|
|
421
|
+
* @param {any} pValue - The value of the input object.
|
|
422
|
+
* @param {string} pHTMLSelector - The HTML selector for the input object.
|
|
423
|
+
* @param {number} pRowIndex - The row index of the tabular data.
|
|
424
|
+
* @param {string} pTransactionGUID - The transaction GUID for the event dispatch.
|
|
425
|
+
* @return {boolean}
|
|
426
|
+
*/
|
|
427
|
+
onInputInitializeTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID)
|
|
428
|
+
{
|
|
429
|
+
super.onInputInitializeTabular(pView, pGroup, pInput, pValue, pHTMLSelector, pRowIndex, pTransactionGUID);
|
|
430
|
+
let tmpErr = new Error('ObjectEditor InputType is not supported inside Tabular rows.');
|
|
431
|
+
if (this.log) this.log.warn('[Pict-Input-ObjectEditor] tabular not supported', { inputHash: pInput && pInput.Hash });
|
|
432
|
+
throw tmpErr;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Fires when data is marshaled to the form for this input — re-seed the
|
|
437
|
+
* stash with the fresh value and re-paint, preserving the current mode.
|
|
438
|
+
*
|
|
439
|
+
* @param {Object} pView - The view object.
|
|
440
|
+
* @param {Object} pGroup - The group definition object.
|
|
441
|
+
* @param {number} pRow - The Row index.
|
|
442
|
+
* @param {Object} pInput - The input object.
|
|
443
|
+
* @param {any} pValue - The value to marshal.
|
|
444
|
+
* @param {string} pHTMLSelector - The HTML selector.
|
|
445
|
+
* @param {string} pTransactionGUID - The transaction GUID for the event dispatch.
|
|
446
|
+
* @returns {boolean}
|
|
447
|
+
*/
|
|
448
|
+
onDataMarshalToForm(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID)
|
|
449
|
+
{
|
|
450
|
+
let tmpInst = this._instances[pInput.Hash];
|
|
451
|
+
let tmpEditable = tmpInst ? (tmpInst.mode === 'edit') : this._resolveEditable(pInput);
|
|
452
|
+
this._mount(pInput, pValue, tmpEditable);
|
|
453
|
+
return super.onDataMarshalToForm(pView, pGroup, pRow, pInput, pValue, pHTMLSelector, pTransactionGUID);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Fires when the form gathers this input's value (the save direction).
|
|
458
|
+
* Serialize the (in-place-mutated) stash into the hidden field so the form
|
|
459
|
+
* marshals the latest object. No change dispatch — this is a read.
|
|
460
|
+
*
|
|
461
|
+
* @param {Object} pView - The view object.
|
|
462
|
+
* @param {Object} pInput - The input object.
|
|
463
|
+
* @param {any} pValue - The value from AppData.
|
|
464
|
+
* @param {string} pHTMLSelector - The HTML selector.
|
|
465
|
+
* @returns {boolean}
|
|
466
|
+
*/
|
|
467
|
+
onDataRequest(pView, pInput, pValue, pHTMLSelector)
|
|
468
|
+
{
|
|
469
|
+
if (this._instances[pInput.Hash])
|
|
470
|
+
{
|
|
471
|
+
this._writeHiddenInputValue(pInput.Macro.RawHTMLID, this._currentObject(pInput.Hash), false);
|
|
472
|
+
}
|
|
473
|
+
return super.onDataRequest(pView, pInput, pValue, pHTMLSelector);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = PictInputObjectEditor;
|
|
478
|
+
module.exports.default_configuration = _DefaultProviderConfiguration;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Regression guard for the Informary "assign straight to the element" marshal optimization
|
|
3
|
+
(marshalSpecificElementDataToForm assigns to pFormElement directly instead of re-resolving a
|
|
4
|
+
selector built from that element's own data-i-* attributes — a per-cell full-document scan).
|
|
5
|
+
|
|
6
|
+
That optimization is only correct because the selector getContentBrowserAddress() builds is
|
|
7
|
+
UNIQUE — it resolves to exactly the one element being marshalled. This test renders a real form
|
|
8
|
+
(non-tabular fields + a multi-row tabular group) into a jsdom DOM and asserts, for EVERY
|
|
9
|
+
datum-bound cell, that the actual getContentBrowserAddress() output resolves to exactly that
|
|
10
|
+
element. If a future template change makes the (form, datum, container, index) tuple non-unique,
|
|
11
|
+
the marshal would write to the wrong/extra element — and this test fails.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const libBrowserEnv = require('browser-env');
|
|
15
|
+
libBrowserEnv();
|
|
16
|
+
|
|
17
|
+
const Chai = require('chai');
|
|
18
|
+
const Expect = Chai.expect;
|
|
19
|
+
|
|
20
|
+
const libPict = require('pict');
|
|
21
|
+
const libPictSectionForm = require('../source/Pict-Section-Form.js');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {(pict: libPict) => void} fOnReady
|
|
25
|
+
*/
|
|
26
|
+
function buildRenderedForm(fOnReady)
|
|
27
|
+
{
|
|
28
|
+
const tmpManifest =
|
|
29
|
+
{
|
|
30
|
+
Scope: 'InformarySelectorForm',
|
|
31
|
+
Descriptors:
|
|
32
|
+
{
|
|
33
|
+
Title: { Name: 'Title', Hash: 'Title', DataType: 'String', PictForm: { Section: 'Sheet', Group: 'Meta', InputType: 'Text' } },
|
|
34
|
+
Teacher: { Name: 'Teacher', Hash: 'Teacher', DataType: 'String', PictForm: { Section: 'Sheet', Group: 'Meta', InputType: 'Text' } }
|
|
35
|
+
},
|
|
36
|
+
Sections:
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
Hash: 'Sheet', Name: 'Sheet', Groups:
|
|
40
|
+
[
|
|
41
|
+
{ Hash: 'Meta', Name: 'Meta' },
|
|
42
|
+
{ Hash: 'Grades', Name: 'Grades', Layout: 'Tabular', RecordSetAddress: 'Grades', RecordManifest: 'GradeRow' }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
ReferenceManifests:
|
|
47
|
+
{
|
|
48
|
+
GradeRow:
|
|
49
|
+
{
|
|
50
|
+
Scope: 'GradeRow',
|
|
51
|
+
Descriptors:
|
|
52
|
+
{
|
|
53
|
+
StudentName: { Name: 'Student', Hash: 'StudentName', DataType: 'String', PictForm: { Section: 'Sheet', Group: 'Grades', InputType: 'Text' } },
|
|
54
|
+
Score: { Name: 'Score', Hash: 'Score', DataType: 'Number', PictForm: { Section: 'Sheet', Group: 'Grades', InputType: 'Text' } }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const tmpAppData =
|
|
60
|
+
{
|
|
61
|
+
Title: 'Term 1', Teacher: 'Ms. Frizzle',
|
|
62
|
+
Grades:
|
|
63
|
+
[
|
|
64
|
+
{ StudentName: 'Alice', Score: 95 },
|
|
65
|
+
{ StudentName: 'Bob', Score: 80 },
|
|
66
|
+
{ StudentName: 'Carol', Score: 60 },
|
|
67
|
+
{ StudentName: 'Dan', Score: 72 }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
class TestApp extends libPictSectionForm.PictFormApplication
|
|
72
|
+
{
|
|
73
|
+
onAfterInitialize() { this.solve(); return super.onAfterInitialize(); }
|
|
74
|
+
}
|
|
75
|
+
TestApp.default_configuration = JSON.parse(JSON.stringify(libPictSectionForm.PictFormApplication.default_configuration));
|
|
76
|
+
TestApp.default_configuration.pict_configuration =
|
|
77
|
+
{
|
|
78
|
+
Product: 'InformarySelectorTest',
|
|
79
|
+
DefaultAppData: tmpAppData,
|
|
80
|
+
DefaultFormManifest: tmpManifest
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.body.innerHTML = '<div id="Pict-Form-Container"></div>';
|
|
84
|
+
const _Pict = new libPict(TestApp.default_configuration.pict_configuration);
|
|
85
|
+
_Pict.LogNoisiness = 0;
|
|
86
|
+
_Pict.addApplication('InformarySelectorTest', TestApp.default_configuration, TestApp);
|
|
87
|
+
_Pict.PictApplication.initializeAsync(() =>
|
|
88
|
+
{
|
|
89
|
+
// browser-env doesn't drive the render cycle on init; render the form views explicitly so
|
|
90
|
+
// the data-i-* cells land in the DOM.
|
|
91
|
+
Object.keys(_Pict.views).forEach((pHash) =>
|
|
92
|
+
{
|
|
93
|
+
const tmpView = _Pict.views[pHash];
|
|
94
|
+
if (tmpView && tmpView.isPictSectionForm)
|
|
95
|
+
{
|
|
96
|
+
try { tmpView.render(); }
|
|
97
|
+
catch (pError) { /* a view that can't render in jsdom is fine; we assert on what did */ }
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
fOnReady(_Pict);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
suite('Pict Provider Informary — render-time selector uniqueness', () =>
|
|
105
|
+
{
|
|
106
|
+
test('getContentBrowserAddress resolves to EXACTLY its own element for every datum cell', (fDone) =>
|
|
107
|
+
{
|
|
108
|
+
buildRenderedForm((_Pict) =>
|
|
109
|
+
{
|
|
110
|
+
try
|
|
111
|
+
{
|
|
112
|
+
const tmpInformary = _Pict.providers.Informary;
|
|
113
|
+
const tmpElements = document.querySelectorAll('[data-i-form][data-i-datum]');
|
|
114
|
+
// Guard against a vacuous pass (render produced no cells).
|
|
115
|
+
Expect(tmpElements.length).to.be.greaterThan(6, 'the form rendered its datum cells to the DOM');
|
|
116
|
+
|
|
117
|
+
let tmpTabularChecked = 0;
|
|
118
|
+
let tmpNonTabularChecked = 0;
|
|
119
|
+
tmpElements.forEach((pElement) =>
|
|
120
|
+
{
|
|
121
|
+
const tmpForm = pElement.getAttribute('data-i-form');
|
|
122
|
+
const tmpDatum = pElement.getAttribute('data-i-datum');
|
|
123
|
+
const tmpContainer = pElement.getAttribute('data-i-container');
|
|
124
|
+
const tmpIndexRaw = pElement.getAttribute('data-i-index');
|
|
125
|
+
// Mirror the marshal: tmpIndex = Number(getAttribute('data-i-index')).
|
|
126
|
+
const tmpIndex = (tmpIndexRaw == null) ? tmpIndexRaw : Number(tmpIndexRaw);
|
|
127
|
+
if (tmpContainer) { tmpTabularChecked++; } else { tmpNonTabularChecked++; }
|
|
128
|
+
|
|
129
|
+
// The ACTUAL selector the old marshal re-resolved, from the element's own attributes.
|
|
130
|
+
const tmpSelector = tmpInformary.getContentBrowserAddress(tmpForm, tmpDatum, tmpContainer, tmpIndex);
|
|
131
|
+
const tmpMatches = document.querySelectorAll(tmpSelector);
|
|
132
|
+
Expect(tmpMatches.length).to.equal(1, `selector [${tmpSelector}] must match exactly one element`);
|
|
133
|
+
Expect(tmpMatches[0]).to.equal(pElement, `selector [${tmpSelector}] must resolve to its own element`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Make sure we exercised BOTH selector shapes — the 4-part container/index (tabular)
|
|
137
|
+
// AND the 2-part (non-tabular) path through getContentBrowserAddress.
|
|
138
|
+
Expect(tmpTabularChecked).to.be.greaterThan(0, 'exercised tabular container/index cells');
|
|
139
|
+
Expect(tmpNonTabularChecked).to.be.greaterThan(0, 'exercised non-tabular cells');
|
|
140
|
+
fDone();
|
|
141
|
+
}
|
|
142
|
+
catch (pError) { fDone(pError); }
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|