pict-section-formeditor 1.0.5 → 1.0.6
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/example_applications/form_editor/FormEditor-Example-Application.js +4 -0
- package/package.json +3 -3
- package/source/Pict-Section-FormEditor-DefaultConfiguration.js +60 -0
- package/source/providers/Pict-Provider-FormEditorIconography.js +9 -0
- package/source/providers/Pict-Provider-FormEditorRendering.js +19 -0
- package/source/views/PictView-FormEditor-InlineEditing.js +7 -3
- package/source/views/PictView-FormEditor-PropertiesPanel.js +192 -0
- package/source/views/PictView-FormEditor.js +191 -1
- package/test/Pict-Section-FormEditor_tests.js +131 -1
- package/example_applications/form_editor/html/form_editor_example.js +0 -20981
|
@@ -47,6 +47,10 @@ class FormEditorExampleApplication extends libPictApplication
|
|
|
47
47
|
ManifestDataAddress: 'AppData.FormConfig',
|
|
48
48
|
DefaultDestinationAddress: '#FormEditor-Container',
|
|
49
49
|
ActiveTab: 'visual',
|
|
50
|
+
ExtendedDescriptorProperties:
|
|
51
|
+
[
|
|
52
|
+
{ Name: 'Units', Address: 'PictForm.Units', DataType: 'String', Description: 'Unit of measure (e.g. kg, lbs, meters)' }
|
|
53
|
+
],
|
|
50
54
|
Renderables:
|
|
51
55
|
[
|
|
52
56
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-formeditor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Pict visual editor for pict-section-form configurations",
|
|
5
5
|
"main": "source/Pict-Section-FormEditor.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"pict-section-code": "^1.0.2",
|
|
28
28
|
"pict-section-content": "^0.0.6",
|
|
29
|
-
"pict-section-form": "^1.0.
|
|
29
|
+
"pict-section-form": "^1.0.190",
|
|
30
30
|
"pict-section-markdowneditor": "^1.0.0",
|
|
31
31
|
"pict-section-objecteditor": "^1.0.0",
|
|
32
32
|
"pict-view": "^1.0.66"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"pict": "^1.0.
|
|
35
|
+
"pict": "^1.0.351",
|
|
36
36
|
"quackage": "^1.0.56"
|
|
37
37
|
},
|
|
38
38
|
"mocha": {
|
|
@@ -13,6 +13,24 @@ module.exports = (
|
|
|
13
13
|
// Which tab is active by default: 'visual', 'objecteditor', 'json'
|
|
14
14
|
ActiveTab: 'visual',
|
|
15
15
|
|
|
16
|
+
// Extended descriptor properties to display in the Input properties panel.
|
|
17
|
+
// Each entry defines a custom field that maps to a dot-notation address
|
|
18
|
+
// within the Descriptor object (e.g. 'PictForm.Units' for Descriptor.PictForm.Units).
|
|
19
|
+
//
|
|
20
|
+
// Example:
|
|
21
|
+
// [
|
|
22
|
+
// { Name: 'Units', Address: 'PictForm.Units', DataType: 'String' },
|
|
23
|
+
// { Name: 'Extra Data', Address: 'ExtraData', DataType: 'String' },
|
|
24
|
+
// { Name: 'Entity', Address: 'PictForm.Configuration.Entity', DataType: 'String' }
|
|
25
|
+
// ]
|
|
26
|
+
//
|
|
27
|
+
// Each entry supports:
|
|
28
|
+
// Name - Display label in the properties panel
|
|
29
|
+
// Address - Dot-notation path relative to the Descriptor (required)
|
|
30
|
+
// DataType - 'String' (default), 'Number', or 'Boolean'
|
|
31
|
+
// Description - Optional tooltip / placeholder text
|
|
32
|
+
ExtendedDescriptorProperties: [],
|
|
33
|
+
|
|
16
34
|
CSS: /*css*/`
|
|
17
35
|
.pict-formeditor
|
|
18
36
|
{
|
|
@@ -3397,6 +3415,48 @@ module.exports = (
|
|
|
3397
3415
|
line-height: 1.5;
|
|
3398
3416
|
}
|
|
3399
3417
|
|
|
3418
|
+
/* ---- Export Buttons ---- */
|
|
3419
|
+
.pict-fe-export-buttons
|
|
3420
|
+
{
|
|
3421
|
+
display: flex;
|
|
3422
|
+
gap: 10px;
|
|
3423
|
+
flex-wrap: wrap;
|
|
3424
|
+
}
|
|
3425
|
+
.pict-fe-export-btn
|
|
3426
|
+
{
|
|
3427
|
+
display: inline-flex;
|
|
3428
|
+
align-items: center;
|
|
3429
|
+
gap: 6px;
|
|
3430
|
+
padding: 8px 16px;
|
|
3431
|
+
border: 1px solid #D4C4A8;
|
|
3432
|
+
border-radius: 6px;
|
|
3433
|
+
background: #FDFCFA;
|
|
3434
|
+
color: #3D3229;
|
|
3435
|
+
font-size: 13px;
|
|
3436
|
+
font-weight: 600;
|
|
3437
|
+
cursor: pointer;
|
|
3438
|
+
transition: border-color 0.15s, background 0.15s;
|
|
3439
|
+
}
|
|
3440
|
+
.pict-fe-export-btn:hover
|
|
3441
|
+
{
|
|
3442
|
+
border-color: #9E6B47;
|
|
3443
|
+
background: #FAF5EE;
|
|
3444
|
+
}
|
|
3445
|
+
.pict-fe-export-btn:active
|
|
3446
|
+
{
|
|
3447
|
+
background: #F3EAE0;
|
|
3448
|
+
}
|
|
3449
|
+
.pict-fe-export-btn svg
|
|
3450
|
+
{
|
|
3451
|
+
flex-shrink: 0;
|
|
3452
|
+
}
|
|
3453
|
+
.pict-fe-import-export-divider
|
|
3454
|
+
{
|
|
3455
|
+
border: none;
|
|
3456
|
+
border-top: 1px solid #E8E0D4;
|
|
3457
|
+
margin: 4px 0;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3400
3460
|
/* ---- Toast Notifications ---- */
|
|
3401
3461
|
.pict-fe-toast-container
|
|
3402
3462
|
{
|
|
@@ -729,6 +729,15 @@ class PictProviderFormEditorIconography
|
|
|
729
729
|
'<circle cx="9" cy="18" r="' + tmpR + '" fill="currentColor"/>' +
|
|
730
730
|
'<circle cx="15" cy="18" r="' + tmpR + '" fill="currentColor"/>');
|
|
731
731
|
};
|
|
732
|
+
|
|
733
|
+
// Download — downward arrow into a tray (uses currentColor)
|
|
734
|
+
this._Icons.Action['Download'] = function(pSize, pColors, pSW)
|
|
735
|
+
{
|
|
736
|
+
return _svg(pSize,
|
|
737
|
+
'<path d="M12 3 L12 15" stroke="currentColor" stroke-width="' + pSW + '"/>' +
|
|
738
|
+
'<path d="M8 11 L12 15 L16 11" stroke="currentColor" stroke-width="' + pSW + '" fill="none"/>' +
|
|
739
|
+
'<path d="M4 17 L4 20 L20 20 L20 17" stroke="currentColor" stroke-width="' + pSW + '" fill="none"/>');
|
|
740
|
+
};
|
|
732
741
|
}
|
|
733
742
|
|
|
734
743
|
/* ======================================================================== */
|
|
@@ -605,6 +605,25 @@ class FormEditorRendering extends libPictProvider
|
|
|
605
605
|
let tmpHTML = '';
|
|
606
606
|
|
|
607
607
|
tmpHTML += '<div class="pict-fe-import-container">';
|
|
608
|
+
|
|
609
|
+
// Export section
|
|
610
|
+
tmpHTML += '<h3 class="pict-fe-import-title">Export Form Configuration</h3>';
|
|
611
|
+
tmpHTML += '<p class="pict-fe-import-description">Download the current form configuration as a JSON manifest or CSV spreadsheet.</p>';
|
|
612
|
+
tmpHTML += '<div class="pict-fe-export-buttons">';
|
|
613
|
+
tmpHTML += `<button class="pict-fe-export-btn" onclick="${tmpViewRef}.exportJSON()">`;
|
|
614
|
+
tmpHTML += `${tmpParent._IconographyProvider.getIcon('Action', 'Download', 18)}`;
|
|
615
|
+
tmpHTML += '<span>Export JSON</span>';
|
|
616
|
+
tmpHTML += '</button>';
|
|
617
|
+
tmpHTML += `<button class="pict-fe-export-btn" onclick="${tmpViewRef}.exportCSV()">`;
|
|
618
|
+
tmpHTML += `${tmpParent._IconographyProvider.getIcon('Action', 'Download', 18)}`;
|
|
619
|
+
tmpHTML += '<span>Export CSV</span>';
|
|
620
|
+
tmpHTML += '</button>';
|
|
621
|
+
tmpHTML += '</div>';
|
|
622
|
+
|
|
623
|
+
// Divider
|
|
624
|
+
tmpHTML += '<hr class="pict-fe-import-export-divider" />';
|
|
625
|
+
|
|
626
|
+
// Import section
|
|
608
627
|
tmpHTML += '<h3 class="pict-fe-import-title">Import Form Configuration</h3>';
|
|
609
628
|
tmpHTML += '<p class="pict-fe-import-description">Drop a CSV or JSON file below to load a form configuration. CSV files are processed through ManifestFactory. JSON files are loaded directly as manifests. If the file contains multiple forms, the first will be loaded and the rest will be available in the Load Configuration selector.</p>';
|
|
610
629
|
|
|
@@ -47,12 +47,15 @@ class PictViewFormEditorInlineEditing extends libPictView
|
|
|
47
47
|
// Replace the span with an inline editor
|
|
48
48
|
let tmpEditorHTML = '';
|
|
49
49
|
|
|
50
|
+
// Build the commit call referencing the inline editing child view
|
|
51
|
+
let tmpInlineRef = `${tmpViewRef}._InlineEditingView`;
|
|
52
|
+
|
|
50
53
|
if (pProperty === 'Layout')
|
|
51
54
|
{
|
|
52
55
|
// Layout uses a select dropdown
|
|
53
56
|
// onclick stopPropagation prevents the parent span's onclick from re-calling beginEditProperty
|
|
54
57
|
let tmpLayouts = ['Record', 'Tabular', 'RecordSet'];
|
|
55
|
-
let tmpCommitCall = `${
|
|
58
|
+
let tmpCommitCall = `${tmpInlineRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')`;
|
|
56
59
|
tmpEditorHTML += `<select class="pict-fe-inline-edit-select" id="${tmpElementId}-Input" onclick="event.stopPropagation()" onchange="${tmpCommitCall}" onblur="setTimeout(function(){${tmpCommitCall}},150)" onkeydown="if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}">`;
|
|
57
60
|
for (let i = 0; i < tmpLayouts.length; i++)
|
|
58
61
|
{
|
|
@@ -66,7 +69,7 @@ class PictViewFormEditorInlineEditing extends libPictView
|
|
|
66
69
|
// Name and Hash use a text input
|
|
67
70
|
// onclick stopPropagation prevents the parent span's onclick from re-calling beginEditProperty
|
|
68
71
|
let tmpHashClass = (pProperty === 'Hash') ? ' pict-fe-inline-edit-hash' : '';
|
|
69
|
-
tmpEditorHTML += `<input class="pict-fe-inline-edit-input${tmpHashClass}" id="${tmpElementId}-Input" type="text" value="${this._ParentFormEditor._UtilitiesProvider._escapeAttr(tmpCurrentValue)}" onclick="event.stopPropagation()" onblur="${
|
|
72
|
+
tmpEditorHTML += `<input class="pict-fe-inline-edit-input${tmpHashClass}" id="${tmpElementId}-Input" type="text" value="${this._ParentFormEditor._UtilitiesProvider._escapeAttr(tmpCurrentValue)}" onclick="event.stopPropagation()" onblur="${tmpInlineRef}.commitEditProperty('${pType}', ${pSectionIndex}, ${pGroupIndex}, '${pProperty}')" onkeydown="if(event.key==='Enter'){this.blur();}if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}" />`;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
this.pict.ContentAssignment.assignContent(`#${tmpElementId}`, tmpEditorHTML);
|
|
@@ -224,7 +227,8 @@ class PictViewFormEditorInlineEditing extends libPictView
|
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
229
|
|
|
227
|
-
let
|
|
230
|
+
let tmpInlineRef = `${tmpViewRef}._InlineEditingView`;
|
|
231
|
+
let tmpCommitCall = `${tmpInlineRef}.commitEditInputDataType(${pSectionIndex}, ${pGroupIndex}, ${pRowIndex}, ${pInputIndex})`;
|
|
228
232
|
let tmpEditorHTML = `<select class="pict-fe-inline-edit-select" id="${tmpElementId}-Input" onclick="event.stopPropagation()" onchange="${tmpCommitCall}" onblur="setTimeout(function(){${tmpCommitCall}},150)" onkeydown="if(event.key==='Escape'){this.dataset.cancelled='true';this.blur();}">`;
|
|
229
233
|
for (let i = 0; i < this._ParentFormEditor._ManyfestDataTypes.length; i++)
|
|
230
234
|
{
|
|
@@ -3024,6 +3024,9 @@ class PictViewFormEditorPropertiesPanel extends libPictView
|
|
|
3024
3024
|
tmpHTML += '<div class="pict-fe-props-section-divider"></div>';
|
|
3025
3025
|
tmpHTML += this._renderInputTypeProperties(tmpInputType, tmpDescriptor, tmpPanelViewRef);
|
|
3026
3026
|
|
|
3027
|
+
// Extended descriptor properties (configured via options or programmatic API)
|
|
3028
|
+
tmpHTML += this._renderExtendedDescriptorProperties(tmpDescriptor, tmpPanelViewRef);
|
|
3029
|
+
|
|
3027
3030
|
// Solver assignment and references for this input
|
|
3028
3031
|
tmpHTML += this._renderInputSolverInfo(tmpInputHash);
|
|
3029
3032
|
|
|
@@ -3749,6 +3752,195 @@ class PictViewFormEditorPropertiesPanel extends libPictView
|
|
|
3749
3752
|
this._ParentFormEditor.renderVisualEditor();
|
|
3750
3753
|
}
|
|
3751
3754
|
|
|
3755
|
+
/* -------------------------------------------------------------------------- */
|
|
3756
|
+
/* Extended Descriptor Properties */
|
|
3757
|
+
/* -------------------------------------------------------------------------- */
|
|
3758
|
+
|
|
3759
|
+
/**
|
|
3760
|
+
* Render extended descriptor properties defined via the
|
|
3761
|
+
* ExtendedDescriptorProperties configuration option or the
|
|
3762
|
+
* addExtendedDescriptorProperty() API.
|
|
3763
|
+
*
|
|
3764
|
+
* Each entry maps a display name to a dot-notation address within the
|
|
3765
|
+
* Descriptor object. For example { Name: 'Units', Address: 'PictForm.Units' }
|
|
3766
|
+
* reads from and writes to Descriptor.PictForm.Units.
|
|
3767
|
+
*
|
|
3768
|
+
* @param {object} pDescriptor - The full Descriptor object for the selected input
|
|
3769
|
+
* @param {string} pPanelViewRef - The browser-accessible view reference string
|
|
3770
|
+
* @returns {string} HTML string
|
|
3771
|
+
*/
|
|
3772
|
+
_renderExtendedDescriptorProperties(pDescriptor, pPanelViewRef)
|
|
3773
|
+
{
|
|
3774
|
+
let tmpExtended = this._ParentFormEditor._ExtendedDescriptorProperties;
|
|
3775
|
+
if (!tmpExtended || tmpExtended.length === 0)
|
|
3776
|
+
{
|
|
3777
|
+
return '';
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
let tmpHTML = '';
|
|
3781
|
+
tmpHTML += '<div class="pict-fe-props-section-divider"></div>';
|
|
3782
|
+
tmpHTML += '<div class="pict-fe-props-section-header">Extended Properties</div>';
|
|
3783
|
+
|
|
3784
|
+
for (let i = 0; i < tmpExtended.length; i++)
|
|
3785
|
+
{
|
|
3786
|
+
let tmpProp = tmpExtended[i];
|
|
3787
|
+
let tmpCurrentValue = this._resolveDescriptorAddress(pDescriptor, tmpProp.Address);
|
|
3788
|
+
let tmpDataType = tmpProp.DataType || 'String';
|
|
3789
|
+
let tmpDescription = tmpProp.Description || '';
|
|
3790
|
+
// Escape the address for safe embedding in an onclick attribute
|
|
3791
|
+
let tmpEscapedAddress = this._escapeAttr(tmpProp.Address);
|
|
3792
|
+
|
|
3793
|
+
tmpHTML += '<div class="pict-fe-props-field">';
|
|
3794
|
+
tmpHTML += `<div class="pict-fe-props-label" title="${this._escapeAttr(tmpDescription)}">${this._escapeHTML(tmpProp.Name)}</div>`;
|
|
3795
|
+
|
|
3796
|
+
if (tmpDataType === 'Boolean')
|
|
3797
|
+
{
|
|
3798
|
+
let tmpChecked = tmpCurrentValue ? ' checked' : '';
|
|
3799
|
+
tmpHTML += '<label class="pict-fe-props-checkbox-label">';
|
|
3800
|
+
tmpHTML += `<input type="checkbox" class="pict-fe-props-checkbox"${tmpChecked} onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.checked, 'Boolean')" />`;
|
|
3801
|
+
if (tmpDescription)
|
|
3802
|
+
{
|
|
3803
|
+
tmpHTML += ` ${this._escapeHTML(tmpDescription)}`;
|
|
3804
|
+
}
|
|
3805
|
+
tmpHTML += '</label>';
|
|
3806
|
+
}
|
|
3807
|
+
else if (tmpDataType === 'Number')
|
|
3808
|
+
{
|
|
3809
|
+
let tmpDisplayValue = (typeof tmpCurrentValue === 'number') ? String(tmpCurrentValue) : '';
|
|
3810
|
+
tmpHTML += `<input class="pict-fe-props-input" type="number" value="${this._escapeAttr(tmpDisplayValue)}" placeholder="${this._escapeAttr(tmpDescription)}" onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.value, 'Number')" />`;
|
|
3811
|
+
}
|
|
3812
|
+
else
|
|
3813
|
+
{
|
|
3814
|
+
let tmpDisplayValue = (typeof tmpCurrentValue === 'string') ? tmpCurrentValue : (tmpCurrentValue !== null && tmpCurrentValue !== undefined ? String(tmpCurrentValue) : '');
|
|
3815
|
+
tmpHTML += `<input class="pict-fe-props-input" type="text" value="${this._escapeAttr(tmpDisplayValue)}" placeholder="${this._escapeAttr(tmpDescription)}" onchange="${pPanelViewRef}.commitExtendedPropertyChange('${tmpEscapedAddress}', this.value, 'String')" />`;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
tmpHTML += '</div>';
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
return tmpHTML;
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
/**
|
|
3825
|
+
* Resolve a dot-notation address within a Descriptor object.
|
|
3826
|
+
*
|
|
3827
|
+
* @param {object} pDescriptor - The Descriptor object
|
|
3828
|
+
* @param {string} pAddress - Dot-notation path (e.g. 'PictForm.Units')
|
|
3829
|
+
* @returns {*} The resolved value, or undefined if not found
|
|
3830
|
+
*/
|
|
3831
|
+
_resolveDescriptorAddress(pDescriptor, pAddress)
|
|
3832
|
+
{
|
|
3833
|
+
if (!pDescriptor || typeof pAddress !== 'string')
|
|
3834
|
+
{
|
|
3835
|
+
return undefined;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
let tmpSegments = pAddress.split('.');
|
|
3839
|
+
let tmpCurrent = pDescriptor;
|
|
3840
|
+
|
|
3841
|
+
for (let i = 0; i < tmpSegments.length; i++)
|
|
3842
|
+
{
|
|
3843
|
+
if (tmpCurrent === null || tmpCurrent === undefined || typeof tmpCurrent !== 'object')
|
|
3844
|
+
{
|
|
3845
|
+
return undefined;
|
|
3846
|
+
}
|
|
3847
|
+
tmpCurrent = tmpCurrent[tmpSegments[i]];
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
return tmpCurrent;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/**
|
|
3854
|
+
* Set a value at a dot-notation address within a Descriptor object,
|
|
3855
|
+
* creating intermediate objects as needed.
|
|
3856
|
+
*
|
|
3857
|
+
* @param {object} pDescriptor - The Descriptor object
|
|
3858
|
+
* @param {string} pAddress - Dot-notation path (e.g. 'PictForm.Units')
|
|
3859
|
+
* @param {*} pValue - The value to set
|
|
3860
|
+
*/
|
|
3861
|
+
_setDescriptorAddress(pDescriptor, pAddress, pValue)
|
|
3862
|
+
{
|
|
3863
|
+
if (!pDescriptor || typeof pAddress !== 'string')
|
|
3864
|
+
{
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
let tmpSegments = pAddress.split('.');
|
|
3869
|
+
let tmpCurrent = pDescriptor;
|
|
3870
|
+
|
|
3871
|
+
// Navigate to (and create) intermediate objects
|
|
3872
|
+
for (let i = 0; i < tmpSegments.length - 1; i++)
|
|
3873
|
+
{
|
|
3874
|
+
if (!tmpCurrent.hasOwnProperty(tmpSegments[i]) || typeof tmpCurrent[tmpSegments[i]] !== 'object')
|
|
3875
|
+
{
|
|
3876
|
+
tmpCurrent[tmpSegments[i]] = {};
|
|
3877
|
+
}
|
|
3878
|
+
tmpCurrent = tmpCurrent[tmpSegments[i]];
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
let tmpFinalKey = tmpSegments[tmpSegments.length - 1];
|
|
3882
|
+
|
|
3883
|
+
if (pValue === undefined || pValue === null || pValue === '')
|
|
3884
|
+
{
|
|
3885
|
+
delete tmpCurrent[tmpFinalKey];
|
|
3886
|
+
}
|
|
3887
|
+
else
|
|
3888
|
+
{
|
|
3889
|
+
tmpCurrent[tmpFinalKey] = pValue;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
/**
|
|
3894
|
+
* Commit a change to an extended descriptor property.
|
|
3895
|
+
*
|
|
3896
|
+
* @param {string} pAddress - Dot-notation path within the Descriptor (e.g. 'PictForm.Units')
|
|
3897
|
+
* @param {*} pValue - The new value from the form control
|
|
3898
|
+
* @param {string} pDataType - 'String', 'Number', or 'Boolean'
|
|
3899
|
+
*/
|
|
3900
|
+
commitExtendedPropertyChange(pAddress, pValue, pDataType)
|
|
3901
|
+
{
|
|
3902
|
+
if (!this._SelectedInput || !this._ParentFormEditor)
|
|
3903
|
+
{
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
let tmpResolved = this._resolveSelectedDescriptor();
|
|
3908
|
+
if (!tmpResolved || !tmpResolved.Descriptor)
|
|
3909
|
+
{
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
let tmpFinalValue;
|
|
3914
|
+
|
|
3915
|
+
switch (pDataType)
|
|
3916
|
+
{
|
|
3917
|
+
case 'Boolean':
|
|
3918
|
+
tmpFinalValue = !!pValue;
|
|
3919
|
+
break;
|
|
3920
|
+
|
|
3921
|
+
case 'Number':
|
|
3922
|
+
{
|
|
3923
|
+
let tmpNumValue = parseFloat(pValue);
|
|
3924
|
+
if (isNaN(tmpNumValue) || pValue === '')
|
|
3925
|
+
{
|
|
3926
|
+
tmpFinalValue = undefined;
|
|
3927
|
+
}
|
|
3928
|
+
else
|
|
3929
|
+
{
|
|
3930
|
+
tmpFinalValue = tmpNumValue;
|
|
3931
|
+
}
|
|
3932
|
+
break;
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
default: // String
|
|
3936
|
+
tmpFinalValue = (typeof pValue === 'string' && pValue.length > 0) ? pValue : undefined;
|
|
3937
|
+
break;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
this._setDescriptorAddress(tmpResolved.Descriptor, pAddress, tmpFinalValue);
|
|
3941
|
+
this._ParentFormEditor.renderVisualEditor();
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3752
3944
|
/* -------------------------------------------------------------------------- */
|
|
3753
3945
|
/* Options Tab */
|
|
3754
3946
|
/* -------------------------------------------------------------------------- */
|
|
@@ -16,6 +16,7 @@ const libChildPictManager = require('../providers/Pict-Provider-ChildPictManager
|
|
|
16
16
|
const libPreviewCSS = require('../providers/Pict-Provider-PreviewCSS.js');
|
|
17
17
|
const libFormEditorDocumentation = require('../providers/Pict-Provider-FormEditorDocumentation.js');
|
|
18
18
|
const libManifestFactory = require('pict-section-form').ManifestFactory;
|
|
19
|
+
const libManifestConversionToCSV = require('pict-section-form').ManifestConversionToCSV;
|
|
19
20
|
|
|
20
21
|
class PictViewFormEditor extends libPictView
|
|
21
22
|
{
|
|
@@ -44,6 +45,28 @@ class PictViewFormEditor extends libPictView
|
|
|
44
45
|
this._ContentEditorView = null;
|
|
45
46
|
this._ContentEditorContext = null;
|
|
46
47
|
|
|
48
|
+
// Extended descriptor properties for the Input properties panel.
|
|
49
|
+
// Populated from options.ExtendedDescriptorProperties and can be
|
|
50
|
+
// modified at runtime via addExtendedDescriptorProperty().
|
|
51
|
+
this._ExtendedDescriptorProperties = [];
|
|
52
|
+
if (Array.isArray(tmpOptions.ExtendedDescriptorProperties))
|
|
53
|
+
{
|
|
54
|
+
for (let i = 0; i < tmpOptions.ExtendedDescriptorProperties.length; i++)
|
|
55
|
+
{
|
|
56
|
+
let tmpProp = tmpOptions.ExtendedDescriptorProperties[i];
|
|
57
|
+
if (tmpProp && typeof tmpProp.Address === 'string' && tmpProp.Address.length > 0)
|
|
58
|
+
{
|
|
59
|
+
this._ExtendedDescriptorProperties.push(
|
|
60
|
+
{
|
|
61
|
+
Name: tmpProp.Name || tmpProp.Address,
|
|
62
|
+
Address: tmpProp.Address,
|
|
63
|
+
DataType: tmpProp.DataType || 'String',
|
|
64
|
+
Description: tmpProp.Description || ''
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
// Supported Manyfest DataTypes
|
|
48
71
|
this._ManyfestDataTypes =
|
|
49
72
|
[
|
|
@@ -108,7 +131,7 @@ class PictViewFormEditor extends libPictView
|
|
|
108
131
|
);
|
|
109
132
|
this._RenderingProvider._ParentFormEditor = this;
|
|
110
133
|
|
|
111
|
-
// Create the documentation provider for the embedded help system
|
|
134
|
+
// Create the documentation provider for the embedded help system.
|
|
112
135
|
let tmpDocumentationHash = `${pServiceHash || 'FormEditor'}-Documentation`;
|
|
113
136
|
this._DocumentationProvider = this.pict.addProvider(
|
|
114
137
|
tmpDocumentationHash,
|
|
@@ -208,6 +231,12 @@ class PictViewFormEditor extends libPictView
|
|
|
208
231
|
this.fable.addServiceType('ManifestFactory', libManifestFactory);
|
|
209
232
|
}
|
|
210
233
|
|
|
234
|
+
// Register ManifestConversionToCSV service type if not already present (needed for CSV export)
|
|
235
|
+
if (!this.fable.servicesMap.hasOwnProperty('ManifestConversionToCSV'))
|
|
236
|
+
{
|
|
237
|
+
this.fable.addServiceType('ManifestConversionToCSV', libManifestConversionToCSV);
|
|
238
|
+
}
|
|
239
|
+
|
|
211
240
|
// Ensure the manifest data address exists in AppData
|
|
212
241
|
let tmpManifest = this._resolveManifestData();
|
|
213
242
|
if (!tmpManifest)
|
|
@@ -951,6 +980,167 @@ class PictViewFormEditor extends libPictView
|
|
|
951
980
|
this.pict.ContentAssignment.assignContent(`#FormEditor-ImportStatus-${tmpHash}`, tmpHTML);
|
|
952
981
|
}
|
|
953
982
|
|
|
983
|
+
/* -------------------------------------------------------------------------- */
|
|
984
|
+
/* Code Section: Export (CSV / JSON) */
|
|
985
|
+
/* -------------------------------------------------------------------------- */
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Export the current manifest as a JSON file download.
|
|
989
|
+
*/
|
|
990
|
+
exportJSON()
|
|
991
|
+
{
|
|
992
|
+
let tmpManifest = this._resolveManifestData();
|
|
993
|
+
if (!tmpManifest)
|
|
994
|
+
{
|
|
995
|
+
this._showToast('error', 'No manifest data to export.');
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
let tmpJSON = JSON.stringify(tmpManifest, null, '\t');
|
|
1000
|
+
let tmpFileName = tmpManifest.Scope || tmpManifest.Form || 'manifest';
|
|
1001
|
+
this._triggerFileDownload(tmpFileName + '.json', tmpJSON, 'application/json');
|
|
1002
|
+
this._showToast('success', `Exported JSON: ${tmpFileName}.json`);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Export the current manifest as a CSV file download.
|
|
1007
|
+
*
|
|
1008
|
+
* Uses ManifestConversionToCSV from pict-section-form to convert the
|
|
1009
|
+
* manifest to a tabular array, then formats as a CSV string.
|
|
1010
|
+
*/
|
|
1011
|
+
exportCSV()
|
|
1012
|
+
{
|
|
1013
|
+
let tmpManifest = this._resolveManifestData();
|
|
1014
|
+
if (!tmpManifest)
|
|
1015
|
+
{
|
|
1016
|
+
this._showToast('error', 'No manifest data to export.');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Instantiate the conversion service without registering it globally
|
|
1021
|
+
let tmpConverter = this.fable.instantiateServiceProviderWithoutRegistration('ManifestConversionToCSV', {}, `${this.UUID}-CSVExport`);
|
|
1022
|
+
|
|
1023
|
+
let tmpCSVDataArray = tmpConverter.createTabularArrayFromManifests(tmpManifest);
|
|
1024
|
+
|
|
1025
|
+
if (!tmpCSVDataArray || tmpCSVDataArray.length === 0)
|
|
1026
|
+
{
|
|
1027
|
+
this._showToast('error', 'Could not convert manifest to CSV.');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Convert the 2D array to a CSV string with proper escaping
|
|
1032
|
+
let tmpCSVLines = [];
|
|
1033
|
+
for (let i = 0; i < tmpCSVDataArray.length; i++)
|
|
1034
|
+
{
|
|
1035
|
+
let tmpRow = tmpCSVDataArray[i];
|
|
1036
|
+
let tmpEscapedRow = [];
|
|
1037
|
+
for (let j = 0; j < tmpRow.length; j++)
|
|
1038
|
+
{
|
|
1039
|
+
let tmpCell = tmpRow[j];
|
|
1040
|
+
if ((typeof tmpCell === 'string') && ((tmpCell.indexOf(',') >= 0) || (tmpCell.indexOf('"') >= 0) || (tmpCell.indexOf('\n') >= 0)))
|
|
1041
|
+
{
|
|
1042
|
+
tmpEscapedRow.push('"' + tmpCell.replace(/"/g, '""') + '"');
|
|
1043
|
+
}
|
|
1044
|
+
else
|
|
1045
|
+
{
|
|
1046
|
+
tmpEscapedRow.push(tmpCell);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
tmpCSVLines.push(tmpEscapedRow.join(','));
|
|
1050
|
+
}
|
|
1051
|
+
let tmpCSVString = tmpCSVLines.join('\n');
|
|
1052
|
+
|
|
1053
|
+
let tmpFileName = tmpManifest.Scope || tmpManifest.Form || 'manifest';
|
|
1054
|
+
this._triggerFileDownload(tmpFileName + '.csv', tmpCSVString, 'text/csv');
|
|
1055
|
+
this._showToast('success', `Exported CSV: ${tmpFileName}.csv`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Trigger a browser file download from an in-memory string.
|
|
1060
|
+
*
|
|
1061
|
+
* @param {string} pFileName - The suggested file name
|
|
1062
|
+
* @param {string} pContent - The file content
|
|
1063
|
+
* @param {string} pMimeType - MIME type for the blob
|
|
1064
|
+
*/
|
|
1065
|
+
_triggerFileDownload(pFileName, pContent, pMimeType)
|
|
1066
|
+
{
|
|
1067
|
+
if (typeof document === 'undefined')
|
|
1068
|
+
{
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
let tmpBlob = new Blob([pContent], { type: pMimeType });
|
|
1073
|
+
let tmpURL = URL.createObjectURL(tmpBlob);
|
|
1074
|
+
|
|
1075
|
+
let tmpLink = document.createElement('a');
|
|
1076
|
+
tmpLink.href = tmpURL;
|
|
1077
|
+
tmpLink.download = pFileName;
|
|
1078
|
+
tmpLink.style.display = 'none';
|
|
1079
|
+
document.body.appendChild(tmpLink);
|
|
1080
|
+
tmpLink.click();
|
|
1081
|
+
|
|
1082
|
+
// Clean up
|
|
1083
|
+
document.body.removeChild(tmpLink);
|
|
1084
|
+
URL.revokeObjectURL(tmpURL);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/* -------------------------------------------------------------------------- */
|
|
1088
|
+
/* Code Section: Extended Descriptor Properties */
|
|
1089
|
+
/* -------------------------------------------------------------------------- */
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Add an extended descriptor property to the Input properties panel.
|
|
1093
|
+
*
|
|
1094
|
+
* @param {string} pAddress - Dot-notation path relative to the Descriptor (e.g. 'PictForm.Units')
|
|
1095
|
+
* @param {string} [pName] - Display label (defaults to the last segment of the address)
|
|
1096
|
+
* @param {string} [pDataType] - 'String' (default), 'Number', or 'Boolean'
|
|
1097
|
+
* @param {string} [pDescription] - Tooltip / placeholder text
|
|
1098
|
+
*/
|
|
1099
|
+
addExtendedDescriptorProperty(pAddress, pName, pDataType, pDescription)
|
|
1100
|
+
{
|
|
1101
|
+
if (typeof pAddress !== 'string' || pAddress.length === 0)
|
|
1102
|
+
{
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Avoid duplicates
|
|
1107
|
+
for (let i = 0; i < this._ExtendedDescriptorProperties.length; i++)
|
|
1108
|
+
{
|
|
1109
|
+
if (this._ExtendedDescriptorProperties[i].Address === pAddress)
|
|
1110
|
+
{
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
let tmpSegments = pAddress.split('.');
|
|
1116
|
+
let tmpDefaultName = tmpSegments[tmpSegments.length - 1];
|
|
1117
|
+
|
|
1118
|
+
this._ExtendedDescriptorProperties.push(
|
|
1119
|
+
{
|
|
1120
|
+
Name: pName || tmpDefaultName,
|
|
1121
|
+
Address: pAddress,
|
|
1122
|
+
DataType: pDataType || 'String',
|
|
1123
|
+
Description: pDescription || ''
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Remove an extended descriptor property by address.
|
|
1129
|
+
*
|
|
1130
|
+
* @param {string} pAddress - Dot-notation path to remove
|
|
1131
|
+
*/
|
|
1132
|
+
removeExtendedDescriptorProperty(pAddress)
|
|
1133
|
+
{
|
|
1134
|
+
for (let i = 0; i < this._ExtendedDescriptorProperties.length; i++)
|
|
1135
|
+
{
|
|
1136
|
+
if (this._ExtendedDescriptorProperties[i].Address === pAddress)
|
|
1137
|
+
{
|
|
1138
|
+
this._ExtendedDescriptorProperties.splice(i, 1);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
954
1144
|
/**
|
|
955
1145
|
* Show a floating toast notification inside the form editor.
|
|
956
1146
|
*
|