pict-section-form 1.2.4 → 1.3.1
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/package.json +3 -2
- package/source/providers/Pict-Provider-DynamicSolver.js +2 -0
- package/source/providers/Pict-Provider-DynamicTabularData.js +60 -14
- 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/source/providers/layouts/Pict-Layout-Tabular.js +39 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-form",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Pict dynamic form sections",
|
|
5
5
|
"main": "source/Pict-Section-Form.js",
|
|
6
6
|
"directories": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"browser-env": "^3.3.0",
|
|
39
39
|
"eslint": "^9.39.2",
|
|
40
40
|
"jquery": "^4.0.0",
|
|
41
|
-
"pict": "^1.0.
|
|
41
|
+
"pict": "^1.0.387",
|
|
42
42
|
"pict-application": "^1.0.34",
|
|
43
43
|
"pict-docuserve": "^1.4.19",
|
|
44
44
|
"pict-service-commandlineutility": "^1.0.20",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"pict-provider": "^1.0.13",
|
|
54
54
|
"pict-section-excalidraw": "^1.0.3",
|
|
55
55
|
"pict-section-markdowneditor": "^1.0.19",
|
|
56
|
+
"pict-section-objecteditor": "^1.0.3",
|
|
56
57
|
"pict-section-tuigrid": "^1.0.31",
|
|
57
58
|
"pict-template": "^1.0.15",
|
|
58
59
|
"pict-view": "^1.0.68"
|
|
@@ -19,6 +19,7 @@ const libInputLink = require('./inputs/Pict-Provider-Input-Link.js');
|
|
|
19
19
|
const libInputTemplatedEntityLookup = require('./inputs/Pict-Provider-Input-TemplatedEntityLookup.js');
|
|
20
20
|
const libInputChart = require('./inputs/Pict-Provider-Input-Chart.js');
|
|
21
21
|
const libInputTabularTriggerGroup = require('./inputs/Pict-Provider-Input-TabularTriggerGroup.js');
|
|
22
|
+
const libInputObjectEditor = require('./inputs/Pict-Provider-Input-ObjectEditor.js');
|
|
22
23
|
|
|
23
24
|
/** @type {Record<string, any>} */
|
|
24
25
|
const _DefaultProviderConfiguration = (
|
|
@@ -82,6 +83,7 @@ class PictDynamicSolver extends libPictProvider
|
|
|
82
83
|
this.pict.addProviderSingleton('Pict-Input-Link', libInputLink.default_configuration, libInputLink);
|
|
83
84
|
this.pict.addProviderSingleton('Pict-Input-Chart', libInputChart.default_configuration, libInputChart);
|
|
84
85
|
this.pict.addProviderSingleton('Pict-Input-TabularTriggerGroup', libInputTabularTriggerGroup.default_configuration, libInputTabularTriggerGroup);
|
|
86
|
+
this.pict.addProviderSingleton('Pict-Input-ObjectEditor', libInputObjectEditor.default_configuration, libInputObjectEditor);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
logSolveOutcome(pSolveOutcome)
|
|
@@ -234,11 +234,9 @@ class DynamicTabularData extends libPictProvider
|
|
|
234
234
|
return false;
|
|
235
235
|
}
|
|
236
236
|
)
|
|
237
|
-
// We expect this view to be in the set.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
tmpViewsToRender[i].render();
|
|
241
|
-
}
|
|
237
|
+
// We expect this view to be in the set. Re-rendering an entity-selector
|
|
238
|
+
// table clobbers its selected values in the model, so preserve them.
|
|
239
|
+
this._renderViewsPreservingRecordSet(tmpGroup.RecordSetAddress, tmpViewsToRender);
|
|
242
240
|
|
|
243
241
|
// Rebuild any OTHER views whose DynamicColumns are sourced from this record set
|
|
244
242
|
// (e.g. a "% Passing" table whose columns are generated from this "Products" table)
|
|
@@ -494,11 +492,9 @@ class DynamicTabularData extends libPictProvider
|
|
|
494
492
|
return false;
|
|
495
493
|
}
|
|
496
494
|
)
|
|
497
|
-
// We expect this view to be in the set.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
tmpViewsToRender[i].render();
|
|
501
|
-
}
|
|
495
|
+
// We expect this view to be in the set. Re-rendering an entity-selector
|
|
496
|
+
// table clobbers its selected values in the model, so preserve them.
|
|
497
|
+
this._renderViewsPreservingRecordSet(tmpGroup.RecordSetAddress, tmpViewsToRender);
|
|
502
498
|
|
|
503
499
|
// Rebuild any OTHER views whose DynamicColumns are sourced from this record set
|
|
504
500
|
// (e.g. a "% Passing" table whose columns are generated from this "Products" table)
|
|
@@ -518,6 +514,57 @@ class DynamicTabularData extends libPictProvider
|
|
|
518
514
|
}
|
|
519
515
|
}
|
|
520
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Render every view in pViewsToRender while preserving pRecordSetAddress's data in
|
|
519
|
+
* the model. Re-rendering a table that contains entity-selector cells (Material /
|
|
520
|
+
* Organization / Product / etc.) destroys and recreates their select2 widgets; the
|
|
521
|
+
* transient empty widget gets read back into the model and wipes the selected value
|
|
522
|
+
* AND its resolved __Record for EVERY row -- the "selected Source / Name disappears
|
|
523
|
+
* after adding or deleting a row" data-loss bug. Because it clobbers the saved
|
|
524
|
+
* FormData, the value is then also missing from the exported PDF. A render() must be
|
|
525
|
+
* model-non-destructive, so snapshot the record set before rendering and restore any
|
|
526
|
+
* cleared fields afterward. No-op for plain (non-entity) tables, where render never
|
|
527
|
+
* changes the model (snapshot == post-render state, restore is a no-op).
|
|
528
|
+
*
|
|
529
|
+
* @param {string} pRecordSetAddress - RecordSetAddress of the table being re-rendered.
|
|
530
|
+
* @param {Array<Object>} pViewsToRender - Views bound to that record set to render.
|
|
531
|
+
*/
|
|
532
|
+
_renderViewsPreservingRecordSet(pRecordSetAddress, pViewsToRender)
|
|
533
|
+
{
|
|
534
|
+
if (!Array.isArray(pViewsToRender) || (pViewsToRender.length < 1))
|
|
535
|
+
{
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
// Snapshot the record set (deep clone each row) from a view that owns it.
|
|
539
|
+
let tmpRecordSet = false;
|
|
540
|
+
let tmpSnapshot = false;
|
|
541
|
+
for (let i = 0; i < pViewsToRender.length; i++)
|
|
542
|
+
{
|
|
543
|
+
let tmpCandidate = pViewsToRender[i].sectionManifest.getValueByHash(pViewsToRender[i].getMarshalDestinationObject(), pRecordSetAddress);
|
|
544
|
+
if (Array.isArray(tmpCandidate))
|
|
545
|
+
{
|
|
546
|
+
tmpRecordSet = tmpCandidate;
|
|
547
|
+
tmpSnapshot = tmpCandidate.map((pRow) => (pRow && (typeof pRow === 'object')) ? JSON.parse(JSON.stringify(pRow)) : pRow);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (let i = 0; i < pViewsToRender.length; i++)
|
|
552
|
+
{
|
|
553
|
+
pViewsToRender[i].render();
|
|
554
|
+
}
|
|
555
|
+
// Restore any model fields a destructive re-render cleared (entity value + __Record).
|
|
556
|
+
if (Array.isArray(tmpRecordSet) && Array.isArray(tmpSnapshot))
|
|
557
|
+
{
|
|
558
|
+
for (let i = 0; i < tmpRecordSet.length; i++)
|
|
559
|
+
{
|
|
560
|
+
if (tmpRecordSet[i] && (typeof tmpRecordSet[i] === 'object') && tmpSnapshot[i] && (typeof tmpSnapshot[i] === 'object'))
|
|
561
|
+
{
|
|
562
|
+
Object.assign(tmpRecordSet[i], tmpSnapshot[i]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
521
568
|
/**
|
|
522
569
|
* Rebuild every OTHER section-form view whose DynamicColumns are sourced from
|
|
523
570
|
* pSourceRecordSetAddress: re-resolve their generated columns and rebuild their
|
|
@@ -817,10 +864,9 @@ class DynamicTabularData extends libPictProvider
|
|
|
817
864
|
}
|
|
818
865
|
return false;
|
|
819
866
|
});
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
867
|
+
// Re-rendering an entity-selector table clobbers its selected values in the
|
|
868
|
+
// model, so preserve them across the render.
|
|
869
|
+
this._renderViewsPreservingRecordSet(pGroup.RecordSetAddress, tmpViewsToRender);
|
|
824
870
|
|
|
825
871
|
// Rebuild any OTHER views whose DynamicColumns are sourced from this record set
|
|
826
872
|
// HERE, in the render phase -- BEFORE the marshal below -- so the column DOM is
|
package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates-ReadOnly.js
CHANGED
|
@@ -186,6 +186,16 @@ Glug glug glug Oo... -->
|
|
|
186
186
|
<!-- DataType DateTime {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
187
187
|
<input disabled type="hidden" {~D:Record.Macro.InputFullProperties~} value="">
|
|
188
188
|
<span>{~D:Record.Name~}:</span> <input disabled {~D:Record.Macro.ControlAttr~} id="DATETIME-INPUT-FOR-{~D:Record.Macro.RawHTMLID~}" onchange="{~D:Record.Macro.DataRequestFunction~}" type="datetime-local" value="" />
|
|
189
|
+
`
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"HashPostfix": "-Template-Input-InputType-ObjectEditor",
|
|
193
|
+
"DefaultInputExtensions": ["Pict-Input-ObjectEditor"],
|
|
194
|
+
"Template": /*HTML*/`
|
|
195
|
+
<!-- InputType ObjectEditor {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
196
|
+
<input disabled type="hidden" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="">
|
|
197
|
+
<span>{~D:Record.Name~}:</span>
|
|
198
|
+
<div {~D:Record.Macro.ControlAttr~} id="DISPLAY-FOR-{~D:Record.Macro.RawHTMLID~}" class="pict-section-form-objecteditor pict-section-form-objecteditor-readonly"></div>
|
|
189
199
|
`
|
|
190
200
|
},
|
|
191
201
|
/*
|
|
@@ -324,6 +324,16 @@ Glug glug glug Oo... -->
|
|
|
324
324
|
<input type="hidden" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="">
|
|
325
325
|
<span>{~D:Record.Name~}:</span>
|
|
326
326
|
<div {~D:Record.Macro.ControlAttr~} id="DISPLAY-FOR-{~D:Record.Macro.RawHTMLID~}" class="pict-section-form-diagram"></div>
|
|
327
|
+
`
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"HashPostfix": "-Template-Input-InputType-ObjectEditor",
|
|
331
|
+
"DefaultInputExtensions": ["Pict-Input-ObjectEditor"],
|
|
332
|
+
"Template": /*HTML*/`
|
|
333
|
+
<!-- InputType ObjectEditor {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
334
|
+
<input type="hidden" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="">
|
|
335
|
+
<span>{~D:Record.Name~}:</span>
|
|
336
|
+
<div {~D:Record.Macro.ControlAttr~} id="DISPLAY-FOR-{~D:Record.Macro.RawHTMLID~}" class="pict-section-form-objecteditor"></div>
|
|
327
337
|
`
|
|
328
338
|
},
|
|
329
339
|
/*
|
|
@@ -555,6 +565,19 @@ Glug glug glug Oo... -->
|
|
|
555
565
|
<span>{~D:Record.PictForm.ExtraDescription~}</span>
|
|
556
566
|
<div {~D:Record.Macro.ControlAttr~} id="DISPLAY-FOR-{~D:Record.Macro.RawHTMLID~}" class="pict-section-form-diagram"></div>
|
|
557
567
|
</div>
|
|
568
|
+
`
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
"HashPostfix": "-VerticalTemplate-Input-InputType-ObjectEditor",
|
|
572
|
+
"DefaultInputExtensions": ["Pict-Input-ObjectEditor"],
|
|
573
|
+
"Template": /*HTML*/`
|
|
574
|
+
<!-- InputType ObjectEditor {~D:Record.Hash~} {~D:Record.DataType~} -->
|
|
575
|
+
<div class="pict-form-vertical-input">
|
|
576
|
+
<input type="hidden" {~D:Record.Macro.InputFullProperties~} {~D:Record.Macro.InputChangeHandler~} value="">
|
|
577
|
+
<label class="pict-form-label">{~D:Record.Name~}</label>
|
|
578
|
+
<span>{~D:Record.PictForm.ExtraDescription~}</span>
|
|
579
|
+
<div {~D:Record.Macro.ControlAttr~} id="DISPLAY-FOR-{~D:Record.Macro.RawHTMLID~}" class="pict-section-form-objecteditor"></div>
|
|
580
|
+
</div>
|
|
558
581
|
`
|
|
559
582
|
},
|
|
560
583
|
/*
|
|
@@ -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;
|
|
@@ -1423,7 +1423,12 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1423
1423
|
}
|
|
1424
1424
|
if (tmpEditingRight)
|
|
1425
1425
|
{
|
|
1426
|
-
|
|
1426
|
+
// Parity cell for the right-side editing-controls column. Tag it with the same
|
|
1427
|
+
// pict-tabular-editing-controls-header class as the left-controls header (below)
|
|
1428
|
+
// so host CSS sizes it like the controls column AND collapses it in lockstep when
|
|
1429
|
+
// that column is hidden -- otherwise it lingers as an empty phantom column and the
|
|
1430
|
+
// stacked-header rule's bottom border overshoots the data columns.
|
|
1431
|
+
tmpTemplate += `<th class="pict-row-label-spacer pict-tabular-editing-controls-header"></th>`;
|
|
1427
1432
|
}
|
|
1428
1433
|
tmpTemplate += tmpMetatemplateGenerator.getMetatemplateTemplateReference(pView, `-TabularTemplate-ExtraHeaderRow-Postfix`, `getGroup("${pGroup.GroupIndex}")`);
|
|
1429
1434
|
}
|
|
@@ -1456,7 +1461,9 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1456
1461
|
}
|
|
1457
1462
|
if (tmpEditingRight)
|
|
1458
1463
|
{
|
|
1459
|
-
|
|
1464
|
+
// Parity cell for the right-side editing-controls column (mirrors the extra-header
|
|
1465
|
+
// row above) -- collapses with the column via pict-tabular-editing-controls-header.
|
|
1466
|
+
tmpColumnSelectRow += `<th class="pict-row-label-spacer pict-tabular-editing-controls-header"></th>`;
|
|
1460
1467
|
}
|
|
1461
1468
|
tmpColumnSelectRow += `</tr>`;
|
|
1462
1469
|
tmpTemplate += tmpColumnSelectRow;
|
|
@@ -1647,6 +1654,8 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1647
1654
|
{
|
|
1648
1655
|
pView.rebuildCustomTemplate();
|
|
1649
1656
|
pView.render();
|
|
1657
|
+
// Refill the freshly rebuilt table DOM in this same marshal cycle.
|
|
1658
|
+
this._refillAfterDynamicColumnRebuild(pView);
|
|
1650
1659
|
}
|
|
1651
1660
|
finally
|
|
1652
1661
|
{
|
|
@@ -1666,6 +1675,8 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1666
1675
|
try
|
|
1667
1676
|
{
|
|
1668
1677
|
pView.render();
|
|
1678
|
+
// Same-cycle refill: a label-only re-render still repaints the cell DOM.
|
|
1679
|
+
this._refillAfterDynamicColumnRebuild(pView);
|
|
1669
1680
|
}
|
|
1670
1681
|
finally
|
|
1671
1682
|
{
|
|
@@ -1691,6 +1702,8 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1691
1702
|
{
|
|
1692
1703
|
pView.rebuildCustomTemplate();
|
|
1693
1704
|
pView.render();
|
|
1705
|
+
// Refill the freshly rebuilt table DOM in this same marshal cycle.
|
|
1706
|
+
this._refillAfterDynamicColumnRebuild(pView);
|
|
1694
1707
|
}
|
|
1695
1708
|
finally
|
|
1696
1709
|
{
|
|
@@ -1706,6 +1719,30 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1706
1719
|
this._reapplyTabularSelectionHighlights(pView, pGroup);
|
|
1707
1720
|
return true;
|
|
1708
1721
|
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* After a mid-marshal DynamicColumns rebuild/re-render, the freshly painted table DOM
|
|
1725
|
+
* is empty: the enclosing onMarshalToView already ran its marshalDataToForm BEFORE this
|
|
1726
|
+
* hook re-rendered, so the dynamic cells lost the values that pass had just written and
|
|
1727
|
+
* would only refill on the NEXT marshal -- the "% Passing table blanks on row add/delete
|
|
1728
|
+
* or Source/Name select until another action brings it back" bug. Re-marshal the view's
|
|
1729
|
+
* data into the new DOM here so the cells repopulate within the SAME cycle. This is safe
|
|
1730
|
+
* to call from inside onDataMarshalToForm: marshalDataToForm only writes model values into
|
|
1731
|
+
* the inputs and does NOT itself invoke onDataMarshalToForm, so it cannot recurse here.
|
|
1732
|
+
*
|
|
1733
|
+
* @param {Object} pView
|
|
1734
|
+
*/
|
|
1735
|
+
_refillAfterDynamicColumnRebuild(pView)
|
|
1736
|
+
{
|
|
1737
|
+
try
|
|
1738
|
+
{
|
|
1739
|
+
this.pict.providers.Informary.marshalDataToForm(pView.getMarshalDestinationObject(), pView.formID, pView.sectionManifest);
|
|
1740
|
+
}
|
|
1741
|
+
catch (pError)
|
|
1742
|
+
{
|
|
1743
|
+
this.log.error(`Error refilling tabular dynamic-column cells after rebuild: ${pError}`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1709
1746
|
}
|
|
1710
1747
|
|
|
1711
1748
|
module.exports = TabularLayout;
|